@grainulation/silo 1.0.0 → 1.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/lib/server.js CHANGED
@@ -9,26 +9,36 @@
9
9
  * silo serve [--port 9095] [--root /path/to/repo]
10
10
  */
11
11
 
12
- import { createServer } from 'node:http';
13
- import { readFileSync, existsSync, readdirSync, writeFileSync, statSync } from 'node:fs';
14
- import { join, resolve, extname, dirname } from 'node:path';
15
- import { fileURLToPath } from 'node:url';
16
- import { createRequire } from 'node:module';
12
+ import { createServer } from "node:http";
13
+ import {
14
+ readFileSync,
15
+ existsSync,
16
+ readdirSync,
17
+ writeFileSync,
18
+ statSync,
19
+ } from "node:fs";
20
+ import { join, resolve, extname, dirname } from "node:path";
21
+ import { fileURLToPath } from "node:url";
22
+ import { createRequire } from "node:module";
17
23
 
18
24
  const __dirname = dirname(fileURLToPath(import.meta.url));
19
25
  const require = createRequire(import.meta.url);
20
26
 
21
27
  // ── Crash handlers ──
22
- process.on('uncaughtException', (err) => {
23
- process.stderr.write(`[${new Date().toISOString()}] FATAL: ${err.stack || err}\n`);
28
+ process.on("uncaughtException", (err) => {
29
+ process.stderr.write(
30
+ `[${new Date().toISOString()}] FATAL: ${err.stack || err}\n`,
31
+ );
24
32
  process.exit(1);
25
33
  });
26
- process.on('unhandledRejection', (reason) => {
27
- process.stderr.write(`[${new Date().toISOString()}] WARN unhandledRejection: ${reason}\n`);
34
+ process.on("unhandledRejection", (reason) => {
35
+ process.stderr.write(
36
+ `[${new Date().toISOString()}] WARN unhandledRejection: ${reason}\n`,
37
+ );
28
38
  });
29
39
 
30
- const PUBLIC_DIR = join(__dirname, '..', 'public');
31
- const PACKS_DIR = join(__dirname, '..', 'packs');
40
+ const PUBLIC_DIR = join(__dirname, "..", "public");
41
+ const PACKS_DIR = join(__dirname, "..", "packs");
32
42
 
33
43
  // ── CLI args ──────────────────────────────────────────────────────────────────
34
44
 
@@ -38,38 +48,67 @@ function arg(name, fallback) {
38
48
  return i !== -1 && args[i + 1] ? args[i + 1] : fallback;
39
49
  }
40
50
 
41
- const PORT = parseInt(arg('port', '9095'), 10);
42
- const ROOT = resolve(arg('root', process.cwd()));
43
- const CORS_ORIGIN = arg('cors', null);
51
+ const PORT = parseInt(arg("port", "9095"), 10);
52
+ const ROOT = resolve(arg("root", process.cwd()));
53
+ const CORS_ORIGIN = arg("cors", null);
44
54
 
45
55
  // ── Verbose logging ──────────────────────────────────────────────────────────
46
56
 
47
- const verbose = process.argv.includes('--verbose') || process.argv.includes('-v');
57
+ const verbose =
58
+ process.argv.includes("--verbose") || process.argv.includes("-v");
48
59
  function vlog(...a) {
49
60
  if (!verbose) return;
50
61
  const ts = new Date().toISOString();
51
- process.stderr.write(`[${ts}] silo: ${a.join(' ')}\n`);
62
+ process.stderr.write(`[${ts}] silo: ${a.join(" ")}\n`);
52
63
  }
53
64
 
54
65
  // ── Routes manifest ──────────────────────────────────────────────────────────
55
66
 
56
67
  const ROUTES = [
57
- { method: 'GET', path: '/health', description: 'Health check endpoint' },
58
- { method: 'GET', path: '/events', description: 'SSE event stream for live updates' },
59
- { method: 'GET', path: '/api/packs', description: 'List all available knowledge packs' },
60
- { method: 'GET', path: '/api/packs/:name', description: 'Get pack details and claims by name' },
61
- { method: 'POST', path: '/api/import', description: 'Import a pack into a target claims file' },
62
- { method: 'GET', path: '/api/search', description: 'Search claims by ?q, ?type, ?evidence, ?limit' },
63
- { method: 'POST', path: '/api/refresh', description: 'Reload packs from disk' },
64
- { method: 'GET', path: '/api/docs', description: 'This API documentation page' },
68
+ { method: "GET", path: "/health", description: "Health check endpoint" },
69
+ {
70
+ method: "GET",
71
+ path: "/events",
72
+ description: "SSE event stream for live updates",
73
+ },
74
+ {
75
+ method: "GET",
76
+ path: "/api/packs",
77
+ description: "List all available knowledge packs",
78
+ },
79
+ {
80
+ method: "GET",
81
+ path: "/api/packs/:name",
82
+ description: "Get pack details and claims by name",
83
+ },
84
+ {
85
+ method: "POST",
86
+ path: "/api/import",
87
+ description: "Import a pack into a target claims file",
88
+ },
89
+ {
90
+ method: "GET",
91
+ path: "/api/search",
92
+ description: "Search claims by ?q, ?type, ?evidence, ?limit",
93
+ },
94
+ {
95
+ method: "POST",
96
+ path: "/api/refresh",
97
+ description: "Reload packs from disk",
98
+ },
99
+ {
100
+ method: "GET",
101
+ path: "/api/docs",
102
+ description: "This API documentation page",
103
+ },
65
104
  ];
66
105
 
67
106
  // ── Load existing CJS modules via createRequire ──────────────────────────────
68
107
 
69
- const { Store } = require('./store.js');
70
- const { Search } = require('./search.js');
71
- const { Packs } = require('./packs.js');
72
- const { ImportExport } = require('./import-export.js');
108
+ const { Store } = require("./store.js");
109
+ const { Search } = require("./search.js");
110
+ const { Packs } = require("./packs.js");
111
+ const { ImportExport } = require("./import-export.js");
73
112
 
74
113
  const store = new Store();
75
114
  const search = new Search(store);
@@ -88,7 +127,11 @@ const sseClients = new Set();
88
127
  function broadcast(event) {
89
128
  const data = `data: ${JSON.stringify(event)}\n\n`;
90
129
  for (const res of sseClients) {
91
- try { res.write(data); } catch { sseClients.delete(res); }
130
+ try {
131
+ res.write(data);
132
+ } catch {
133
+ sseClients.delete(res);
134
+ }
92
135
  }
93
136
  }
94
137
 
@@ -100,24 +143,24 @@ function loadPacks() {
100
143
 
101
144
  function refreshState() {
102
145
  state.packs = loadPacks();
103
- broadcast({ type: 'state', data: state });
146
+ broadcast({ type: "state", data: state });
104
147
  }
105
148
 
106
149
  // ── MIME types ────────────────────────────────────────────────────────────────
107
150
 
108
151
  const MIME = {
109
- '.html': 'text/html; charset=utf-8',
110
- '.css': 'text/css; charset=utf-8',
111
- '.js': 'application/javascript; charset=utf-8',
112
- '.json': 'application/json; charset=utf-8',
113
- '.svg': 'image/svg+xml',
114
- '.png': 'image/png',
152
+ ".html": "text/html; charset=utf-8",
153
+ ".css": "text/css; charset=utf-8",
154
+ ".js": "application/javascript; charset=utf-8",
155
+ ".json": "application/json; charset=utf-8",
156
+ ".svg": "image/svg+xml",
157
+ ".png": "image/png",
115
158
  };
116
159
 
117
160
  // ── Helpers ───────────────────────────────────────────────────────────────────
118
161
 
119
162
  function jsonResponse(res, code, data) {
120
- res.writeHead(code, { 'Content-Type': 'application/json; charset=utf-8' });
163
+ res.writeHead(code, { "Content-Type": "application/json; charset=utf-8" });
121
164
  res.end(JSON.stringify(data));
122
165
  }
123
166
 
@@ -125,12 +168,23 @@ function readBody(req) {
125
168
  return new Promise((resolve, reject) => {
126
169
  const chunks = [];
127
170
  let size = 0;
128
- req.on('data', c => { size += c.length; if (size > 1048576) { resolve(null); req.destroy(); return; } chunks.push(c); });
129
- req.on('end', () => {
130
- try { resolve(JSON.parse(Buffer.concat(chunks).toString())); }
131
- catch { resolve(null); }
171
+ req.on("data", (c) => {
172
+ size += c.length;
173
+ if (size > 1048576) {
174
+ resolve(null);
175
+ req.destroy();
176
+ return;
177
+ }
178
+ chunks.push(c);
132
179
  });
133
- req.on('error', () => resolve(null));
180
+ req.on("end", () => {
181
+ try {
182
+ resolve(JSON.parse(Buffer.concat(chunks).toString()));
183
+ } catch {
184
+ resolve(null);
185
+ }
186
+ });
187
+ req.on("error", () => resolve(null));
134
188
  });
135
189
  }
136
190
 
@@ -141,71 +195,89 @@ const server = createServer(async (req, res) => {
141
195
 
142
196
  // CORS (only when --cors is passed)
143
197
  if (CORS_ORIGIN) {
144
- res.setHeader('Access-Control-Allow-Origin', CORS_ORIGIN);
145
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
146
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
198
+ res.setHeader("Access-Control-Allow-Origin", CORS_ORIGIN);
199
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
200
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
147
201
  }
148
202
 
149
- if (req.method === 'OPTIONS' && CORS_ORIGIN) {
203
+ if (req.method === "OPTIONS" && CORS_ORIGIN) {
150
204
  res.writeHead(204);
151
205
  res.end();
152
206
  return;
153
207
  }
154
208
 
155
- vlog('request', req.method, url.pathname);
209
+ vlog("request", req.method, url.pathname);
156
210
 
157
211
  // ── API: docs ──
158
- if (req.method === 'GET' && url.pathname === '/api/docs') {
212
+ if (req.method === "GET" && url.pathname === "/api/docs") {
159
213
  const html = `<!DOCTYPE html><html><head><title>silo API</title>
160
214
  <style>body{font-family:system-ui;background:#0a0e1a;color:#e8ecf1;max-width:800px;margin:40px auto;padding:0 20px}
161
215
  table{width:100%;border-collapse:collapse}th,td{padding:8px 12px;border-bottom:1px solid #1e293b;text-align:left}
162
216
  th{color:#9ca3af}code{background:#1e293b;padding:2px 6px;border-radius:4px;font-size:13px}</style></head>
163
217
  <body><h1>silo API</h1><p>${ROUTES.length} endpoints</p>
164
218
  <table><tr><th>Method</th><th>Path</th><th>Description</th></tr>
165
- ${ROUTES.map(r => '<tr><td><code>'+r.method+'</code></td><td><code>'+r.path+'</code></td><td>'+r.description+'</td></tr>').join('')}
219
+ ${ROUTES.map((r) => "<tr><td><code>" + r.method + "</code></td><td><code>" + r.path + "</code></td><td>" + r.description + "</td></tr>").join("")}
166
220
  </table></body></html>`;
167
- res.writeHead(200, { 'Content-Type': 'text/html' });
221
+ res.writeHead(200, { "Content-Type": "text/html" });
168
222
  res.end(html);
169
223
  return;
170
224
  }
171
225
 
172
226
  // ── Health check ──
173
- if (req.method === 'GET' && url.pathname === '/health') {
174
- jsonResponse(res, 200, { status: 'ok', uptime: process.uptime(), packs: state.packs.length });
227
+ if (req.method === "GET" && url.pathname === "/health") {
228
+ jsonResponse(res, 200, {
229
+ status: "ok",
230
+ uptime: process.uptime(),
231
+ packs: state.packs.length,
232
+ });
175
233
  return;
176
234
  }
177
235
 
178
236
  // ── SSE endpoint ──
179
- if (req.method === 'GET' && url.pathname === '/events') {
237
+ if (req.method === "GET" && url.pathname === "/events") {
180
238
  res.writeHead(200, {
181
- 'Content-Type': 'text/event-stream',
182
- 'Cache-Control': 'no-cache',
183
- 'Connection': 'keep-alive',
239
+ "Content-Type": "text/event-stream",
240
+ "Cache-Control": "no-cache",
241
+ Connection: "keep-alive",
184
242
  });
185
- res.write(`data: ${JSON.stringify({ type: 'state', data: state })}\n\n`);
243
+ res.write(`data: ${JSON.stringify({ type: "state", data: state })}\n\n`);
186
244
  const heartbeat = setInterval(() => {
187
- try { res.write(': heartbeat\n\n'); } catch { clearInterval(heartbeat); }
245
+ try {
246
+ res.write(": heartbeat\n\n");
247
+ } catch {
248
+ clearInterval(heartbeat);
249
+ }
188
250
  }, 15000);
189
251
  sseClients.add(res);
190
- vlog('sse', `client connected (${sseClients.size} total)`);
191
- req.on('close', () => { clearInterval(heartbeat); sseClients.delete(res); vlog('sse', `client disconnected (${sseClients.size} total)`); });
252
+ vlog("sse", `client connected (${sseClients.size} total)`);
253
+ req.on("close", () => {
254
+ clearInterval(heartbeat);
255
+ sseClients.delete(res);
256
+ vlog("sse", `client disconnected (${sseClients.size} total)`);
257
+ });
192
258
  return;
193
259
  }
194
260
 
195
261
  // ── API: list packs ──
196
- if (req.method === 'GET' && url.pathname === '/api/packs') {
262
+ if (req.method === "GET" && url.pathname === "/api/packs") {
197
263
  const packList = loadPacks();
198
264
  jsonResponse(res, 200, { packs: packList });
199
265
  return;
200
266
  }
201
267
 
202
268
  // ── API: get pack details ──
203
- if (req.method === 'GET' && url.pathname.startsWith('/api/packs/')) {
204
- const name = decodeURIComponent(url.pathname.slice('/api/packs/'.length));
205
- if (!name) { jsonResponse(res, 400, { error: 'missing pack name' }); return; }
269
+ if (req.method === "GET" && url.pathname.startsWith("/api/packs/")) {
270
+ const name = decodeURIComponent(url.pathname.slice("/api/packs/".length));
271
+ if (!name) {
272
+ jsonResponse(res, 400, { error: "missing pack name" });
273
+ return;
274
+ }
206
275
 
207
276
  const pack = packs.get(name);
208
- if (!pack) { jsonResponse(res, 404, { error: `pack "${name}" not found` }); return; }
277
+ if (!pack) {
278
+ jsonResponse(res, 404, { error: `pack "${name}" not found` });
279
+ return;
280
+ }
209
281
 
210
282
  jsonResponse(res, 200, {
211
283
  id: name,
@@ -219,25 +291,27 @@ ${ROUTES.map(r => '<tr><td><code>'+r.method+'</code></td><td><code>'+r.path+'</c
219
291
  }
220
292
 
221
293
  // ── API: import pack ──
222
- if (req.method === 'POST' && url.pathname === '/api/import') {
294
+ if (req.method === "POST" && url.pathname === "/api/import") {
223
295
  try {
224
296
  const body = await readBody(req);
225
297
  const { pack: packName, targetDir } = body;
226
298
  if (!packName || !targetDir) {
227
- jsonResponse(res, 400, { error: 'missing pack or targetDir' });
299
+ jsonResponse(res, 400, { error: "missing pack or targetDir" });
228
300
  return;
229
301
  }
230
302
 
231
303
  const absTarget = resolve(targetDir);
232
304
  if (!absTarget.startsWith(ROOT)) {
233
- jsonResponse(res, 400, { error: 'target directory must be within the project root' });
305
+ jsonResponse(res, 400, {
306
+ error: "target directory must be within the project root",
307
+ });
234
308
  return;
235
309
  }
236
310
 
237
- const targetPath = resolve(absTarget, 'claims.json');
311
+ const targetPath = resolve(absTarget, "claims.json");
238
312
  // Ensure the target file exists
239
313
  if (!existsSync(targetPath)) {
240
- writeFileSync(targetPath, '[]', 'utf-8');
314
+ writeFileSync(targetPath, "[]", "utf-8");
241
315
  }
242
316
 
243
317
  const result = io.pull(packName, targetPath);
@@ -255,11 +329,11 @@ ${ROUTES.map(r => '<tr><td><code>'+r.method+'</code></td><td><code>'+r.path+'</c
255
329
  }
256
330
 
257
331
  // ── API: search ──
258
- if (req.method === 'GET' && url.pathname === '/api/search') {
259
- const q = url.searchParams.get('q') || '';
260
- const type = url.searchParams.get('type') || undefined;
261
- const evidence = url.searchParams.get('evidence') || undefined;
262
- const limit = parseInt(url.searchParams.get('limit') || '20', 10);
332
+ if (req.method === "GET" && url.pathname === "/api/search") {
333
+ const q = url.searchParams.get("q") || "";
334
+ const type = url.searchParams.get("type") || undefined;
335
+ const evidence = url.searchParams.get("evidence") || undefined;
336
+ const limit = parseInt(url.searchParams.get("limit") || "20", 10);
263
337
 
264
338
  if (!q) {
265
339
  jsonResponse(res, 200, { query: q, results: [] });
@@ -276,26 +350,34 @@ ${ROUTES.map(r => '<tr><td><code>'+r.method+'</code></td><td><code>'+r.path+'</c
276
350
  // Also search directly in packs/ directory
277
351
  if (existsSync(PACKS_DIR)) {
278
352
  for (const file of readdirSync(PACKS_DIR)) {
279
- if (!file.endsWith('.json')) continue;
353
+ if (!file.endsWith(".json")) continue;
280
354
  try {
281
- const data = JSON.parse(readFileSync(join(PACKS_DIR, file), 'utf-8'));
282
- const packName = data.name || file.replace('.json', '');
355
+ const data = JSON.parse(readFileSync(join(PACKS_DIR, file), "utf-8"));
356
+ const packName = data.name || file.replace(".json", "");
283
357
  for (const claim of data.claims || []) {
284
358
  if (type && claim.type !== type) continue;
285
359
  if (evidence && claim.evidence !== evidence) continue;
286
360
  const searchable = [
287
- claim.content || claim.text || '',
288
- claim.type || '',
289
- claim.topic || '',
290
- (claim.tags || []).join(' '),
291
- ].join(' ').toLowerCase();
292
-
293
- const tokens = q.toLowerCase().split(/\s+/).filter(t => t.length > 1);
361
+ claim.content || claim.text || "",
362
+ claim.type || "",
363
+ claim.topic || "",
364
+ (claim.tags || []).join(" "),
365
+ ]
366
+ .join(" ")
367
+ .toLowerCase();
368
+
369
+ const tokens = q
370
+ .toLowerCase()
371
+ .split(/\s+/)
372
+ .filter((t) => t.length > 1);
294
373
  let score = 0;
295
374
  for (const token of tokens) {
296
375
  if (searchable.includes(token)) {
297
376
  score += 1;
298
- if (searchable.includes(` ${token} `) || searchable.startsWith(`${token} `)) {
377
+ if (
378
+ searchable.includes(` ${token} `) ||
379
+ searchable.startsWith(`${token} `)
380
+ ) {
299
381
  score += 0.5;
300
382
  }
301
383
  }
@@ -304,7 +386,9 @@ ${ROUTES.map(r => '<tr><td><code>'+r.method+'</code></td><td><code>'+r.path+'</c
304
386
  results.push({ claim, collection: `pack:${packName}`, score });
305
387
  }
306
388
  }
307
- } catch { /* skip malformed */ }
389
+ } catch {
390
+ /* skip malformed */
391
+ }
308
392
  }
309
393
  }
310
394
 
@@ -326,54 +410,61 @@ ${ROUTES.map(r => '<tr><td><code>'+r.method+'</code></td><td><code>'+r.path+'</c
326
410
  }
327
411
 
328
412
  // ── API: refresh ──
329
- if (req.method === 'POST' && url.pathname === '/api/refresh') {
413
+ if (req.method === "POST" && url.pathname === "/api/refresh") {
330
414
  refreshState();
331
415
  jsonResponse(res, 200, state);
332
416
  return;
333
417
  }
334
418
 
335
419
  // ── Static files ──
336
- let filePath = url.pathname === '/' ? '/index.html' : url.pathname;
337
- const resolved = resolve(PUBLIC_DIR, '.' + filePath);
420
+ let filePath = url.pathname === "/" ? "/index.html" : url.pathname;
421
+ const resolved = resolve(PUBLIC_DIR, "." + filePath);
338
422
  if (!resolved.startsWith(PUBLIC_DIR)) {
339
423
  res.writeHead(403);
340
- res.end('forbidden');
424
+ res.end("forbidden");
341
425
  return;
342
426
  }
343
427
 
344
428
  if (existsSync(resolved) && statSync(resolved).isFile()) {
345
429
  const ext = extname(resolved);
346
- const mime = MIME[ext] || 'application/octet-stream';
430
+ const mime = MIME[ext] || "application/octet-stream";
347
431
  try {
348
432
  const content = readFileSync(resolved);
349
- res.writeHead(200, { 'Content-Type': mime });
433
+ res.writeHead(200, { "Content-Type": mime });
350
434
  res.end(content);
351
435
  } catch {
352
436
  res.writeHead(500);
353
- res.end('read error');
437
+ res.end("read error");
354
438
  }
355
439
  return;
356
440
  }
357
441
 
358
442
  // ── 404 ──
359
- res.writeHead(404, { 'Content-Type': 'text/plain' });
360
- res.end('not found');
443
+ res.writeHead(404, { "Content-Type": "text/plain" });
444
+ res.end("not found");
361
445
  });
362
446
 
363
447
  // ── File watching (fingerprint-based polling) ─────────────────────────────────
364
448
 
365
- let lastFingerprint = '';
449
+ let lastFingerprint = "";
366
450
  function computeFingerprint() {
367
451
  const parts = [];
368
452
  try {
369
453
  if (existsSync(PACKS_DIR)) {
370
454
  for (const file of readdirSync(PACKS_DIR)) {
371
- if (!file.endsWith('.json')) continue;
372
- try { const s = statSync(join(PACKS_DIR, file)); parts.push(file + ':' + s.mtimeMs); } catch { /* skip */ }
455
+ if (!file.endsWith(".json")) continue;
456
+ try {
457
+ const s = statSync(join(PACKS_DIR, file));
458
+ parts.push(file + ":" + s.mtimeMs);
459
+ } catch {
460
+ /* skip */
461
+ }
373
462
  }
374
463
  }
375
- } catch { /* skip */ }
376
- return parts.join('|');
464
+ } catch {
465
+ /* skip */
466
+ }
467
+ return parts.join("|");
377
468
  }
378
469
 
379
470
  function startWatcher() {
@@ -383,7 +474,7 @@ function startWatcher() {
383
474
  if (fp !== lastFingerprint) {
384
475
  lastFingerprint = fp;
385
476
  refreshState();
386
- vlog('watcher', 'packs changed, state refreshed');
477
+ vlog("watcher", "packs changed, state refreshed");
387
478
  }
388
479
  }, 5000);
389
480
  }
@@ -391,23 +482,27 @@ function startWatcher() {
391
482
  // ── Graceful shutdown ─────────────────────────────────────────────────────────
392
483
  const shutdown = (signal) => {
393
484
  console.log(`\nsilo: ${signal} received, shutting down...`);
394
- for (const res of sseClients) { try { res.end(); } catch {} }
485
+ for (const res of sseClients) {
486
+ try {
487
+ res.end();
488
+ } catch {}
489
+ }
395
490
  sseClients.clear();
396
491
  server.close(() => process.exit(0));
397
492
  setTimeout(() => process.exit(1), 5000);
398
493
  };
399
- process.on('SIGTERM', () => shutdown('SIGTERM'));
400
- process.on('SIGINT', () => shutdown('SIGINT'));
494
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
495
+ process.on("SIGINT", () => shutdown("SIGINT"));
401
496
 
402
497
  // ── Start ─────────────────────────────────────────────────────────────────────
403
498
 
404
499
  refreshState();
405
500
  startWatcher();
406
501
 
407
- server.on('error', (err) => {
408
- if (err.code === 'EADDRINUSE') {
502
+ server.on("error", (err) => {
503
+ if (err.code === "EADDRINUSE") {
409
504
  console.error(`silo: port ${PORT} is already in use. Try --port <other>.`);
410
- } else if (err.code === 'EACCES') {
505
+ } else if (err.code === "EACCES") {
411
506
  console.error(`silo: port ${PORT} requires elevated privileges.`);
412
507
  } else {
413
508
  console.error(`silo: server error: ${err.message}`);
@@ -415,8 +510,8 @@ server.on('error', (err) => {
415
510
  process.exit(1);
416
511
  });
417
512
 
418
- server.listen(PORT, '127.0.0.1', () => {
419
- vlog('listen', `port=${PORT}`, `root=${ROOT}`);
513
+ server.listen(PORT, "127.0.0.1", () => {
514
+ vlog("listen", `port=${PORT}`, `root=${ROOT}`);
420
515
  console.log(`silo: serving on http://localhost:${PORT}`);
421
516
  console.log(` packs: ${state.packs.length} available`);
422
517
  console.log(` root: ${ROOT}`);
package/lib/store.js CHANGED
@@ -5,24 +5,29 @@
5
5
  * No database, no dependencies — just the filesystem.
6
6
  */
7
7
 
8
- const fs = require('node:fs');
9
- const path = require('node:path');
10
- const crypto = require('node:crypto');
8
+ const fs = require("node:fs");
9
+ const path = require("node:path");
10
+ const crypto = require("node:crypto");
11
11
 
12
- const DEFAULT_SILO_DIR = path.join(require('node:os').homedir(), '.silo');
12
+ const DEFAULT_SILO_DIR = path.join(require("node:os").homedir(), ".silo");
13
13
 
14
14
  class Store {
15
15
  constructor(siloDir = DEFAULT_SILO_DIR) {
16
16
  this.root = siloDir;
17
- this.claimsDir = path.join(siloDir, 'claims');
18
- this.templatesDir = path.join(siloDir, 'templates');
19
- this.packsDir = path.join(siloDir, 'packs');
20
- this.indexPath = path.join(siloDir, 'index.json');
17
+ this.claimsDir = path.join(siloDir, "claims");
18
+ this.templatesDir = path.join(siloDir, "templates");
19
+ this.packsDir = path.join(siloDir, "packs");
20
+ this.indexPath = path.join(siloDir, "index.json");
21
21
  }
22
22
 
23
23
  /** Ensure the silo directory structure exists. */
24
24
  init() {
25
- for (const dir of [this.root, this.claimsDir, this.templatesDir, this.packsDir]) {
25
+ for (const dir of [
26
+ this.root,
27
+ this.claimsDir,
28
+ this.templatesDir,
29
+ this.packsDir,
30
+ ]) {
26
31
  if (!fs.existsSync(dir)) {
27
32
  fs.mkdirSync(dir, { recursive: true });
28
33
  }
@@ -44,7 +49,7 @@ class Store {
44
49
  const entry = {
45
50
  id,
46
51
  name,
47
- type: 'claims',
52
+ type: "claims",
48
53
  claimCount: claims.length,
49
54
  hash: this._hash(JSON.stringify(claims)),
50
55
  storedAt: new Date().toISOString(),
@@ -77,12 +82,20 @@ class Store {
77
82
  this.init();
78
83
  const results = [];
79
84
  const index = this._readJSON(this.indexPath);
80
- for (const entry of (index.collections || [])) {
85
+ for (const entry of index.collections || []) {
81
86
  const data = this.getClaims(entry.id);
82
87
  if (!data) {
83
- results.push({ id: entry.id, ok: false, warning: 'Collection file missing' });
88
+ results.push({
89
+ id: entry.id,
90
+ ok: false,
91
+ warning: "Collection file missing",
92
+ });
84
93
  } else {
85
- results.push({ id: entry.id, ok: !data._integrityWarning, warning: data._integrityWarning || null });
94
+ results.push({
95
+ id: entry.id,
96
+ ok: !data._integrityWarning,
97
+ warning: data._integrityWarning || null,
98
+ });
86
99
  }
87
100
  }
88
101
  return results;
@@ -111,21 +124,24 @@ class Store {
111
124
  // --- Internal helpers ---
112
125
 
113
126
  _readJSON(filePath) {
114
- return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
127
+ return JSON.parse(fs.readFileSync(filePath, "utf-8"));
115
128
  }
116
129
 
117
130
  _writeJSON(filePath, data) {
118
- const tmp = filePath + '.tmp.' + process.pid;
119
- fs.writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n', 'utf-8');
131
+ const tmp = filePath + ".tmp." + process.pid;
132
+ fs.writeFileSync(tmp, JSON.stringify(data, null, 2) + "\n", "utf-8");
120
133
  fs.renameSync(tmp, filePath);
121
134
  }
122
135
 
123
136
  _hash(str) {
124
- return crypto.createHash('sha256').update(str).digest('hex');
137
+ return crypto.createHash("sha256").update(str).digest("hex");
125
138
  }
126
139
 
127
140
  _slugify(str) {
128
- return str.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
141
+ return str
142
+ .toLowerCase()
143
+ .replace(/[^a-z0-9]+/g, "-")
144
+ .replace(/^-|-$/g, "");
129
145
  }
130
146
 
131
147
  _addToIndex(entry) {