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.
- package/CHANGELOG.md +185 -0
- package/LICENSE +21 -0
- package/README.md +763 -0
- package/SECURITY.md +75 -0
- package/bin/threadshelf-mcp.js +12 -0
- package/bin/threadshelf.js +87 -0
- package/dist/mcp/server.js +388 -0
- package/dist/src/chunking.js +72 -0
- package/dist/src/cli.js +24 -0
- package/dist/src/embedding.js +59 -0
- package/dist/src/env.js +2 -0
- package/dist/src/generation/config.js +344 -0
- package/dist/src/generation/downloader.js +172 -0
- package/dist/src/generation/error-log.js +34 -0
- package/dist/src/generation/filesystem-browser.js +83 -0
- package/dist/src/generation/gguf-metadata.js +179 -0
- package/dist/src/generation/hardware.js +87 -0
- package/dist/src/generation/llama-install.js +563 -0
- package/dist/src/generation/llama-process.js +576 -0
- package/dist/src/generation/llama-profile.js +136 -0
- package/dist/src/generation/master-prompts.js +155 -0
- package/dist/src/generation/model-catalog.js +276 -0
- package/dist/src/generation/model-discovery.js +60 -0
- package/dist/src/generation/model-download.js +151 -0
- package/dist/src/generation/openai-compatible.js +231 -0
- package/dist/src/generation/providers/llama-cpp.js +97 -0
- package/dist/src/generation/providers/openrouter.js +106 -0
- package/dist/src/generation/quick-setup.js +215 -0
- package/dist/src/generation/registry.js +23 -0
- package/dist/src/generation/service.js +100 -0
- package/dist/src/generation/threads.js +311 -0
- package/dist/src/generation/types.js +1 -0
- package/dist/src/ingest-cli.js +95 -0
- package/dist/src/ingest.js +257 -0
- package/dist/src/load-env.js +17 -0
- package/dist/src/model-label.js +15 -0
- package/dist/src/parser.js +811 -0
- package/dist/src/paths.js +79 -0
- package/dist/src/routes/collections.js +97 -0
- package/dist/src/routes/files.js +136 -0
- package/dist/src/routes/generation.js +536 -0
- package/dist/src/routes/health.js +6 -0
- package/dist/src/routes/index.js +21 -0
- package/dist/src/routes/ingest.js +300 -0
- package/dist/src/routes/insights.js +24 -0
- package/dist/src/routes/loopback.js +15 -0
- package/dist/src/routes/model-catalog.js +178 -0
- package/dist/src/routes/search.js +57 -0
- package/dist/src/routes/stream-abort.js +23 -0
- package/dist/src/routes/thread.js +43 -0
- package/dist/src/search-cli.js +93 -0
- package/dist/src/server.js +78 -0
- package/dist/src/services/collections.js +58 -0
- package/dist/src/services/insights.js +111 -0
- package/dist/src/services/search.js +68 -0
- package/dist/src/services/stats.js +35 -0
- package/dist/src/services/thread.js +140 -0
- package/dist/src/store.js +1138 -0
- package/dist/src/validation.js +250 -0
- package/dist/src/watch.js +83 -0
- package/package.json +103 -0
- package/public/assets/index-CIm_Idqi.js +38 -0
- package/public/assets/index-Dv09K2vS.css +1 -0
- package/public/favicon.svg +6 -0
- package/public/index.html +28 -0
- package/scripts/openrouter-export-all.js +228 -0
- package/scripts/openrouter-export-browser.js +153 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
export class ValidationError extends Error {
|
|
2
|
+
field;
|
|
3
|
+
constructor(message, { field } = {}) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = 'ValidationError';
|
|
6
|
+
this.field = field;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
const COLLECTION_PATTERN = /^[a-z0-9][a-z0-9_-]{0,62}$/;
|
|
10
|
+
const RESERVED_COLLECTIONS = new Set(['all']);
|
|
11
|
+
const PROTECTED_COLLECTIONS = new Set(['all', 'chunks', 'threadshelf_conversations']);
|
|
12
|
+
export const MAX_QUERY_CHARS = 4000;
|
|
13
|
+
export const MAX_STRING_CHARS = 1_000_000;
|
|
14
|
+
export const MAX_SEARCH_N = 50;
|
|
15
|
+
export const normalizeCollectionName = (value, { field = 'collection' } = {}) => {
|
|
16
|
+
if (value === undefined || value === null) {
|
|
17
|
+
throw new ValidationError(`Missing ${field}`, { field });
|
|
18
|
+
}
|
|
19
|
+
const slug = String(value)
|
|
20
|
+
.trim()
|
|
21
|
+
.replace(/[^a-z0-9_-]/gi, '_')
|
|
22
|
+
.replace(/_+/g, '_')
|
|
23
|
+
.replace(/^[_-]+|[_-]+$/g, '')
|
|
24
|
+
.toLowerCase()
|
|
25
|
+
.slice(0, 63);
|
|
26
|
+
if (!slug) {
|
|
27
|
+
throw new ValidationError(`Invalid ${field}: empty after normalization`, { field });
|
|
28
|
+
}
|
|
29
|
+
if (!COLLECTION_PATTERN.test(slug)) {
|
|
30
|
+
throw new ValidationError(`Invalid ${field}: must match ${COLLECTION_PATTERN}`, { field });
|
|
31
|
+
}
|
|
32
|
+
if (RESERVED_COLLECTIONS.has(slug)) {
|
|
33
|
+
throw new ValidationError(`Reserved ${field}: ${slug}`, { field });
|
|
34
|
+
}
|
|
35
|
+
return slug;
|
|
36
|
+
};
|
|
37
|
+
export const normalizeCollectionSelector = (value, { defaultValue = 'chunks', field = 'collection' } = {}) => {
|
|
38
|
+
if (value === undefined || value === null || value === '')
|
|
39
|
+
return defaultValue;
|
|
40
|
+
const raw = String(value).trim().toLowerCase();
|
|
41
|
+
if (raw === 'all')
|
|
42
|
+
return 'all';
|
|
43
|
+
return normalizeCollectionName(raw, { field });
|
|
44
|
+
};
|
|
45
|
+
export const assertDeletableCollection = (name) => {
|
|
46
|
+
const normalized = normalizeCollectionName(name);
|
|
47
|
+
if (PROTECTED_COLLECTIONS.has(normalized)) {
|
|
48
|
+
throw new ValidationError(`Collection cannot be deleted: ${normalized}`, {
|
|
49
|
+
field: 'collection',
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return normalized;
|
|
53
|
+
};
|
|
54
|
+
export const assertClearableCollection = (name) => {
|
|
55
|
+
const normalized = normalizeCollectionName(name);
|
|
56
|
+
if (normalized === 'all' || normalized === 'threadshelf_conversations') {
|
|
57
|
+
throw new ValidationError(`Collection cannot be cleared: ${normalized}`, {
|
|
58
|
+
field: 'collection',
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return normalized;
|
|
62
|
+
};
|
|
63
|
+
export const normalizeQuery = (value, { field = 'q', maxLength = MAX_QUERY_CHARS } = {}) => {
|
|
64
|
+
if (value === undefined || value === null) {
|
|
65
|
+
throw new ValidationError(`Missing ${field}`, { field });
|
|
66
|
+
}
|
|
67
|
+
if (typeof value !== 'string') {
|
|
68
|
+
throw new ValidationError(`Invalid ${field}: must be a string`, { field });
|
|
69
|
+
}
|
|
70
|
+
const trimmed = value.trim();
|
|
71
|
+
if (!trimmed) {
|
|
72
|
+
throw new ValidationError(`Invalid ${field}: empty`, { field });
|
|
73
|
+
}
|
|
74
|
+
if (trimmed.length > maxLength) {
|
|
75
|
+
throw new ValidationError(`Invalid ${field}: max ${maxLength} chars`, { field });
|
|
76
|
+
}
|
|
77
|
+
return trimmed;
|
|
78
|
+
};
|
|
79
|
+
export const normalizeCount = (value, { defaultValue, min = 1, max = MAX_SEARCH_N, field = 'n', } = {}) => {
|
|
80
|
+
if (value === undefined || value === null || value === '') {
|
|
81
|
+
if (defaultValue === undefined) {
|
|
82
|
+
throw new ValidationError(`Missing ${field}`, { field });
|
|
83
|
+
}
|
|
84
|
+
return defaultValue;
|
|
85
|
+
}
|
|
86
|
+
const num = typeof value === 'number' ? value : Number(value);
|
|
87
|
+
if (!Number.isFinite(num)) {
|
|
88
|
+
throw new ValidationError(`Invalid ${field}: not a number`, { field });
|
|
89
|
+
}
|
|
90
|
+
if (!Number.isInteger(num)) {
|
|
91
|
+
throw new ValidationError(`Invalid ${field}: must be an integer`, { field });
|
|
92
|
+
}
|
|
93
|
+
if (num < min) {
|
|
94
|
+
throw new ValidationError(`Invalid ${field}: minimum ${min}`, { field });
|
|
95
|
+
}
|
|
96
|
+
if (num > max) {
|
|
97
|
+
throw new ValidationError(`Invalid ${field}: maximum ${max}`, { field });
|
|
98
|
+
}
|
|
99
|
+
return num;
|
|
100
|
+
};
|
|
101
|
+
const ALLOWED_ROLES = new Set(['user', 'thinking', 'ai']);
|
|
102
|
+
export const normalizeRoles = (value, { field = 'roles' } = {}) => {
|
|
103
|
+
if (value === undefined || value === null || value === '')
|
|
104
|
+
return null;
|
|
105
|
+
let list;
|
|
106
|
+
if (Array.isArray(value)) {
|
|
107
|
+
list = value;
|
|
108
|
+
}
|
|
109
|
+
else if (typeof value === 'string') {
|
|
110
|
+
list = value.split(',');
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
throw new ValidationError(`Invalid ${field}: must be string or array`, { field });
|
|
114
|
+
}
|
|
115
|
+
const normalized = list.map((entry) => String(entry).trim().toLowerCase()).filter(Boolean);
|
|
116
|
+
if (normalized.length === 0)
|
|
117
|
+
return null;
|
|
118
|
+
for (const role of normalized) {
|
|
119
|
+
if (!ALLOWED_ROLES.has(role)) {
|
|
120
|
+
throw new ValidationError(`Invalid ${field}: unknown role "${role}"`, { field });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return [...new Set(normalized)];
|
|
124
|
+
};
|
|
125
|
+
export const normalizeOptionalString = (value, { field = 'value', maxLength = 200 } = {}) => {
|
|
126
|
+
if (value === undefined || value === null || value === '')
|
|
127
|
+
return undefined;
|
|
128
|
+
if (typeof value !== 'string') {
|
|
129
|
+
throw new ValidationError(`Invalid ${field}: must be a string`, { field });
|
|
130
|
+
}
|
|
131
|
+
const trimmed = value.trim();
|
|
132
|
+
if (!trimmed)
|
|
133
|
+
return undefined;
|
|
134
|
+
if (trimmed.length > maxLength) {
|
|
135
|
+
throw new ValidationError(`Invalid ${field}: max ${maxLength} chars`, { field });
|
|
136
|
+
}
|
|
137
|
+
return trimmed;
|
|
138
|
+
};
|
|
139
|
+
export const normalizeOptionalIsoDate = (value, { field = 'date', endOfDay = false } = {}) => {
|
|
140
|
+
if (value === undefined || value === null || value === '')
|
|
141
|
+
return undefined;
|
|
142
|
+
if (typeof value !== 'string') {
|
|
143
|
+
throw new ValidationError(`Invalid ${field}: must be a string`, { field });
|
|
144
|
+
}
|
|
145
|
+
const trimmed = value.trim();
|
|
146
|
+
if (!trimmed)
|
|
147
|
+
return undefined;
|
|
148
|
+
const normalized = /^\d{4}-\d{2}-\d{2}$/.test(trimmed)
|
|
149
|
+
? `${trimmed}T${endOfDay ? '23:59:59.999' : '00:00:00.000'}Z`
|
|
150
|
+
: trimmed;
|
|
151
|
+
const timestamp = Date.parse(normalized);
|
|
152
|
+
if (Number.isNaN(timestamp)) {
|
|
153
|
+
throw new ValidationError(`Invalid ${field}: must be an ISO-like date string`, { field });
|
|
154
|
+
}
|
|
155
|
+
return new Date(timestamp).toISOString();
|
|
156
|
+
};
|
|
157
|
+
export const normalizeDateRange = (fromValue, toValue) => {
|
|
158
|
+
const from = normalizeOptionalIsoDate(fromValue, { field: 'from' });
|
|
159
|
+
const to = normalizeOptionalIsoDate(toValue, { field: 'to', endOfDay: true });
|
|
160
|
+
if (from && to && from > to) {
|
|
161
|
+
throw new ValidationError('Invalid date range: from must be before to', { field: 'from' });
|
|
162
|
+
}
|
|
163
|
+
return { from, to };
|
|
164
|
+
};
|
|
165
|
+
export const normalizeSearchMode = (value, { field = 'mode' } = {}) => {
|
|
166
|
+
if (value === undefined || value === null || value === '')
|
|
167
|
+
return 'semantic';
|
|
168
|
+
const mode = String(value).trim().toLowerCase();
|
|
169
|
+
if (mode === 'semantic' || mode === 'keyword')
|
|
170
|
+
return mode;
|
|
171
|
+
throw new ValidationError(`Invalid ${field}: expected "semantic" or "keyword"`, { field });
|
|
172
|
+
};
|
|
173
|
+
export const normalizeBoolean = (value, { defaultValue = false, field = 'flag' } = {}) => {
|
|
174
|
+
if (value === undefined || value === null || value === '')
|
|
175
|
+
return defaultValue;
|
|
176
|
+
if (typeof value === 'boolean')
|
|
177
|
+
return value;
|
|
178
|
+
if (typeof value === 'number')
|
|
179
|
+
return value !== 0;
|
|
180
|
+
if (typeof value === 'string') {
|
|
181
|
+
const v = value.trim().toLowerCase();
|
|
182
|
+
if (['1', 'true', 'yes', 'on'].includes(v))
|
|
183
|
+
return true;
|
|
184
|
+
if (['0', 'false', 'no', 'off'].includes(v))
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
throw new ValidationError(`Invalid ${field}: expected boolean`, { field });
|
|
188
|
+
};
|
|
189
|
+
export const validateTurn = (turn, { index } = {}) => {
|
|
190
|
+
if (!turn || typeof turn !== 'object' || Array.isArray(turn)) {
|
|
191
|
+
throw new ValidationError(`Invalid turn at index ${index}: must be an object`);
|
|
192
|
+
}
|
|
193
|
+
const t = turn;
|
|
194
|
+
const roleKeys = ['user', 'thinking', 'ai'].filter((key) => t[key] !== undefined);
|
|
195
|
+
if (roleKeys.length !== 1) {
|
|
196
|
+
throw new ValidationError(`Invalid turn at index ${index}: must have exactly one of user/thinking/ai (had ${roleKeys.join(', ') || 'none'})`);
|
|
197
|
+
}
|
|
198
|
+
const role = roleKeys[0];
|
|
199
|
+
if (typeof t[role] !== 'string') {
|
|
200
|
+
throw new ValidationError(`Invalid turn at index ${index}: ${role} must be a string`);
|
|
201
|
+
}
|
|
202
|
+
if (t[role].length === 0) {
|
|
203
|
+
throw new ValidationError(`Invalid turn at index ${index}: ${role} must not be empty`);
|
|
204
|
+
}
|
|
205
|
+
if (t.model !== undefined && (typeof t.model !== 'string' || !t.model.trim())) {
|
|
206
|
+
throw new ValidationError(`Invalid turn at index ${index}: model must be a non-empty string when present`);
|
|
207
|
+
}
|
|
208
|
+
if (t.createdAt !== undefined &&
|
|
209
|
+
(typeof t.createdAt !== 'string' || Number.isNaN(Date.parse(t.createdAt)))) {
|
|
210
|
+
throw new ValidationError(`Invalid turn at index ${index}: createdAt must be an ISO-like date string when present`);
|
|
211
|
+
}
|
|
212
|
+
if (t.createdInThreadShelf !== undefined && typeof t.createdInThreadShelf !== 'boolean') {
|
|
213
|
+
throw new ValidationError(`Invalid turn at index ${index}: createdInThreadShelf must be a boolean when present`);
|
|
214
|
+
}
|
|
215
|
+
if (t.generationProvider !== undefined &&
|
|
216
|
+
!['llama-cpp', 'openrouter'].includes(String(t.generationProvider))) {
|
|
217
|
+
throw new ValidationError(`Invalid turn at index ${index}: generationProvider must be llama-cpp or openrouter`);
|
|
218
|
+
}
|
|
219
|
+
const normalized = {
|
|
220
|
+
role,
|
|
221
|
+
text: t[role],
|
|
222
|
+
model: t.model,
|
|
223
|
+
};
|
|
224
|
+
if (t.createdAt !== undefined) {
|
|
225
|
+
return { ...normalized, createdAt: t.createdAt };
|
|
226
|
+
}
|
|
227
|
+
return normalized;
|
|
228
|
+
};
|
|
229
|
+
export const validateTurns = (turns) => {
|
|
230
|
+
if (!Array.isArray(turns)) {
|
|
231
|
+
throw new ValidationError('Invalid turns: expected an array');
|
|
232
|
+
}
|
|
233
|
+
turns.forEach((turn, index) => validateTurn(turn, { index }));
|
|
234
|
+
return turns;
|
|
235
|
+
};
|
|
236
|
+
export const isSafeRelativePath = (input) => {
|
|
237
|
+
if (typeof input !== 'string' || input.length === 0)
|
|
238
|
+
return false;
|
|
239
|
+
if (input.length > 1024)
|
|
240
|
+
return false;
|
|
241
|
+
if (input.includes('\0'))
|
|
242
|
+
return false;
|
|
243
|
+
const normalized = input.replace(/\\/g, '/');
|
|
244
|
+
if (normalized.startsWith('/'))
|
|
245
|
+
return false;
|
|
246
|
+
if (/^[a-zA-Z]:/.test(normalized))
|
|
247
|
+
return false;
|
|
248
|
+
const segments = normalized.split('/');
|
|
249
|
+
return segments.every((segment) => segment !== '..' && segment.length <= 255);
|
|
250
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { watch } from 'fs';
|
|
2
|
+
import { stat } from 'fs/promises';
|
|
3
|
+
import { basename, join, resolve } from 'path';
|
|
4
|
+
import { ingestFiles, isExportFileName } from './ingest.js';
|
|
5
|
+
// Collects change events and flushes a deduplicated batch once the folder has
|
|
6
|
+
// been quiet for debounceMs. Flushes never overlap: changes arriving while a
|
|
7
|
+
// flush runs queue up for the next one.
|
|
8
|
+
export const createChangeBatcher = ({ debounceMs, onFlush, }) => {
|
|
9
|
+
const pending = new Set();
|
|
10
|
+
let timer = null;
|
|
11
|
+
let running = Promise.resolve();
|
|
12
|
+
const flush = () => {
|
|
13
|
+
timer = null;
|
|
14
|
+
const batch = [...pending];
|
|
15
|
+
pending.clear();
|
|
16
|
+
if (!batch.length)
|
|
17
|
+
return;
|
|
18
|
+
running = running.then(() => onFlush(batch)).catch(() => { });
|
|
19
|
+
};
|
|
20
|
+
return {
|
|
21
|
+
add(path) {
|
|
22
|
+
pending.add(path);
|
|
23
|
+
if (timer)
|
|
24
|
+
clearTimeout(timer);
|
|
25
|
+
timer = setTimeout(flush, debounceMs);
|
|
26
|
+
},
|
|
27
|
+
async settle() {
|
|
28
|
+
if (timer) {
|
|
29
|
+
clearTimeout(timer);
|
|
30
|
+
flush();
|
|
31
|
+
}
|
|
32
|
+
await running;
|
|
33
|
+
},
|
|
34
|
+
close() {
|
|
35
|
+
if (timer)
|
|
36
|
+
clearTimeout(timer);
|
|
37
|
+
timer = null;
|
|
38
|
+
pending.clear();
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
export const watchFolder = (collection, folderPath, opts = {}) => {
|
|
43
|
+
const resolved = resolve(folderPath);
|
|
44
|
+
const runIngest = opts.ingest ?? ((files) => ingestFiles(collection, files));
|
|
45
|
+
const batcher = createChangeBatcher({
|
|
46
|
+
debounceMs: opts.debounceMs ?? 2000,
|
|
47
|
+
onFlush: async (paths) => {
|
|
48
|
+
const files = [];
|
|
49
|
+
for (const path of paths) {
|
|
50
|
+
try {
|
|
51
|
+
const info = await stat(path);
|
|
52
|
+
if (info.isFile())
|
|
53
|
+
files.push(path);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// Deleted or unreadable between event and flush — keep the index as-is.
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (!files.length)
|
|
60
|
+
return;
|
|
61
|
+
try {
|
|
62
|
+
const result = await runIngest(files);
|
|
63
|
+
opts.onBatch?.(files, result);
|
|
64
|
+
}
|
|
65
|
+
catch (e) {
|
|
66
|
+
opts.onBatch?.(files, null, e);
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
const watcher = watch(resolved, { recursive: true }, (_event, filename) => {
|
|
71
|
+
if (!filename)
|
|
72
|
+
return;
|
|
73
|
+
if (!isExportFileName(basename(filename)))
|
|
74
|
+
return;
|
|
75
|
+
batcher.add(join(resolved, filename));
|
|
76
|
+
});
|
|
77
|
+
return {
|
|
78
|
+
close: async () => {
|
|
79
|
+
watcher.close();
|
|
80
|
+
await batcher.settle();
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "threadshelf",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "Local semantic search, backup, and RAG for your AI chats — ChatGPT, Claude, Google AI Studio, OpenRouter, LM Studio, and Grok — searchable from a web UI, HTTP API, or MCP",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=20.19.0"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/ChrystianSchutz/ThreadShelf.git"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/ChrystianSchutz/ThreadShelf#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/ChrystianSchutz/ThreadShelf/issues"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"semantic-search",
|
|
20
|
+
"rag",
|
|
21
|
+
"local-first",
|
|
22
|
+
"chatgpt",
|
|
23
|
+
"claude",
|
|
24
|
+
"openrouter",
|
|
25
|
+
"lm-studio",
|
|
26
|
+
"grok",
|
|
27
|
+
"lancedb",
|
|
28
|
+
"mcp"
|
|
29
|
+
],
|
|
30
|
+
"bin": {
|
|
31
|
+
"threadshelf": "./bin/threadshelf.js",
|
|
32
|
+
"threadshelf-mcp": "./bin/threadshelf-mcp.js"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"bin/",
|
|
36
|
+
"dist/",
|
|
37
|
+
"public/",
|
|
38
|
+
"scripts/openrouter-export-all.js",
|
|
39
|
+
"scripts/openrouter-export-browser.js",
|
|
40
|
+
"README.md",
|
|
41
|
+
"CHANGELOG.md",
|
|
42
|
+
"LICENSE",
|
|
43
|
+
"SECURITY.md"
|
|
44
|
+
],
|
|
45
|
+
"workspaces": [
|
|
46
|
+
"client"
|
|
47
|
+
],
|
|
48
|
+
"overrides": {
|
|
49
|
+
"@huggingface/transformers": {
|
|
50
|
+
"sharp": "0.35.3"
|
|
51
|
+
},
|
|
52
|
+
"onnxruntime-node": {
|
|
53
|
+
"adm-zip": "0.6.0"
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"test": "node scripts/run-node-tests.js test",
|
|
58
|
+
"test:e2e": "node scripts/run-node-tests.js test/e2e",
|
|
59
|
+
"test:playwright": "playwright test",
|
|
60
|
+
"check:repo": "tsx scripts/check-repo-hygiene.ts",
|
|
61
|
+
"check": "npm run check:repo && npm run lint && npx tsc --noEmit && npm test && npm run test:e2e && npm run build:client && npm run test:playwright",
|
|
62
|
+
"parse": "tsx src/cli.ts",
|
|
63
|
+
"ingest": "tsx src/ingest-cli.ts",
|
|
64
|
+
"search": "tsx src/search-cli.ts",
|
|
65
|
+
"setup:llama": "tsx scripts/setup-llama-cpp.ts",
|
|
66
|
+
"start": "tsx src/server.ts",
|
|
67
|
+
"dev": "tsx --watch src/server.ts",
|
|
68
|
+
"dev:client": "npm run dev --workspace client",
|
|
69
|
+
"build": "npm run build:client && npm run build:server",
|
|
70
|
+
"build:client": "npm run build --workspace client",
|
|
71
|
+
"docs:screenshots": "tsx scripts/capture-docs-screenshots.ts",
|
|
72
|
+
"mcp": "tsx mcp/server.ts",
|
|
73
|
+
"lint": "eslint src/ mcp/ client/src/",
|
|
74
|
+
"format": "prettier --write \"src/**/*.ts\" \"client/src/**/*.{ts,tsx}\"",
|
|
75
|
+
"build:server": "tsc -p tsconfig.build.json",
|
|
76
|
+
"build:package": "npm run clean:dist && npm run build:client && npm run build:server",
|
|
77
|
+
"clean:dist": "node -e \"fs.rmSync('dist',{recursive:true,force:true})\"",
|
|
78
|
+
"prepack": "npm run build:package",
|
|
79
|
+
"pack:verify": "node scripts/verify-package.js"
|
|
80
|
+
},
|
|
81
|
+
"dependencies": {
|
|
82
|
+
"@huggingface/transformers": "^4.2.0",
|
|
83
|
+
"@lancedb/lancedb": "^0.29.0",
|
|
84
|
+
"express": "^5.2.1",
|
|
85
|
+
"multer": "^2.2.0"
|
|
86
|
+
},
|
|
87
|
+
"devDependencies": {
|
|
88
|
+
"@eslint/js": "^10.0.1",
|
|
89
|
+
"@playwright/test": "^1.62.1",
|
|
90
|
+
"@types/express": "^5.0.6",
|
|
91
|
+
"@types/multer": "^2.1.0",
|
|
92
|
+
"@types/node": "^26.1.2",
|
|
93
|
+
"eslint": "^10.5.0",
|
|
94
|
+
"eslint-config-prettier": "^10.1.8",
|
|
95
|
+
"eslint-plugin-react-hooks": "^7.1.1",
|
|
96
|
+
"prettier": "^3.8.4",
|
|
97
|
+
"sass": "^1.101.0",
|
|
98
|
+
"tsx": "^4.22.4",
|
|
99
|
+
"typescript": "^6.0.3",
|
|
100
|
+
"typescript-eslint": "^8.61.1",
|
|
101
|
+
"vite": "^8.2.0"
|
|
102
|
+
}
|
|
103
|
+
}
|