@sheepbun/yips 0.1.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.
Files changed (99) hide show
  1. package/dist/agent/commands/command-catalog.js +243 -0
  2. package/dist/agent/commands/commands.js +418 -0
  3. package/dist/agent/conductor.js +118 -0
  4. package/dist/agent/context/code-context.js +68 -0
  5. package/dist/agent/context/memory-store.js +159 -0
  6. package/dist/agent/context/session-store.js +211 -0
  7. package/dist/agent/protocol/tool-protocol.js +160 -0
  8. package/dist/agent/skills/skills.js +327 -0
  9. package/dist/agent/tools/tool-executor.js +415 -0
  10. package/dist/agent/tools/tool-safety.js +52 -0
  11. package/dist/app/index.js +35 -0
  12. package/dist/app/repl.js +105 -0
  13. package/dist/app/update-check.js +132 -0
  14. package/dist/app/version.js +51 -0
  15. package/dist/code-context.js +68 -0
  16. package/dist/colors.js +204 -0
  17. package/dist/command-catalog.js +242 -0
  18. package/dist/commands.js +350 -0
  19. package/dist/conductor.js +94 -0
  20. package/dist/config/config.js +335 -0
  21. package/dist/config/hooks.js +187 -0
  22. package/dist/config.js +335 -0
  23. package/dist/downloader-state.js +302 -0
  24. package/dist/downloader-ui.js +289 -0
  25. package/dist/gateway/adapters/discord.js +108 -0
  26. package/dist/gateway/adapters/formatting.js +96 -0
  27. package/dist/gateway/adapters/telegram.js +106 -0
  28. package/dist/gateway/adapters/types.js +2 -0
  29. package/dist/gateway/adapters/whatsapp.js +124 -0
  30. package/dist/gateway/auth-policy.js +66 -0
  31. package/dist/gateway/core.js +87 -0
  32. package/dist/gateway/headless-conductor.js +328 -0
  33. package/dist/gateway/message-router.js +23 -0
  34. package/dist/gateway/rate-limiter.js +48 -0
  35. package/dist/gateway/runtime/backend-policy.js +18 -0
  36. package/dist/gateway/runtime/discord-bot.js +104 -0
  37. package/dist/gateway/runtime/discord-main.js +69 -0
  38. package/dist/gateway/session-manager.js +77 -0
  39. package/dist/gateway/types.js +2 -0
  40. package/dist/hardware.js +92 -0
  41. package/dist/hooks.js +187 -0
  42. package/dist/index.js +34 -0
  43. package/dist/input-engine.js +250 -0
  44. package/dist/llama-client.js +227 -0
  45. package/dist/llama-server.js +620 -0
  46. package/dist/llm/llama-client.js +227 -0
  47. package/dist/llm/llama-server.js +620 -0
  48. package/dist/llm/token-counter.js +47 -0
  49. package/dist/memory-store.js +159 -0
  50. package/dist/messages.js +59 -0
  51. package/dist/model-downloader.js +382 -0
  52. package/dist/model-manager-state.js +118 -0
  53. package/dist/model-manager-ui.js +194 -0
  54. package/dist/model-manager.js +190 -0
  55. package/dist/models/hardware.js +92 -0
  56. package/dist/models/model-downloader.js +382 -0
  57. package/dist/models/model-manager.js +190 -0
  58. package/dist/prompt-box.js +78 -0
  59. package/dist/prompt-composer.js +498 -0
  60. package/dist/repl.js +105 -0
  61. package/dist/session-store.js +211 -0
  62. package/dist/spinner.js +76 -0
  63. package/dist/title-box.js +388 -0
  64. package/dist/token-counter.js +47 -0
  65. package/dist/tool-executor.js +415 -0
  66. package/dist/tool-protocol.js +121 -0
  67. package/dist/tool-safety.js +52 -0
  68. package/dist/tui/app.js +2553 -0
  69. package/dist/tui/startup.js +56 -0
  70. package/dist/tui-input-routing.js +53 -0
  71. package/dist/tui.js +51 -0
  72. package/dist/types/app-types.js +2 -0
  73. package/dist/types.js +2 -0
  74. package/dist/ui/colors.js +204 -0
  75. package/dist/ui/downloader/downloader-state.js +302 -0
  76. package/dist/ui/downloader/downloader-ui.js +289 -0
  77. package/dist/ui/input/input-engine.js +250 -0
  78. package/dist/ui/input/tui-input-routing.js +53 -0
  79. package/dist/ui/input/vt-session.js +168 -0
  80. package/dist/ui/messages.js +59 -0
  81. package/dist/ui/model-manager/model-manager-state.js +118 -0
  82. package/dist/ui/model-manager/model-manager-ui.js +194 -0
  83. package/dist/ui/prompt/prompt-box.js +78 -0
  84. package/dist/ui/prompt/prompt-composer.js +498 -0
  85. package/dist/ui/spinner.js +76 -0
  86. package/dist/ui/title-box.js +388 -0
  87. package/dist/ui/tui/app.js +6 -0
  88. package/dist/ui/tui/autocomplete.js +85 -0
  89. package/dist/ui/tui/constants.js +18 -0
  90. package/dist/ui/tui/history.js +29 -0
  91. package/dist/ui/tui/layout.js +341 -0
  92. package/dist/ui/tui/runtime-core.js +2584 -0
  93. package/dist/ui/tui/runtime-utils.js +53 -0
  94. package/dist/ui/tui/start-tui.js +54 -0
  95. package/dist/ui/tui/startup.js +56 -0
  96. package/dist/version.js +51 -0
  97. package/dist/vt-session.js +168 -0
  98. package/install.sh +457 -0
  99. package/package.json +128 -0
@@ -0,0 +1,159 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.saveMemory = saveMemory;
4
+ exports.listMemories = listMemories;
5
+ exports.readMemory = readMemory;
6
+ const promises_1 = require("node:fs/promises");
7
+ const node_os_1 = require("node:os");
8
+ const node_path_1 = require("node:path");
9
+ const DEFAULT_MEMORY_DIR = (0, node_path_1.join)((0, node_os_1.homedir)(), ".yips", "memories");
10
+ const MEMORY_DIR_ENV = "YIPS_MEMORIES_DIR";
11
+ function resolveMemoryDir() {
12
+ const override = process.env[MEMORY_DIR_ENV]?.trim();
13
+ if (override && override.length > 0) {
14
+ return override;
15
+ }
16
+ return DEFAULT_MEMORY_DIR;
17
+ }
18
+ function pad2(value) {
19
+ return value.toString().padStart(2, "0");
20
+ }
21
+ function toMemoryTimestamp(now) {
22
+ return [
23
+ now.getFullYear(),
24
+ "-",
25
+ pad2(now.getMonth() + 1),
26
+ "-",
27
+ pad2(now.getDate()),
28
+ "_",
29
+ pad2(now.getHours()),
30
+ "-",
31
+ pad2(now.getMinutes()),
32
+ "-",
33
+ pad2(now.getSeconds())
34
+ ].join("");
35
+ }
36
+ function slugifyTitle(input) {
37
+ const base = input
38
+ .toLowerCase()
39
+ .replace(/[^a-z0-9\s]/gu, " ")
40
+ .trim()
41
+ .replace(/\s+/gu, "-")
42
+ .slice(0, 48)
43
+ .replace(/-+$/gu, "");
44
+ return base.length > 0 ? base : "memory";
45
+ }
46
+ function normalizeContent(content) {
47
+ return content.trim().replace(/\r\n/gu, "\n");
48
+ }
49
+ function parseFileStem(path) {
50
+ const stem = (0, node_path_1.basename)(path, ".md");
51
+ const splitIndex = stem.indexOf("_");
52
+ if (splitIndex <= 0) {
53
+ return { createdAt: new Date(0), slug: stem };
54
+ }
55
+ const datePart = stem.slice(0, splitIndex);
56
+ const remainder = stem.slice(splitIndex + 1);
57
+ const secondSplit = remainder.indexOf("_");
58
+ if (secondSplit <= 0) {
59
+ return { createdAt: new Date(0), slug: stem };
60
+ }
61
+ const timePart = remainder.slice(0, secondSplit);
62
+ const slug = remainder.slice(secondSplit + 1);
63
+ const timestamp = new Date(`${datePart}T${timePart.replace(/-/gu, ":")}`);
64
+ if (Number.isNaN(timestamp.valueOf())) {
65
+ return { createdAt: new Date(0), slug };
66
+ }
67
+ return { createdAt: timestamp, slug };
68
+ }
69
+ function extractBody(content) {
70
+ const marker = "## Memory";
71
+ const markerIndex = content.indexOf(marker);
72
+ if (markerIndex < 0) {
73
+ return content.trim();
74
+ }
75
+ return content.slice(markerIndex + marker.length).trim();
76
+ }
77
+ function previewOf(content) {
78
+ const firstLine = extractBody(content)
79
+ .split(/\n/gu)
80
+ .map((line) => line.trim())
81
+ .find((line) => line.length > 0);
82
+ if (!firstLine) {
83
+ return "(empty memory)";
84
+ }
85
+ return firstLine.length > 96 ? `${firstLine.slice(0, 93)}...` : firstLine;
86
+ }
87
+ async function saveMemory(content, now = new Date()) {
88
+ const normalized = normalizeContent(content);
89
+ if (normalized.length === 0) {
90
+ throw new Error("Cannot save empty memory.");
91
+ }
92
+ const memoryDir = resolveMemoryDir();
93
+ await (0, promises_1.mkdir)(memoryDir, { recursive: true });
94
+ const titleSource = normalized.split(/\n/gu)[0] ?? "memory";
95
+ const slug = slugifyTitle(titleSource);
96
+ const id = `${toMemoryTimestamp(now)}_${slug}`;
97
+ const path = (0, node_path_1.join)(memoryDir, `${id}.md`);
98
+ const body = [
99
+ "# Memory",
100
+ "",
101
+ `**Created**: ${now.toISOString()}`,
102
+ "",
103
+ "## Memory",
104
+ "",
105
+ normalized,
106
+ ""
107
+ ].join("\n");
108
+ await (0, promises_1.writeFile)(path, body, "utf8");
109
+ return {
110
+ id,
111
+ path,
112
+ title: titleSource.trim().slice(0, 96),
113
+ createdAt: now,
114
+ preview: previewOf(body)
115
+ };
116
+ }
117
+ async function listMemories(limit) {
118
+ const memoryDir = resolveMemoryDir();
119
+ try {
120
+ const entries = await (0, promises_1.readdir)(memoryDir, { withFileTypes: true });
121
+ const files = entries
122
+ .filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".md"))
123
+ .map((entry) => (0, node_path_1.join)(memoryDir, entry.name));
124
+ const loaded = await Promise.all(files.map(async (path) => {
125
+ const text = await (0, promises_1.readFile)(path, "utf8");
126
+ const parsed = parseFileStem(path);
127
+ return {
128
+ id: (0, node_path_1.basename)(path, ".md"),
129
+ path,
130
+ title: parsed.slug.replace(/-/gu, " ").trim() || "memory",
131
+ createdAt: parsed.createdAt,
132
+ preview: previewOf(text)
133
+ };
134
+ }));
135
+ const sorted = loaded.sort((left, right) => right.createdAt.valueOf() - left.createdAt.valueOf());
136
+ return typeof limit === "number" && limit > 0 ? sorted.slice(0, limit) : sorted;
137
+ }
138
+ catch {
139
+ return [];
140
+ }
141
+ }
142
+ async function readMemory(id) {
143
+ const normalizedId = id.trim().replace(/\.md$/iu, "");
144
+ if (normalizedId.length === 0) {
145
+ throw new Error("Memory id is required.");
146
+ }
147
+ const path = (0, node_path_1.join)(resolveMemoryDir(), `${normalizedId}.md`);
148
+ const text = await (0, promises_1.readFile)(path, "utf8");
149
+ const parsed = parseFileStem(path);
150
+ const content = extractBody(text);
151
+ return {
152
+ id: normalizedId,
153
+ path,
154
+ title: parsed.slug.replace(/-/gu, " ").trim() || "memory",
155
+ createdAt: parsed.createdAt,
156
+ preview: previewOf(text),
157
+ content
158
+ };
159
+ }
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ /** Conversation message formatting with ANSI truecolor styling. */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.formatUserMessage = formatUserMessage;
5
+ exports.formatAssistantMessage = formatAssistantMessage;
6
+ exports.formatErrorMessage = formatErrorMessage;
7
+ exports.formatWarningMessage = formatWarningMessage;
8
+ exports.formatSuccessMessage = formatSuccessMessage;
9
+ exports.formatDimMessage = formatDimMessage;
10
+ const colors_1 = require("./colors");
11
+ function formatTimestamp(date) {
12
+ const hours = date.getHours();
13
+ const minutes = date.getMinutes().toString().padStart(2, "0");
14
+ const period = hours >= 12 ? "PM" : "AM";
15
+ const displayHours = hours % 12 || 12;
16
+ return `${displayHours}:${minutes} ${period}`;
17
+ }
18
+ function formatUserMessage(text) {
19
+ const lines = text.split("\n");
20
+ const first = (0, colors_1.colorText)(`>>> ${lines[0] ?? ""}`, colors_1.INPUT_PINK);
21
+ if (lines.length <= 1) {
22
+ return first;
23
+ }
24
+ const rest = lines.slice(1).map((line) => (0, colors_1.colorText)(line, colors_1.INPUT_PINK));
25
+ return [first, ...rest].join("\n");
26
+ }
27
+ function formatAssistantMessage(text, timestamp) {
28
+ const time = timestamp ?? new Date();
29
+ const timestampPlain = `[${formatTimestamp(time)}]`;
30
+ const namePlain = "Yips";
31
+ const prefixPlain = `${timestampPlain} ${namePlain}: `;
32
+ const timeStr = (0, colors_1.colorText)(timestampPlain, colors_1.GRADIENT_BLUE);
33
+ const name = (0, colors_1.horizontalGradient)(namePlain, colors_1.GRADIENT_PINK, colors_1.GRADIENT_YELLOW);
34
+ const colon = (0, colors_1.colorText)(":", colors_1.GRADIENT_BLUE);
35
+ const lines = text.split("\n");
36
+ const firstLine = lines[0] ?? "";
37
+ const firstBody = (0, colors_1.horizontalGradient)(firstLine, colors_1.GRADIENT_PINK, colors_1.GRADIENT_YELLOW);
38
+ const first = `${timeStr} ${name}${colon} ${firstBody}`;
39
+ if (lines.length <= 1) {
40
+ return first;
41
+ }
42
+ const indent = " ".repeat(prefixPlain.length);
43
+ const rest = lines
44
+ .slice(1)
45
+ .map((line) => `${indent}${(0, colors_1.horizontalGradient)(line, colors_1.GRADIENT_PINK, colors_1.GRADIENT_YELLOW)}`);
46
+ return [first, ...rest].join("\n");
47
+ }
48
+ function formatErrorMessage(text) {
49
+ return (0, colors_1.colorText)(text, colors_1.ERROR_RED);
50
+ }
51
+ function formatWarningMessage(text) {
52
+ return (0, colors_1.colorText)(text, colors_1.WARNING_YELLOW);
53
+ }
54
+ function formatSuccessMessage(text) {
55
+ return (0, colors_1.colorText)(text, colors_1.SUCCESS_GREEN);
56
+ }
57
+ function formatDimMessage(text) {
58
+ return (0, colors_1.colorText)(text, colors_1.DIM_GRAY);
59
+ }
@@ -0,0 +1,382 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveDefaultModelsDir = resolveDefaultModelsDir;
4
+ exports.isHfDownloadUrl = isHfDownloadUrl;
5
+ exports.parseHfDownloadUrl = parseHfDownloadUrl;
6
+ exports.listGgufModels = listGgufModels;
7
+ exports.listModelFiles = listModelFiles;
8
+ exports.downloadModelFile = downloadModelFile;
9
+ exports.renderModelList = renderModelList;
10
+ exports.renderFileList = renderFileList;
11
+ const node_fs_1 = require("node:fs");
12
+ const promises_1 = require("node:fs/promises");
13
+ const node_os_1 = require("node:os");
14
+ const node_path_1 = require("node:path");
15
+ function encodePath(path) {
16
+ return path
17
+ .split("/")
18
+ .map((segment) => encodeURIComponent(segment))
19
+ .join("/");
20
+ }
21
+ function toPositiveInt(value, fallback = 0) {
22
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
23
+ return fallback;
24
+ }
25
+ return Math.floor(value);
26
+ }
27
+ function toDateText(value) {
28
+ if (typeof value !== "string") {
29
+ return "unknown";
30
+ }
31
+ const trimmed = value.trim();
32
+ return trimmed.length > 0 ? trimmed : "unknown";
33
+ }
34
+ function safeRelativePath(path) {
35
+ const normalized = (0, node_path_1.normalize)(path).replace(/^[\\/]+/u, "");
36
+ const segments = normalized.split(/[\\/]/u).filter((segment) => segment.length > 0);
37
+ if (segments.some((segment) => segment === "..")) {
38
+ throw new Error(`Unsafe filename: ${path}`);
39
+ }
40
+ if (segments.length === 0) {
41
+ throw new Error("Filename cannot be empty.");
42
+ }
43
+ return segments.join("/");
44
+ }
45
+ function extractSizeBytes(value) {
46
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) {
47
+ return value;
48
+ }
49
+ if (typeof value !== "object" || value === null) {
50
+ return null;
51
+ }
52
+ const source = value;
53
+ const direct = source.total;
54
+ if (typeof direct === "number" && Number.isFinite(direct) && direct > 0) {
55
+ return direct;
56
+ }
57
+ const lfs = source.lfs;
58
+ if (typeof lfs === "object" && lfs !== null) {
59
+ const lfsSize = lfs.size;
60
+ if (typeof lfsSize === "number" && Number.isFinite(lfsSize) && lfsSize > 0) {
61
+ return lfsSize;
62
+ }
63
+ }
64
+ return null;
65
+ }
66
+ function evaluateSuitability(sizeBytes, totalMemoryGb) {
67
+ if (sizeBytes === null) {
68
+ return { canRun: true, reason: "Unknown size" };
69
+ }
70
+ if (typeof totalMemoryGb !== "number" || !Number.isFinite(totalMemoryGb) || totalMemoryGb <= 0) {
71
+ return { canRun: true, reason: "No memory limit" };
72
+ }
73
+ const memoryLimitMultiplier = 1.2;
74
+ const sizeGb = sizeBytes / (1024 * 1024 * 1024);
75
+ const effectiveLimit = totalMemoryGb * memoryLimitMultiplier;
76
+ if (sizeGb <= effectiveLimit) {
77
+ return { canRun: true, reason: "Fits RAM+VRAM" };
78
+ }
79
+ return {
80
+ canRun: false,
81
+ reason: `Model too large (${sizeGb.toFixed(1)}GB > ${effectiveLimit.toFixed(1)}GB)`
82
+ };
83
+ }
84
+ function extractQuant(path) {
85
+ const upper = path.toUpperCase();
86
+ if (upper.includes("Q4_K_M"))
87
+ return "Q4_K_M (Balanced)";
88
+ if (upper.includes("Q5_K_M"))
89
+ return "Q5_K_M (High Quality)";
90
+ if (upper.includes("Q8_0"))
91
+ return "Q8_0 (Max Quality)";
92
+ if (upper.includes("Q2_K"))
93
+ return "Q2_K (Max Speed)";
94
+ const match = upper.match(/(Q\d+_[A-Z0-9_]+)/u);
95
+ return match?.[1] ?? "Unknown";
96
+ }
97
+ async function readJson(response) {
98
+ if (!response.ok) {
99
+ throw new Error(`HTTP ${response.status} ${response.statusText}`);
100
+ }
101
+ return (await response.json());
102
+ }
103
+ function getFetch(fetchImpl) {
104
+ return fetchImpl ?? fetch;
105
+ }
106
+ function resolveDefaultModelsDir() {
107
+ const env = process.env["YIPS_MODELS_DIR"]?.trim();
108
+ if (env && env.length > 0) {
109
+ return env;
110
+ }
111
+ return (0, node_path_1.join)((0, node_os_1.homedir)(), ".yips", "models");
112
+ }
113
+ function isHfDownloadUrl(input) {
114
+ try {
115
+ parseHfDownloadUrl(input);
116
+ return true;
117
+ }
118
+ catch {
119
+ return false;
120
+ }
121
+ }
122
+ function parseHfDownloadUrl(input) {
123
+ const raw = input.trim();
124
+ if (raw.length === 0) {
125
+ throw new Error("URL is required.");
126
+ }
127
+ let url;
128
+ try {
129
+ url = new URL(raw);
130
+ }
131
+ catch {
132
+ throw new Error("Invalid URL.");
133
+ }
134
+ const host = url.hostname.toLowerCase();
135
+ if (host !== "hf.co" && host !== "huggingface.co" && host !== "www.huggingface.co") {
136
+ throw new Error("URL must be from hf.co or huggingface.co.");
137
+ }
138
+ const segments = url.pathname.split("/").filter((segment) => segment.length > 0);
139
+ if (segments.length < 5) {
140
+ throw new Error("URL must include /<owner>/<repo>/resolve/<revision>/<file>.gguf");
141
+ }
142
+ const resolveIndex = segments.findIndex((segment) => segment === "resolve");
143
+ if (resolveIndex !== 2 || segments.length < 5) {
144
+ throw new Error("URL must include /<owner>/<repo>/resolve/<revision>/<file>.gguf");
145
+ }
146
+ const owner = segments[0] ?? "";
147
+ const repo = segments[1] ?? "";
148
+ const revision = segments[3] ?? "main";
149
+ const filenameSegments = segments.slice(4);
150
+ const filename = filenameSegments.join("/");
151
+ if (!filename.toLowerCase().endsWith(".gguf")) {
152
+ throw new Error("URL must point to a .gguf file.");
153
+ }
154
+ return {
155
+ repoId: `${owner}/${repo}`,
156
+ revision,
157
+ filename
158
+ };
159
+ }
160
+ async function listGgufModels(options) {
161
+ const query = options?.query?.trim() ?? "";
162
+ const sort = options?.sort ?? "downloads";
163
+ const limit = Math.max(1, Math.min(100, options?.limit ?? 10));
164
+ const requestModels = async (expanded) => {
165
+ const url = new URL("https://huggingface.co/api/models");
166
+ url.searchParams.set("filter", "gguf");
167
+ url.searchParams.set("sort", sort);
168
+ url.searchParams.set("limit", String(limit));
169
+ if (expanded) {
170
+ url.searchParams.append("expand", "downloads");
171
+ url.searchParams.append("expand", "likes");
172
+ url.searchParams.append("expand", "lastModified");
173
+ url.searchParams.append("expand", "gguf");
174
+ }
175
+ if (query.length > 0) {
176
+ url.searchParams.set("search", query);
177
+ }
178
+ const response = await getFetch(options?.fetchImpl)(url);
179
+ return readJson(response);
180
+ };
181
+ let data;
182
+ try {
183
+ data = await requestModels(true);
184
+ }
185
+ catch (error) {
186
+ const message = error instanceof Error ? error.message : String(error);
187
+ if (!message.includes("HTTP 400") && !message.includes("HTTP 422")) {
188
+ throw error;
189
+ }
190
+ data = await requestModels(false);
191
+ }
192
+ return data
193
+ .map((item) => {
194
+ if (typeof item !== "object" || item === null) {
195
+ return null;
196
+ }
197
+ const model = item;
198
+ const id = typeof model.id === "string" ? model.id : "";
199
+ if (id.trim().length === 0) {
200
+ return null;
201
+ }
202
+ const sizeBytes = extractSizeBytes(model.gguf);
203
+ const suitability = evaluateSuitability(sizeBytes, options?.totalMemoryGb);
204
+ return {
205
+ id,
206
+ downloads: toPositiveInt(model.downloads),
207
+ likes: toPositiveInt(model.likes),
208
+ lastModified: toDateText(model.lastModified),
209
+ sizeBytes,
210
+ canRun: suitability.canRun,
211
+ reason: suitability.reason
212
+ };
213
+ })
214
+ .filter((entry) => entry !== null)
215
+ .filter((entry) => entry.canRun || entry.sizeBytes === null);
216
+ }
217
+ async function listModelFiles(repoId, options) {
218
+ const trimmedRepo = repoId.trim();
219
+ if (trimmedRepo.length === 0) {
220
+ throw new Error("Repository id is required.");
221
+ }
222
+ const encodedRepo = encodePath(trimmedRepo);
223
+ const url = new URL(`https://huggingface.co/api/models/${encodedRepo}`);
224
+ url.searchParams.set("blobs", "true");
225
+ const response = await getFetch(options?.fetchImpl)(url);
226
+ const data = await readJson(response);
227
+ const siblingsValue = data.siblings;
228
+ const siblings = Array.isArray(siblingsValue) ? siblingsValue : [];
229
+ const files = siblings
230
+ .map((item) => {
231
+ if (typeof item !== "object" || item === null) {
232
+ return null;
233
+ }
234
+ const file = item;
235
+ const path = typeof file.rfilename === "string" ? file.rfilename : "";
236
+ if (!path.toLowerCase().endsWith(".gguf")) {
237
+ return null;
238
+ }
239
+ const sizeBytes = extractSizeBytes(file.size ?? file.lfs ?? null);
240
+ const suitability = evaluateSuitability(sizeBytes, options?.totalMemoryGb);
241
+ return {
242
+ path,
243
+ sizeBytes,
244
+ quant: extractQuant(path),
245
+ canRun: suitability.canRun,
246
+ reason: suitability.reason
247
+ };
248
+ })
249
+ .filter((entry) => entry !== null);
250
+ files.sort((left, right) => {
251
+ const leftSize = left.sizeBytes ?? Number.MAX_SAFE_INTEGER;
252
+ const rightSize = right.sizeBytes ?? Number.MAX_SAFE_INTEGER;
253
+ if (leftSize !== rightSize) {
254
+ return leftSize - rightSize;
255
+ }
256
+ return left.path.localeCompare(right.path);
257
+ });
258
+ return files;
259
+ }
260
+ async function downloadModelFile(options) {
261
+ const repoId = options.repoId.trim();
262
+ if (repoId.length === 0) {
263
+ throw new Error("Repository id is required.");
264
+ }
265
+ const safeFilename = safeRelativePath(options.filename.trim());
266
+ const modelsDir = options.modelsDir?.trim() || resolveDefaultModelsDir();
267
+ const repoPath = safeRelativePath(repoId);
268
+ const outputPath = (0, node_path_1.resolve)(modelsDir, repoPath, safeFilename);
269
+ const revision = options.revision?.trim() || "main";
270
+ await (0, promises_1.mkdir)((0, node_path_1.dirname)(outputPath), { recursive: true });
271
+ let byteCount = 0;
272
+ let completed = false;
273
+ try {
274
+ const fileUrl = `https://huggingface.co/${encodePath(repoId)}/resolve/${encodeURIComponent(revision)}/` +
275
+ `${encodePath(safeFilename)}?download=true`;
276
+ const response = await getFetch(options.fetchImpl)(fileUrl, { signal: options.signal });
277
+ if (!response.ok) {
278
+ throw new Error(`Download failed with HTTP ${response.status} ${response.statusText}`);
279
+ }
280
+ if (!response.body) {
281
+ throw new Error("Download failed: response body is empty.");
282
+ }
283
+ const contentLengthHeader = response.headers.get("content-length");
284
+ const parsedLength = contentLengthHeader
285
+ ? Number.parseInt(contentLengthHeader, 10)
286
+ : Number.NaN;
287
+ const totalBytes = Number.isFinite(parsedLength) && parsedLength > 0 ? parsedLength : null;
288
+ const emitProgress = (bytesDownloaded) => {
289
+ if (!options.onProgress) {
290
+ return;
291
+ }
292
+ options.onProgress({
293
+ bytesDownloaded,
294
+ totalBytes
295
+ });
296
+ };
297
+ emitProgress(byteCount);
298
+ await new Promise((resolvePromise, rejectPromise) => {
299
+ const writer = (0, node_fs_1.createWriteStream)(outputPath);
300
+ writer.on("error", rejectPromise);
301
+ writer.on("finish", resolvePromise);
302
+ const reader = response.body?.getReader();
303
+ if (!reader) {
304
+ writer.destroy(new Error("Download failed: could not create stream reader."));
305
+ return;
306
+ }
307
+ const pump = async () => {
308
+ try {
309
+ let reading = true;
310
+ while (reading) {
311
+ const chunk = await reader.read();
312
+ if (chunk.done) {
313
+ writer.end();
314
+ reading = false;
315
+ continue;
316
+ }
317
+ const value = chunk.value;
318
+ byteCount += value.byteLength;
319
+ emitProgress(byteCount);
320
+ if (!writer.write(Buffer.from(value))) {
321
+ await new Promise((drainResolve) => {
322
+ writer.once("drain", drainResolve);
323
+ });
324
+ }
325
+ }
326
+ }
327
+ catch (error) {
328
+ writer.destroy(error instanceof Error ? error : new Error(String(error)));
329
+ }
330
+ };
331
+ void pump();
332
+ });
333
+ emitProgress(byteCount);
334
+ completed = true;
335
+ return { localPath: outputPath, byteCount };
336
+ }
337
+ catch (error) {
338
+ if (!completed) {
339
+ try {
340
+ await (0, promises_1.rm)(outputPath, { force: true });
341
+ }
342
+ catch {
343
+ // Preserve the original download error when cleanup fails.
344
+ }
345
+ }
346
+ throw error;
347
+ }
348
+ }
349
+ function formatCount(value) {
350
+ if (value >= 1_000_000) {
351
+ return `${(value / 1_000_000).toFixed(1)}M`;
352
+ }
353
+ if (value >= 1_000) {
354
+ return `${(value / 1_000).toFixed(1)}k`;
355
+ }
356
+ return String(value);
357
+ }
358
+ function formatSize(bytes) {
359
+ if (bytes === null || bytes <= 0) {
360
+ return "unknown";
361
+ }
362
+ const gib = bytes / (1024 * 1024 * 1024);
363
+ return `${gib.toFixed(2)} GB`;
364
+ }
365
+ function renderModelList(models) {
366
+ if (models.length === 0) {
367
+ return "No GGUF models found.";
368
+ }
369
+ const rows = models.map((model, index) => {
370
+ const date = model.lastModified === "unknown" ? "unknown" : model.lastModified.slice(0, 10);
371
+ return `${index + 1}. ${model.id} (downloads ${formatCount(model.downloads)}, likes ${formatCount(model.likes)}, size ${formatSize(model.sizeBytes)}, updated ${date})`;
372
+ });
373
+ return rows.join("\n");
374
+ }
375
+ function renderFileList(files) {
376
+ if (files.length === 0) {
377
+ return "No GGUF files found for this repository.";
378
+ }
379
+ return files
380
+ .map((file, index) => `${index + 1}. ${file.path} (${formatSize(file.sizeBytes)}, ${file.quant}, ${file.reason})`)
381
+ .join("\n");
382
+ }