threadshelf 1.2.0

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 (67) hide show
  1. package/CHANGELOG.md +185 -0
  2. package/LICENSE +21 -0
  3. package/README.md +763 -0
  4. package/SECURITY.md +75 -0
  5. package/bin/threadshelf-mcp.js +12 -0
  6. package/bin/threadshelf.js +87 -0
  7. package/dist/mcp/server.js +388 -0
  8. package/dist/src/chunking.js +72 -0
  9. package/dist/src/cli.js +24 -0
  10. package/dist/src/embedding.js +59 -0
  11. package/dist/src/env.js +2 -0
  12. package/dist/src/generation/config.js +344 -0
  13. package/dist/src/generation/downloader.js +172 -0
  14. package/dist/src/generation/error-log.js +34 -0
  15. package/dist/src/generation/filesystem-browser.js +83 -0
  16. package/dist/src/generation/gguf-metadata.js +179 -0
  17. package/dist/src/generation/hardware.js +87 -0
  18. package/dist/src/generation/llama-install.js +563 -0
  19. package/dist/src/generation/llama-process.js +576 -0
  20. package/dist/src/generation/llama-profile.js +136 -0
  21. package/dist/src/generation/master-prompts.js +155 -0
  22. package/dist/src/generation/model-catalog.js +276 -0
  23. package/dist/src/generation/model-discovery.js +60 -0
  24. package/dist/src/generation/model-download.js +151 -0
  25. package/dist/src/generation/openai-compatible.js +231 -0
  26. package/dist/src/generation/providers/llama-cpp.js +97 -0
  27. package/dist/src/generation/providers/openrouter.js +106 -0
  28. package/dist/src/generation/quick-setup.js +215 -0
  29. package/dist/src/generation/registry.js +23 -0
  30. package/dist/src/generation/service.js +100 -0
  31. package/dist/src/generation/threads.js +311 -0
  32. package/dist/src/generation/types.js +1 -0
  33. package/dist/src/ingest-cli.js +95 -0
  34. package/dist/src/ingest.js +257 -0
  35. package/dist/src/load-env.js +17 -0
  36. package/dist/src/model-label.js +15 -0
  37. package/dist/src/parser.js +811 -0
  38. package/dist/src/paths.js +79 -0
  39. package/dist/src/routes/collections.js +97 -0
  40. package/dist/src/routes/files.js +136 -0
  41. package/dist/src/routes/generation.js +536 -0
  42. package/dist/src/routes/health.js +6 -0
  43. package/dist/src/routes/index.js +21 -0
  44. package/dist/src/routes/ingest.js +300 -0
  45. package/dist/src/routes/insights.js +24 -0
  46. package/dist/src/routes/loopback.js +15 -0
  47. package/dist/src/routes/model-catalog.js +178 -0
  48. package/dist/src/routes/search.js +57 -0
  49. package/dist/src/routes/stream-abort.js +23 -0
  50. package/dist/src/routes/thread.js +43 -0
  51. package/dist/src/search-cli.js +93 -0
  52. package/dist/src/server.js +78 -0
  53. package/dist/src/services/collections.js +58 -0
  54. package/dist/src/services/insights.js +111 -0
  55. package/dist/src/services/search.js +68 -0
  56. package/dist/src/services/stats.js +35 -0
  57. package/dist/src/services/thread.js +140 -0
  58. package/dist/src/store.js +1138 -0
  59. package/dist/src/validation.js +250 -0
  60. package/dist/src/watch.js +83 -0
  61. package/package.json +103 -0
  62. package/public/assets/index-CIm_Idqi.js +38 -0
  63. package/public/assets/index-Dv09K2vS.css +1 -0
  64. package/public/favicon.svg +6 -0
  65. package/public/index.html +28 -0
  66. package/scripts/openrouter-export-all.js +228 -0
  67. package/scripts/openrouter-export-browser.js +153 -0
@@ -0,0 +1,83 @@
1
+ import { existsSync } from 'fs';
2
+ import { readdir, stat } from 'fs/promises';
3
+ import { homedir, platform } from 'os';
4
+ import { dirname, parse, resolve } from 'path';
5
+ import { ValidationError } from '../validation.js';
6
+ const MAX_DIRECTORIES = 500;
7
+ export const isLoopbackAddress = (address) => {
8
+ let normalized = String(address || '')
9
+ .trim()
10
+ .replace(/^"|"$/g, '')
11
+ .toLowerCase();
12
+ if (normalized.startsWith('['))
13
+ normalized = normalized.slice(1, normalized.indexOf(']'));
14
+ else
15
+ normalized = normalized.replace(/^(\d{1,3}(?:\.\d{1,3}){3}):\d+$/, '$1');
16
+ return normalized === '127.0.0.1' || normalized === '::1' || normalized === '::ffff:127.0.0.1';
17
+ };
18
+ const forwardedAddresses = (value) => (Array.isArray(value) ? value : [value ?? ''])
19
+ .flatMap((header) => header.split(','))
20
+ .map((entry) => entry.trim())
21
+ .filter(Boolean);
22
+ export const isLoopbackRequest = (remoteAddress, forwardedFor, forwarded, requestHostname) => {
23
+ if (!isLoopbackAddress(remoteAddress))
24
+ return false;
25
+ if (requestHostname !== undefined &&
26
+ requestHostname.toLowerCase() !== 'localhost' &&
27
+ !isLoopbackAddress(requestHostname)) {
28
+ return false;
29
+ }
30
+ const proxyAddresses = forwardedAddresses(forwardedFor);
31
+ const forwardedEntries = forwardedAddresses(forwarded);
32
+ for (const entry of forwardedEntries) {
33
+ const matches = [...entry.matchAll(/(?:^|;)\s*for=("?)(\[[^\]]+\]|[^;\s"]+)\1/gi)];
34
+ if (matches.length === 0)
35
+ return false;
36
+ proxyAddresses.push(...matches.map((match) => match[2]).filter((address) => Boolean(address)));
37
+ }
38
+ return proxyAddresses.every(isLoopbackAddress);
39
+ };
40
+ const filesystemRoots = () => {
41
+ const home = resolve(homedir());
42
+ if (platform() !== 'win32') {
43
+ return [
44
+ { name: 'Home', path: home },
45
+ { name: 'Filesystem', path: '/' },
46
+ ];
47
+ }
48
+ const drives = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
49
+ .split('')
50
+ .map((letter) => `${letter}:\\`)
51
+ .filter((path) => existsSync(path))
52
+ .map((path) => ({ name: path.slice(0, 2), path }));
53
+ return [
54
+ { name: 'Home', path: home },
55
+ ...drives.filter((drive) => drive.path !== parse(home).root),
56
+ ];
57
+ };
58
+ export const browseDirectories = async (requestedPath) => {
59
+ if (requestedPath !== undefined &&
60
+ (typeof requestedPath !== 'string' ||
61
+ requestedPath.length > 4096 ||
62
+ requestedPath.includes('\0'))) {
63
+ throw new ValidationError('Invalid directory path', { field: 'path' });
64
+ }
65
+ const path = resolve(requestedPath?.trim() || homedir());
66
+ const info = await stat(path).catch(() => null);
67
+ if (!info?.isDirectory()) {
68
+ throw new ValidationError('Directory does not exist or is not accessible', { field: 'path' });
69
+ }
70
+ const entries = await readdir(path, { withFileTypes: true });
71
+ const directories = entries
72
+ .filter((entry) => entry.isDirectory() && !entry.isSymbolicLink())
73
+ .map((entry) => ({ name: entry.name, path: resolve(path, entry.name) }))
74
+ .sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }));
75
+ const parent = dirname(path);
76
+ return {
77
+ path,
78
+ parent: parent === path ? undefined : parent,
79
+ roots: filesystemRoots(),
80
+ directories: directories.slice(0, MAX_DIRECTORIES),
81
+ truncated: directories.length > MAX_DIRECTORIES,
82
+ };
83
+ };
@@ -0,0 +1,179 @@
1
+ import { open, stat } from 'fs/promises';
2
+ const GGUF_MAGIC = 0x46554747; // "GGUF" read as little-endian uint32
3
+ const CHUNK_BYTES = 1024 * 1024;
4
+ // Tokenizer vocabularies dominate the header. Anything past this is not a sane model.
5
+ const MAX_HEADER_BYTES = 512 * 1024 * 1024;
6
+ const MAX_STRING_BYTES = 64 * 1024 * 1024;
7
+ const MAX_KV_COUNT = 100_000;
8
+ const TYPE_STRING = 8;
9
+ const TYPE_ARRAY = 9;
10
+ const SCALAR_BYTES = {
11
+ 0: 1,
12
+ 1: 1,
13
+ 2: 2,
14
+ 3: 2,
15
+ 4: 4,
16
+ 5: 4,
17
+ 6: 4,
18
+ 7: 1,
19
+ 10: 8,
20
+ 11: 8,
21
+ 12: 8,
22
+ };
23
+ const NUMERIC_SUFFIXES = ['.context_length', '.block_count', '.nextn_predict_layers'];
24
+ class HeaderReader {
25
+ handle;
26
+ buffer = Buffer.alloc(0);
27
+ bufferStart = 0;
28
+ position = 0;
29
+ limit;
30
+ constructor(handle, size) {
31
+ this.handle = handle;
32
+ this.limit = Math.min(size, MAX_HEADER_BYTES);
33
+ }
34
+ ensureWithinLimit(length) {
35
+ if (!Number.isSafeInteger(length) || length < 0 || this.position + length > this.limit) {
36
+ throw new Error('GGUF header is truncated or exceeds the metadata limit');
37
+ }
38
+ }
39
+ async bytes(length) {
40
+ this.ensureWithinLimit(length);
41
+ const offset = this.position - this.bufferStart;
42
+ if (offset < 0 || offset + length > this.buffer.length) {
43
+ const chunk = Buffer.alloc(Math.min(Math.max(length, CHUNK_BYTES), this.limit - this.position));
44
+ const { bytesRead } = await this.handle.read(chunk, 0, chunk.length, this.position);
45
+ if (bytesRead < length)
46
+ throw new Error('GGUF header is truncated');
47
+ this.buffer = chunk.subarray(0, bytesRead);
48
+ this.bufferStart = this.position;
49
+ }
50
+ const start = this.position - this.bufferStart;
51
+ this.position += length;
52
+ return this.buffer.subarray(start, start + length);
53
+ }
54
+ skip(length) {
55
+ this.ensureWithinLimit(length);
56
+ this.position += length;
57
+ }
58
+ async u32() {
59
+ return (await this.bytes(4)).readUInt32LE(0);
60
+ }
61
+ async u64() {
62
+ const value = (await this.bytes(8)).readBigUInt64LE(0);
63
+ if (value > BigInt(Number.MAX_SAFE_INTEGER))
64
+ throw new Error('GGUF length is out of range');
65
+ return Number(value);
66
+ }
67
+ async string() {
68
+ const length = await this.u64();
69
+ if (length > MAX_STRING_BYTES)
70
+ throw new Error('GGUF string is too long');
71
+ return (await this.bytes(length)).toString('utf8');
72
+ }
73
+ }
74
+ const readScalar = async (reader, type) => {
75
+ const value = await reader.bytes(SCALAR_BYTES[type]);
76
+ switch (type) {
77
+ case 0:
78
+ return value.readUInt8(0);
79
+ case 1:
80
+ return value.readInt8(0);
81
+ case 2:
82
+ return value.readUInt16LE(0);
83
+ case 3:
84
+ return value.readInt16LE(0);
85
+ case 4:
86
+ return value.readUInt32LE(0);
87
+ case 5:
88
+ return value.readInt32LE(0);
89
+ case 6:
90
+ return value.readFloatLE(0);
91
+ case 7:
92
+ return value.readUInt8(0) !== 0;
93
+ case 10:
94
+ return Number(value.readBigUInt64LE(0));
95
+ case 11:
96
+ return Number(value.readBigInt64LE(0));
97
+ default:
98
+ return value.readDoubleLE(0);
99
+ }
100
+ };
101
+ const parseGgufHeader = async (path) => {
102
+ const handle = await open(path, 'r');
103
+ try {
104
+ const reader = new HeaderReader(handle, (await handle.stat()).size);
105
+ if ((await reader.u32()) !== GGUF_MAGIC)
106
+ return null;
107
+ // Version 1 used 32-bit lengths; every current llama.cpp GGUF is v2 or v3.
108
+ if ((await reader.u32()) < 2)
109
+ return null;
110
+ await reader.u64(); // tensor count
111
+ const kvCount = await reader.u64();
112
+ if (kvCount > MAX_KV_COUNT)
113
+ return null;
114
+ const strings = new Map();
115
+ const numbers = new Map();
116
+ for (let index = 0; index < kvCount; index += 1) {
117
+ const key = await reader.string();
118
+ const type = await reader.u32();
119
+ if (type === TYPE_STRING) {
120
+ if (key === 'general.architecture' || key === 'general.name') {
121
+ strings.set(key, await reader.string());
122
+ }
123
+ else {
124
+ reader.skip(await reader.u64());
125
+ }
126
+ }
127
+ else if (type === TYPE_ARRAY) {
128
+ const elementType = await reader.u32();
129
+ const count = await reader.u64();
130
+ if (elementType === TYPE_STRING) {
131
+ for (let element = 0; element < count; element += 1)
132
+ reader.skip(await reader.u64());
133
+ }
134
+ else {
135
+ const width = SCALAR_BYTES[elementType];
136
+ if (!width)
137
+ return null;
138
+ reader.skip(width * count);
139
+ }
140
+ }
141
+ else if (SCALAR_BYTES[type]) {
142
+ const value = await readScalar(reader, type);
143
+ if (typeof value === 'number' && NUMERIC_SUFFIXES.some((suffix) => key.endsWith(suffix))) {
144
+ numbers.set(key, value);
145
+ }
146
+ }
147
+ else {
148
+ return null;
149
+ }
150
+ }
151
+ // Keys are namespaced by architecture, which may appear anywhere in the header.
152
+ const architecture = strings.get('general.architecture');
153
+ const numeric = (suffix) => architecture ? numbers.get(`${architecture}${suffix}`) : undefined;
154
+ return {
155
+ architecture,
156
+ name: strings.get('general.name'),
157
+ contextLength: numeric('.context_length'),
158
+ blockCount: numeric('.block_count'),
159
+ nextnPredictLayers: Math.max(0, numeric('.nextn_predict_layers') ?? 0),
160
+ };
161
+ }
162
+ finally {
163
+ await handle.close();
164
+ }
165
+ };
166
+ const metadataCache = new Map();
167
+ /** Reads GGUF metadata once per file version. Unreadable or non-GGUF files yield null. */
168
+ export const readGgufMetadata = async (path) => {
169
+ const info = await stat(path).catch(() => null);
170
+ if (!info?.isFile())
171
+ return null;
172
+ const key = `${path}\0${info.size}\0${info.mtimeMs}`;
173
+ let pending = metadataCache.get(key);
174
+ if (!pending) {
175
+ pending = parseGgufHeader(path).catch(() => null);
176
+ metadataCache.set(key, pending);
177
+ }
178
+ return pending;
179
+ };
@@ -0,0 +1,87 @@
1
+ import { freemem, totalmem } from 'os';
2
+ import { findLlamaExecutables } from './llama-install.js';
3
+ import { inspectLlamaDevices } from './llama-process.js';
4
+ import { runCommandCapture } from './llama-install.js';
5
+ /**
6
+ * Weights are not the whole story: the KV cache, compute buffers and the OS all
7
+ * want memory too. Judging fit against the raw device size would recommend
8
+ * models that load and then immediately thrash.
9
+ */
10
+ const VRAM_HEADROOM = 0.85;
11
+ const RAM_HEADROOM = 0.6;
12
+ export const judgeFit = (sizeBytes, profile) => {
13
+ const budget = profile.modelBudgetBytes;
14
+ if (budget <= 0)
15
+ return 'too-large';
16
+ if (sizeBytes <= budget)
17
+ return 'fits';
18
+ // Above the accelerator budget a model still runs, just partly on CPU.
19
+ if (sizeBytes <= profile.totalRamBytes * RAM_HEADROOM)
20
+ return 'tight';
21
+ return 'too-large';
22
+ };
23
+ const parseNvidiaSmi = (output) => output
24
+ .split(/\r?\n/)
25
+ .map((line) => line.trim())
26
+ .filter(Boolean)
27
+ .map((line, index) => {
28
+ const [name, total, free] = line.split(',').map((part) => part.trim());
29
+ const totalMiB = Number(total);
30
+ const freeMiB = Number(free);
31
+ if (!name || !Number.isFinite(totalMiB))
32
+ return null;
33
+ return {
34
+ id: `CUDA${index}`,
35
+ name,
36
+ totalBytes: totalMiB * 1024 ** 2,
37
+ freeBytes: (Number.isFinite(freeMiB) ? freeMiB : totalMiB) * 1024 ** 2,
38
+ };
39
+ })
40
+ .filter((device) => device !== null);
41
+ export const inspectHardware = async () => {
42
+ const totalRamBytes = totalmem();
43
+ const freeRamBytes = freemem();
44
+ let devices = [];
45
+ let detectionSource = 'none';
46
+ // An installed llama-server reports exactly the devices it would itself use,
47
+ // which beats guessing from vendor tools.
48
+ const executable = (await findLlamaExecutables())[0];
49
+ if (executable) {
50
+ const inspection = await inspectLlamaDevices(executable).catch(() => null);
51
+ if (inspection?.devices.length) {
52
+ devices = inspection.devices;
53
+ detectionSource = 'llama.cpp';
54
+ }
55
+ }
56
+ if (devices.length === 0) {
57
+ const output = await runCommandCapture('nvidia-smi', [
58
+ '--query-gpu=name,memory.total,memory.free',
59
+ '--format=csv,noheader,nounits',
60
+ ]).catch(() => '');
61
+ const parsed = parseNvidiaSmi(output);
62
+ if (parsed.length > 0) {
63
+ devices = parsed;
64
+ detectionSource = 'nvidia-smi';
65
+ }
66
+ }
67
+ const largest = devices.reduce((best, device) => Math.max(best, device.freeBytes || device.totalBytes), 0);
68
+ const vramBudgetBytes = Math.floor(largest * VRAM_HEADROOM);
69
+ const modelBudgetBytes = vramBudgetBytes > 0 ? vramBudgetBytes : Math.floor(totalRamBytes * RAM_HEADROOM);
70
+ const suggestedVariant = process.platform === 'darwin'
71
+ ? 'cpu'
72
+ : detectionSource === 'nvidia-smi' ||
73
+ devices.some((device) => /^cuda/i.test(device.id) || /nvidia|geforce|rtx|quadro|tesla/i.test(device.name))
74
+ ? 'cuda'
75
+ : devices.length > 0
76
+ ? 'vulkan'
77
+ : 'cpu';
78
+ return {
79
+ devices,
80
+ detectionSource,
81
+ totalRamBytes,
82
+ freeRamBytes,
83
+ vramBudgetBytes,
84
+ modelBudgetBytes,
85
+ suggestedVariant,
86
+ };
87
+ };