@hasna/models 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,730 @@
1
+ // @bun
2
+ // src/storage.ts
3
+ import { mkdirSync } from "fs";
4
+ import { dirname } from "path";
5
+ import { Database } from "bun:sqlite";
6
+
7
+ // src/paths.ts
8
+ import { homedir } from "os";
9
+ import { join } from "path";
10
+ function getModelsHome() {
11
+ return process.env["HASNA_MODELS_HOME"] || join(homedir(), ".hasna", "models");
12
+ }
13
+ function getDbPath() {
14
+ return process.env["HASNA_MODELS_DB"] || join(getModelsHome(), "models.db");
15
+ }
16
+ function getAuthConfigPath() {
17
+ return join(getModelsHome(), "auth.json");
18
+ }
19
+ function getCacheRoot() {
20
+ return process.env["HASNA_MODELS_CACHE"] || join(getModelsHome(), "cache");
21
+ }
22
+ function getInstallRoot() {
23
+ return process.env["HASNA_MODELS_INSTALLS"] || join(getModelsHome(), "installs");
24
+ }
25
+
26
+ // src/storage.ts
27
+ var SCHEMA_VERSION = 1;
28
+
29
+ class ModelsStore {
30
+ db;
31
+ constructor(path = getDbPath()) {
32
+ mkdirSync(dirname(path), { recursive: true });
33
+ this.db = new Database(path, { create: true });
34
+ this.db.run("PRAGMA busy_timeout = 5000");
35
+ this.db.run("PRAGMA journal_mode = WAL");
36
+ this.migrate();
37
+ }
38
+ close() {
39
+ this.db.close();
40
+ }
41
+ migrate() {
42
+ this.db.run(`
43
+ CREATE TABLE IF NOT EXISTS schema_meta (
44
+ key TEXT PRIMARY KEY,
45
+ value INTEGER NOT NULL
46
+ )
47
+ `);
48
+ const row = this.db.query("SELECT value FROM schema_meta WHERE key = 'schema_version' LIMIT 1").get();
49
+ const currentVersion = row ? Number(row.value) : 0;
50
+ if (currentVersion >= SCHEMA_VERSION)
51
+ return;
52
+ this.db.run(`
53
+ CREATE TABLE IF NOT EXISTS catalog_entries (
54
+ provider TEXT NOT NULL,
55
+ entity_kind TEXT NOT NULL,
56
+ repo_id TEXT NOT NULL,
57
+ revision TEXT NOT NULL,
58
+ title TEXT NOT NULL,
59
+ author TEXT,
60
+ task TEXT,
61
+ library_name TEXT,
62
+ license TEXT,
63
+ gated INTEGER NOT NULL,
64
+ private INTEGER NOT NULL,
65
+ downloads INTEGER,
66
+ likes INTEGER,
67
+ tags_json TEXT NOT NULL,
68
+ metadata_json TEXT NOT NULL,
69
+ canonical_url TEXT NOT NULL,
70
+ last_modified TEXT,
71
+ indexed_at TEXT NOT NULL,
72
+ PRIMARY KEY (provider, entity_kind, repo_id, revision)
73
+ )
74
+ `);
75
+ this.db.run(`
76
+ CREATE TABLE IF NOT EXISTS remote_files (
77
+ provider TEXT NOT NULL,
78
+ entity_kind TEXT NOT NULL,
79
+ repo_id TEXT NOT NULL,
80
+ revision TEXT NOT NULL,
81
+ path TEXT NOT NULL,
82
+ size INTEGER,
83
+ oid TEXT,
84
+ lfs_oid TEXT,
85
+ format TEXT,
86
+ download_url TEXT NOT NULL,
87
+ metadata_json TEXT NOT NULL,
88
+ indexed_at TEXT NOT NULL,
89
+ PRIMARY KEY (provider, entity_kind, repo_id, revision, path)
90
+ )
91
+ `);
92
+ this.db.run(`
93
+ CREATE TABLE IF NOT EXISTS installs (
94
+ id TEXT PRIMARY KEY,
95
+ provider TEXT NOT NULL,
96
+ entity_kind TEXT NOT NULL,
97
+ repo_id TEXT NOT NULL,
98
+ revision TEXT NOT NULL,
99
+ install_path TEXT NOT NULL,
100
+ bytes INTEGER NOT NULL,
101
+ files_json TEXT NOT NULL,
102
+ status TEXT NOT NULL,
103
+ runtime TEXT,
104
+ metadata_json TEXT NOT NULL,
105
+ created_at TEXT NOT NULL,
106
+ updated_at TEXT NOT NULL
107
+ )
108
+ `);
109
+ this.db.run("CREATE INDEX IF NOT EXISTS idx_catalog_downloads ON catalog_entries(downloads DESC)");
110
+ this.db.run("CREATE INDEX IF NOT EXISTS idx_catalog_task ON catalog_entries(task)");
111
+ this.db.run("CREATE INDEX IF NOT EXISTS idx_files_repo ON remote_files(provider, entity_kind, repo_id)");
112
+ this.db.run("INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', ?)", [SCHEMA_VERSION]);
113
+ }
114
+ upsertCatalog(entries) {
115
+ const now = new Date().toISOString();
116
+ const stmt = this.db.prepare(`
117
+ INSERT INTO catalog_entries (
118
+ provider, entity_kind, repo_id, revision, title, author, task, library_name,
119
+ license, gated, private, downloads, likes, tags_json, metadata_json,
120
+ canonical_url, last_modified, indexed_at
121
+ )
122
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
123
+ ON CONFLICT(provider, entity_kind, repo_id, revision) DO UPDATE SET
124
+ title=excluded.title,
125
+ author=excluded.author,
126
+ task=excluded.task,
127
+ library_name=excluded.library_name,
128
+ license=excluded.license,
129
+ gated=excluded.gated,
130
+ private=excluded.private,
131
+ downloads=excluded.downloads,
132
+ likes=excluded.likes,
133
+ tags_json=excluded.tags_json,
134
+ metadata_json=excluded.metadata_json,
135
+ canonical_url=excluded.canonical_url,
136
+ last_modified=excluded.last_modified,
137
+ indexed_at=excluded.indexed_at
138
+ `);
139
+ const tx = this.db.transaction((items) => {
140
+ for (const entry of items) {
141
+ stmt.run(entry.provider, entry.entityKind, entry.repoId, entry.revision, entry.title, entry.author ?? null, entry.task ?? null, entry.libraryName ?? null, entry.license ?? null, entry.gated ? 1 : 0, entry.private ? 1 : 0, entry.downloads ?? null, entry.likes ?? null, JSON.stringify(entry.tags), JSON.stringify(entry.metadata), entry.canonicalUrl, entry.lastModified ?? null, now);
142
+ }
143
+ });
144
+ tx(entries);
145
+ return entries.length;
146
+ }
147
+ upsertFiles(files) {
148
+ const now = new Date().toISOString();
149
+ const stmt = this.db.prepare(`
150
+ INSERT INTO remote_files (
151
+ provider, entity_kind, repo_id, revision, path, size, oid, lfs_oid,
152
+ format, download_url, metadata_json, indexed_at
153
+ )
154
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
155
+ ON CONFLICT(provider, entity_kind, repo_id, revision, path) DO UPDATE SET
156
+ size=excluded.size,
157
+ oid=excluded.oid,
158
+ lfs_oid=excluded.lfs_oid,
159
+ format=excluded.format,
160
+ download_url=excluded.download_url,
161
+ metadata_json=excluded.metadata_json,
162
+ indexed_at=excluded.indexed_at
163
+ `);
164
+ const tx = this.db.transaction((items) => {
165
+ for (const file of items) {
166
+ stmt.run(file.provider, file.entityKind, file.repoId, file.revision, file.path, file.size ?? null, file.oid ?? null, file.lfsOid ?? null, file.format ?? null, file.downloadUrl, JSON.stringify(file.metadata), now);
167
+ }
168
+ });
169
+ tx(files);
170
+ return files.length;
171
+ }
172
+ recordInstall(artifact, metadata = {}) {
173
+ this.db.prepare(`
174
+ INSERT INTO installs (
175
+ id, provider, entity_kind, repo_id, revision, install_path, bytes,
176
+ files_json, status, runtime, metadata_json, created_at, updated_at
177
+ )
178
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
179
+ ON CONFLICT(id) DO UPDATE SET
180
+ bytes=excluded.bytes,
181
+ files_json=excluded.files_json,
182
+ status=excluded.status,
183
+ runtime=excluded.runtime,
184
+ metadata_json=excluded.metadata_json,
185
+ updated_at=excluded.updated_at
186
+ `).run(artifact.id, artifact.provider, artifact.entityKind, artifact.repoId, artifact.revision, artifact.installPath, artifact.bytes, JSON.stringify(artifact.files), artifact.status, null, JSON.stringify(metadata), artifact.createdAt, artifact.updatedAt);
187
+ return artifact;
188
+ }
189
+ listInstalls() {
190
+ const rows = this.db.query("SELECT * FROM installs ORDER BY updated_at DESC").all();
191
+ return rows.map((row) => ({
192
+ id: String(row.id),
193
+ provider: String(row.provider),
194
+ entityKind: String(row.entity_kind),
195
+ repoId: String(row.repo_id),
196
+ revision: String(row.revision),
197
+ installPath: String(row.install_path),
198
+ bytes: Number(row.bytes),
199
+ files: JSON.parse(String(row.files_json)),
200
+ status: String(row.status),
201
+ createdAt: String(row.created_at),
202
+ updatedAt: String(row.updated_at)
203
+ }));
204
+ }
205
+ findInstall(repoIdOrId) {
206
+ const row = this.db.query("SELECT * FROM installs WHERE id = ? OR repo_id = ? ORDER BY updated_at DESC LIMIT 1").get(repoIdOrId, repoIdOrId);
207
+ if (!row)
208
+ return null;
209
+ return this.listInstalls().find((install) => install.id === row.id) ?? null;
210
+ }
211
+ deleteInstall(id) {
212
+ const result = this.db.prepare("DELETE FROM installs WHERE id = ?").run(id);
213
+ return result.changes > 0;
214
+ }
215
+ catalogStats() {
216
+ const one = (table) => {
217
+ const row = this.db.query(`SELECT COUNT(*) as count FROM ${table}`).get();
218
+ return Number(row?.count ?? 0);
219
+ };
220
+ return {
221
+ catalogEntries: one("catalog_entries"),
222
+ remoteFiles: one("remote_files"),
223
+ installs: one("installs")
224
+ };
225
+ }
226
+ topCatalog(limit = 20) {
227
+ const rows = this.db.query("SELECT * FROM catalog_entries ORDER BY COALESCE(downloads, 0) DESC, COALESCE(likes, 0) DESC LIMIT ?").all(limit);
228
+ return rows.map((row) => ({
229
+ provider: String(row.provider),
230
+ entityKind: String(row.entity_kind),
231
+ repoId: String(row.repo_id),
232
+ revision: String(row.revision),
233
+ canonicalUrl: String(row.canonical_url),
234
+ title: String(row.title),
235
+ author: row.author == null ? null : String(row.author),
236
+ task: row.task == null ? null : String(row.task),
237
+ libraryName: row.library_name == null ? null : String(row.library_name),
238
+ license: row.license == null ? null : String(row.license),
239
+ gated: Boolean(row.gated),
240
+ private: Boolean(row.private),
241
+ downloads: row.downloads == null ? null : Number(row.downloads),
242
+ likes: row.likes == null ? null : Number(row.likes),
243
+ tags: JSON.parse(String(row.tags_json)),
244
+ lastModified: row.last_modified == null ? null : String(row.last_modified),
245
+ metadata: JSON.parse(String(row.metadata_json))
246
+ }));
247
+ }
248
+ }
249
+ function createStore(path) {
250
+ return new ModelsStore(path);
251
+ }
252
+ // src/ref.ts
253
+ var ENTITY_KINDS = new Set(["model", "dataset", "space"]);
254
+ var PROVIDER_ALIASES = new Map([
255
+ ["hf", "huggingface"],
256
+ ["huggingface", "huggingface"]
257
+ ]);
258
+ function parseEntityKind(value) {
259
+ if (ENTITY_KINDS.has(value))
260
+ return value;
261
+ throw new Error(`Unsupported entity kind: ${value}. Expected one of: ${[...ENTITY_KINDS].join(", ")}`);
262
+ }
263
+ function parseProviderRef(input, defaultKind = "model") {
264
+ const trimmed = input.trim();
265
+ if (!trimmed)
266
+ throw new Error("model or dataset ref is required");
267
+ let provider = "huggingface";
268
+ let entityKind = parseEntityKind(defaultKind);
269
+ let rest = trimmed;
270
+ const prefixMatch = rest.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):(.*)$/);
271
+ if (prefixMatch && !prefixMatch[1].includes("/")) {
272
+ const alias = PROVIDER_ALIASES.get(prefixMatch[1]);
273
+ if (alias) {
274
+ provider = alias;
275
+ rest = prefixMatch[2];
276
+ } else if (["model", "dataset", "space"].includes(prefixMatch[1])) {
277
+ entityKind = parseEntityKind(prefixMatch[1]);
278
+ rest = prefixMatch[2];
279
+ } else {
280
+ throw new Error(`Unsupported provider: ${prefixMatch[1]}`);
281
+ }
282
+ }
283
+ if (rest.startsWith("model:")) {
284
+ entityKind = parseEntityKind("model");
285
+ rest = rest.slice("model:".length);
286
+ } else if (rest.startsWith("dataset:")) {
287
+ entityKind = parseEntityKind("dataset");
288
+ rest = rest.slice("dataset:".length);
289
+ } else if (rest.startsWith("space:")) {
290
+ entityKind = parseEntityKind("space");
291
+ rest = rest.slice("space:".length);
292
+ }
293
+ if (rest.includes(":")) {
294
+ throw new Error(`Unsupported ref prefix in: ${input}`);
295
+ }
296
+ let revision = "main";
297
+ const atIndex = rest.lastIndexOf("@");
298
+ if (atIndex > 0) {
299
+ revision = rest.slice(atIndex + 1);
300
+ rest = rest.slice(0, atIndex);
301
+ if (!revision.trim())
302
+ throw new Error(`Revision cannot be empty in ref: ${input}`);
303
+ } else if (rest.endsWith("@")) {
304
+ throw new Error(`Revision cannot be empty in ref: ${input}`);
305
+ }
306
+ if (!rest.includes("/")) {
307
+ throw new Error(`Expected a namespaced repo id such as owner/name, got ${input}`);
308
+ }
309
+ return { provider, entityKind, repoId: rest, revision };
310
+ }
311
+ function formatProviderRef(ref) {
312
+ const prefix = ref.provider === "huggingface" ? "hf" : ref.provider;
313
+ const kind = ref.entityKind === "model" ? "" : `${ref.entityKind}:`;
314
+ return `${prefix}:${kind}${ref.repoId}@${ref.revision}`;
315
+ }
316
+ function encodeRepoId(repoId) {
317
+ return repoId.split("/").map(encodeURIComponent).join("/");
318
+ }
319
+ function safePathSegment(value) {
320
+ return value.replace(/[^a-zA-Z0-9._-]+/g, "__").replace(/^_+|_+$/g, "") || "unnamed";
321
+ }
322
+ // src/auth.ts
323
+ import { mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
324
+ import { dirname as dirname2 } from "path";
325
+ import { spawnSync } from "child_process";
326
+ var HF_ENV_KEYS = [
327
+ "HF_TOKEN",
328
+ "HUGGINGFACE_HUB_TOKEN",
329
+ "HUGGING_FACE_HUB_TOKEN",
330
+ "HUGGINGFACE_TOKEN"
331
+ ];
332
+ var DEFAULT_HF_SECRET_KEYS = [
333
+ "huggingface/token",
334
+ "huggingface/live/token",
335
+ "hf/token"
336
+ ];
337
+ var cachedResolution = null;
338
+ function readAuthConfig() {
339
+ try {
340
+ return JSON.parse(readFileSync(getAuthConfigPath(), "utf8"));
341
+ } catch {
342
+ return {};
343
+ }
344
+ }
345
+ function writeAuthConfig(config) {
346
+ const path = getAuthConfigPath();
347
+ mkdirSync2(dirname2(path), { recursive: true });
348
+ writeFileSync(path, `${JSON.stringify(config, null, 2)}
349
+ `);
350
+ }
351
+ function fromEnv() {
352
+ for (const key of HF_ENV_KEYS) {
353
+ const value = process.env[key]?.trim();
354
+ if (value)
355
+ return value;
356
+ }
357
+ return null;
358
+ }
359
+ function readSecret(key) {
360
+ const result = spawnSync("secrets", ["get", key], {
361
+ encoding: "utf8",
362
+ env: process.env,
363
+ timeout: 1e4,
364
+ stdio: ["ignore", "pipe", "pipe"]
365
+ });
366
+ if (result.status !== 0)
367
+ return null;
368
+ const value = result.stdout.trim();
369
+ return value || null;
370
+ }
371
+ function secretCandidates(config) {
372
+ const configured = [
373
+ process.env["HASNA_MODELS_HF_SECRET_KEY"],
374
+ process.env["HF_SECRET_KEY"],
375
+ config.huggingface?.secretKey
376
+ ].filter((value) => Boolean(value?.trim()));
377
+ return [...new Set([...configured, ...DEFAULT_HF_SECRET_KEYS])];
378
+ }
379
+ function redactAuthStatus(status) {
380
+ const { secretKey: _secretKey, ...rest } = status;
381
+ return rest;
382
+ }
383
+ function resolveHuggingFaceToken() {
384
+ if (cachedResolution)
385
+ return cachedResolution;
386
+ const envToken = fromEnv();
387
+ if (envToken) {
388
+ cachedResolution = { token: envToken, status: { provider: "huggingface", available: true, source: "env" } };
389
+ return cachedResolution;
390
+ }
391
+ const config = readAuthConfig();
392
+ const configToken = config.huggingface?.token?.trim();
393
+ if (configToken) {
394
+ cachedResolution = { token: configToken, status: { provider: "huggingface", available: true, source: "config" } };
395
+ return cachedResolution;
396
+ }
397
+ for (const key of secretCandidates(config)) {
398
+ const token = readSecret(key);
399
+ if (token) {
400
+ cachedResolution = {
401
+ token,
402
+ status: {
403
+ provider: "huggingface",
404
+ available: true,
405
+ source: "secrets",
406
+ secretKey: key
407
+ }
408
+ };
409
+ return cachedResolution;
410
+ }
411
+ }
412
+ cachedResolution = { token: null, status: { provider: "huggingface", available: false, source: "none" } };
413
+ return cachedResolution;
414
+ }
415
+ function getHuggingFaceAuthStatus() {
416
+ return redactAuthStatus(resolveHuggingFaceToken().status);
417
+ }
418
+ function saveHuggingFaceSecretRef(secretKey) {
419
+ const config = readAuthConfig();
420
+ config.huggingface = { ...config.huggingface, secretKey };
421
+ delete config.huggingface.token;
422
+ writeAuthConfig(config);
423
+ cachedResolution = null;
424
+ return redactAuthStatus({ provider: "huggingface", available: Boolean(readSecret(secretKey)), source: "secrets", secretKey });
425
+ }
426
+ // src/huggingface.ts
427
+ import { createWriteStream, mkdirSync as mkdirSync3, renameSync, unlinkSync } from "fs";
428
+ import { dirname as dirname3, isAbsolute, join as join2, resolve, sep } from "path";
429
+ import { pipeline } from "stream/promises";
430
+ import { Readable } from "stream";
431
+ import { randomUUID } from "crypto";
432
+ var HF_ENDPOINT = process.env["HF_ENDPOINT"] || "https://huggingface.co";
433
+ function apiBase() {
434
+ return HF_ENDPOINT.replace(/\/+$/, "");
435
+ }
436
+ function headers() {
437
+ const { token } = resolveHuggingFaceToken();
438
+ return token ? { Authorization: `Bearer ${token}` } : {};
439
+ }
440
+ async function hfJson(path, init) {
441
+ const response = await fetch(`${apiBase()}${path}`, {
442
+ ...init,
443
+ headers: {
444
+ ...headers(),
445
+ ...init?.headers ?? {}
446
+ }
447
+ });
448
+ if (!response.ok) {
449
+ const text = await response.text().catch(() => "");
450
+ throw new Error(`Hugging Face request failed ${response.status} ${response.statusText}: ${text.slice(0, 300)}`);
451
+ }
452
+ return response.json();
453
+ }
454
+ function apiKind(kind) {
455
+ if (kind === "dataset")
456
+ return "datasets";
457
+ if (kind === "space")
458
+ return "spaces";
459
+ return "models";
460
+ }
461
+ function canonicalUrl(kind, repoId) {
462
+ const base = apiBase();
463
+ if (kind === "dataset")
464
+ return `${base}/datasets/${repoId}`;
465
+ if (kind === "space")
466
+ return `${base}/spaces/${repoId}`;
467
+ return `${base}/${repoId}`;
468
+ }
469
+ function extractLicense(tags, cardData) {
470
+ const licenseTag = tags.find((tag) => tag.startsWith("license:"));
471
+ if (licenseTag)
472
+ return licenseTag.slice("license:".length);
473
+ if (cardData && typeof cardData === "object" && "license" in cardData) {
474
+ const raw = cardData.license;
475
+ if (Array.isArray(raw))
476
+ return raw.map(String).join(",");
477
+ if (typeof raw === "string")
478
+ return raw;
479
+ }
480
+ return null;
481
+ }
482
+ function detectFormat(path) {
483
+ const lower = path.toLowerCase();
484
+ if (lower.endsWith(".gguf"))
485
+ return "gguf";
486
+ if (lower.endsWith(".safetensors"))
487
+ return "safetensors";
488
+ if (lower.endsWith(".bin"))
489
+ return "pytorch-bin";
490
+ if (lower.endsWith(".onnx"))
491
+ return "onnx";
492
+ if (lower.endsWith(".h5"))
493
+ return "tensorflow";
494
+ if (lower.endsWith(".msgpack"))
495
+ return "flax";
496
+ if (lower.endsWith(".parquet"))
497
+ return "parquet";
498
+ if (lower.endsWith(".csv"))
499
+ return "csv";
500
+ if (lower.endsWith(".tsv"))
501
+ return "tsv";
502
+ if (lower.endsWith(".txt"))
503
+ return "text";
504
+ if (lower.endsWith(".arrow"))
505
+ return "arrow";
506
+ if (lower.endsWith(".jsonl"))
507
+ return "jsonl";
508
+ if (lower.endsWith(".json"))
509
+ return "json";
510
+ return null;
511
+ }
512
+ function safeDestinationPath(root, filePath) {
513
+ if (isAbsolute(filePath))
514
+ throw new Error(`Unsafe remote file path: ${filePath}`);
515
+ if (/^[a-zA-Z]:[\\/]/.test(filePath))
516
+ throw new Error(`Unsafe remote file path: ${filePath}`);
517
+ const segments = filePath.split(/[\\/]+/);
518
+ if (segments.some((segment) => segment === ".." || segment === "")) {
519
+ throw new Error(`Unsafe remote file path: ${filePath}`);
520
+ }
521
+ const resolvedRoot = resolve(root);
522
+ const destination = resolve(resolvedRoot, ...segments);
523
+ if (destination !== resolvedRoot && !destination.startsWith(`${resolvedRoot}${sep}`)) {
524
+ throw new Error(`Remote file path escapes install root: ${filePath}`);
525
+ }
526
+ return destination;
527
+ }
528
+ function normalizeEntry(raw, kind) {
529
+ const repoId = String(raw.id ?? raw.modelId ?? "");
530
+ const tags = Array.isArray(raw.tags) ? raw.tags.map(String) : [];
531
+ const cardData = raw.cardData;
532
+ return {
533
+ provider: "huggingface",
534
+ entityKind: kind,
535
+ repoId,
536
+ revision: String(raw.sha ?? "main"),
537
+ canonicalUrl: canonicalUrl(kind, repoId),
538
+ title: repoId,
539
+ author: typeof raw.author === "string" ? raw.author : repoId.split("/")[0],
540
+ task: typeof raw.pipeline_tag === "string" ? raw.pipeline_tag : null,
541
+ libraryName: typeof raw.library_name === "string" ? raw.library_name : null,
542
+ license: extractLicense(tags, cardData),
543
+ gated: Boolean(raw.gated),
544
+ private: Boolean(raw.private),
545
+ downloads: typeof raw.downloads === "number" ? raw.downloads : null,
546
+ likes: typeof raw.likes === "number" ? raw.likes : null,
547
+ tags,
548
+ lastModified: typeof raw.lastModified === "string" ? raw.lastModified : null,
549
+ metadata: raw
550
+ };
551
+ }
552
+ function normalizeTreeFile(raw, ref) {
553
+ if (raw.type && raw.type !== "file")
554
+ return null;
555
+ const path = String(raw.path ?? raw.rfilename ?? "");
556
+ if (!path)
557
+ return null;
558
+ const lfs = raw.lfs && typeof raw.lfs === "object" ? raw.lfs : {};
559
+ const size = typeof raw.size === "number" ? raw.size : typeof lfs.size === "number" ? lfs.size : null;
560
+ return {
561
+ provider: "huggingface",
562
+ entityKind: ref.entityKind,
563
+ repoId: ref.repoId,
564
+ revision: ref.revision,
565
+ path,
566
+ size,
567
+ oid: typeof raw.oid === "string" ? raw.oid : null,
568
+ lfsOid: typeof lfs.oid === "string" ? lfs.oid : null,
569
+ format: detectFormat(path),
570
+ downloadUrl: fileDownloadUrl(ref, path),
571
+ metadata: raw
572
+ };
573
+ }
574
+ function normalizeSibling(raw, ref) {
575
+ const path = String(raw.rfilename ?? raw.path ?? "");
576
+ if (!path)
577
+ return null;
578
+ return {
579
+ provider: "huggingface",
580
+ entityKind: ref.entityKind,
581
+ repoId: ref.repoId,
582
+ revision: ref.revision,
583
+ path,
584
+ size: typeof raw.size === "number" ? raw.size : null,
585
+ oid: typeof raw.oid === "string" ? raw.oid : null,
586
+ lfsOid: null,
587
+ format: detectFormat(path),
588
+ downloadUrl: fileDownloadUrl(ref, path),
589
+ metadata: raw
590
+ };
591
+ }
592
+ function fileDownloadUrl(ref, path) {
593
+ const encodedRepo = encodeRepoId(ref.repoId);
594
+ const encodedRevision = encodeURIComponent(ref.revision || "main");
595
+ const encodedPath = path.split("/").map(encodeURIComponent).join("/");
596
+ const prefix = ref.entityKind === "dataset" ? "datasets/" : ref.entityKind === "space" ? "spaces/" : "";
597
+ return `${apiBase()}/${prefix}${encodedRepo}/resolve/${encodedRevision}/${encodedPath}`;
598
+ }
599
+ async function searchHuggingFace(input = {}) {
600
+ const kind = input.entityKind ?? "model";
601
+ const params = new URLSearchParams;
602
+ if (input.query)
603
+ params.set("search", input.query);
604
+ params.set("limit", String(input.limit ?? 20));
605
+ params.set("full", "1");
606
+ params.set("sort", input.sort ?? "downloads");
607
+ params.set("direction", input.direction === "asc" ? "1" : "-1");
608
+ if (input.task)
609
+ params.append("filter", input.task);
610
+ if (input.license)
611
+ params.append("filter", `license:${input.license}`);
612
+ for (const tag of input.tags ?? [])
613
+ params.append("filter", tag);
614
+ const raw = await hfJson(`/api/${apiKind(kind)}?${params.toString()}`);
615
+ return raw.map((entry) => normalizeEntry(entry, kind)).filter((entry) => Boolean(entry.repoId));
616
+ }
617
+ async function getHuggingFaceInfo(refOrInput, defaultKind = "model") {
618
+ const ref = typeof refOrInput === "string" ? parseProviderRef(refOrInput, defaultKind) : refOrInput;
619
+ const raw = await hfJson(`/api/${apiKind(ref.entityKind)}/${encodeRepoId(ref.repoId)}`);
620
+ return normalizeEntry(raw, ref.entityKind);
621
+ }
622
+ async function listHuggingFaceFiles(refOrInput, defaultKind = "model") {
623
+ const ref = typeof refOrInput === "string" ? parseProviderRef(refOrInput, defaultKind) : refOrInput;
624
+ const treePath = `/api/${apiKind(ref.entityKind)}/${encodeRepoId(ref.repoId)}/tree/${encodeURIComponent(ref.revision)}?recursive=1&expand=1`;
625
+ try {
626
+ const raw = await hfJson(treePath);
627
+ return raw.map((entry) => normalizeTreeFile(entry, ref)).filter((entry) => Boolean(entry));
628
+ } catch {
629
+ const info = await hfJson(`/api/${apiKind(ref.entityKind)}/${encodeRepoId(ref.repoId)}`);
630
+ const siblings = Array.isArray(info.siblings) ? info.siblings : [];
631
+ return siblings.map((entry) => normalizeSibling(entry, ref)).filter((entry) => Boolean(entry));
632
+ }
633
+ }
634
+ function matchesFilePattern(path, pattern) {
635
+ const normalized = pattern.trim();
636
+ if (!normalized)
637
+ return false;
638
+ if (normalized === path)
639
+ return true;
640
+ if (normalized.endsWith("/"))
641
+ return path.startsWith(normalized);
642
+ if (normalized.includes("*")) {
643
+ const regex = new RegExp(`^${normalized.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*")}$`);
644
+ return regex.test(path);
645
+ }
646
+ if (!normalized.includes("/")) {
647
+ return path.split("/").pop() === normalized;
648
+ }
649
+ return false;
650
+ }
651
+ function matchAny(path, patterns) {
652
+ if (patterns.length === 0)
653
+ return true;
654
+ return patterns.some((pattern) => matchesFilePattern(path, pattern));
655
+ }
656
+ async function createDownloadPlan(input) {
657
+ const include = input.include ?? [];
658
+ const exclude = input.exclude ?? [];
659
+ const files = (await listHuggingFaceFiles(input.ref)).filter((file) => include.length === 0 || matchAny(file.path, include)).filter((file) => exclude.length === 0 || !matchAny(file.path, exclude));
660
+ const knownBytes = files.reduce((sum, file) => sum + (file.size ?? 0), 0);
661
+ const unknownSizeFiles = files.filter((file) => file.size == null).map((file) => file.path);
662
+ const totalBytes = unknownSizeFiles.length > 0 ? null : knownBytes;
663
+ const maxBytes = input.maxBytes ?? null;
664
+ const destinationRoot = input.destinationRoot ?? join2(getInstallRoot(), input.ref.provider, input.ref.entityKind, safePathSegment(input.ref.repoId), safePathSegment(input.ref.revision));
665
+ return {
666
+ ref: input.ref,
667
+ files,
668
+ totalBytes,
669
+ unknownSizeFiles,
670
+ destinationRoot,
671
+ exceedsMaxBytes: maxBytes != null && totalBytes != null && totalBytes > maxBytes,
672
+ maxBytes
673
+ };
674
+ }
675
+ async function downloadPlannedFiles(plan) {
676
+ if (plan.exceedsMaxBytes) {
677
+ throw new Error(`Download plan exceeds max bytes: ${plan.totalBytes} > ${plan.maxBytes}`);
678
+ }
679
+ if (plan.maxBytes != null && plan.unknownSizeFiles.length > 0) {
680
+ throw new Error(`Download plan has unknown-size files under a byte cap: ${plan.unknownSizeFiles.join(", ")}`);
681
+ }
682
+ const downloaded = [];
683
+ for (const file of plan.files) {
684
+ const destination = safeDestinationPath(plan.destinationRoot, file.path);
685
+ const tempDestination = `${destination}.partial-${randomUUID()}`;
686
+ mkdirSync3(dirname3(destination), { recursive: true });
687
+ const response = await fetch(file.downloadUrl, { headers: headers(), redirect: "follow" });
688
+ if (!response.ok || !response.body) {
689
+ const text = await response.text().catch(() => "");
690
+ throw new Error(`Failed to download ${file.path}: ${response.status} ${response.statusText} ${text.slice(0, 200)}`);
691
+ }
692
+ try {
693
+ await pipeline(Readable.fromWeb(response.body), createWriteStream(tempDestination));
694
+ renameSync(tempDestination, destination);
695
+ const bytes = file.size ?? Bun.file(destination).size;
696
+ downloaded.push({ path: file.path, bytes, destination });
697
+ } catch (error) {
698
+ try {
699
+ unlinkSync(tempDestination);
700
+ } catch {}
701
+ throw error;
702
+ }
703
+ }
704
+ return downloaded;
705
+ }
706
+ export {
707
+ searchHuggingFace,
708
+ saveHuggingFaceSecretRef,
709
+ safePathSegment,
710
+ resolveHuggingFaceToken,
711
+ redactAuthStatus,
712
+ parseProviderRef,
713
+ parseEntityKind,
714
+ matchesFilePattern,
715
+ listHuggingFaceFiles,
716
+ getModelsHome,
717
+ getInstallRoot,
718
+ getHuggingFaceInfo,
719
+ getHuggingFaceAuthStatus,
720
+ getDbPath,
721
+ getCacheRoot,
722
+ getAuthConfigPath,
723
+ formatProviderRef,
724
+ fileDownloadUrl,
725
+ encodeRepoId,
726
+ downloadPlannedFiles,
727
+ createStore,
728
+ createDownloadPlan,
729
+ ModelsStore
730
+ };