@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/bin/silo.js CHANGED
@@ -16,24 +16,29 @@
16
16
  * silo serve-mcp Start the MCP server on stdio
17
17
  */
18
18
 
19
- const { Store } = require('../lib/store.js');
20
- const { Search } = require('../lib/search.js');
21
- const { ImportExport } = require('../lib/import-export.js');
22
- const { Templates } = require('../lib/templates.js');
23
- const { Packs } = require('../lib/packs.js');
19
+ const { Store } = require("../lib/store.js");
20
+ const { Search } = require("../lib/search.js");
21
+ const { ImportExport } = require("../lib/import-export.js");
22
+ const { Templates } = require("../lib/templates.js");
23
+ const { Packs } = require("../lib/packs.js");
24
+ const { Graph } = require("../lib/graph.js");
24
25
 
25
26
  // ── --version / -v (before verbose check) ──
26
- if (process.argv.includes('--version') || (process.argv.includes('-v') && process.argv.length === 3)) {
27
- const pkg = require('../package.json');
28
- process.stdout.write(pkg.version + '\n');
27
+ if (
28
+ process.argv.includes("--version") ||
29
+ (process.argv.includes("-v") && process.argv.length === 3)
30
+ ) {
31
+ const pkg = require("../package.json");
32
+ process.stdout.write(pkg.version + "\n");
29
33
  process.exit(0);
30
34
  }
31
35
 
32
- const verbose = process.argv.includes('--verbose') || process.argv.includes('-v');
36
+ const verbose =
37
+ process.argv.includes("--verbose") || process.argv.includes("-v");
33
38
  function vlog(...a) {
34
39
  if (!verbose) return;
35
40
  const ts = new Date().toISOString();
36
- process.stderr.write(`[${ts}] silo: ${a.join(' ')}\n`);
41
+ process.stderr.write(`[${ts}] silo: ${a.join(" ")}\n`);
37
42
  }
38
43
 
39
44
  const store = new Store();
@@ -45,9 +50,9 @@ const packs = new Packs(store);
45
50
  const args = process.argv.slice(2);
46
51
  const command = args[0];
47
52
 
48
- vlog('startup', `command=${command || '(none)'}`, `cwd=${process.cwd()}`);
53
+ vlog("startup", `command=${command || "(none)"}`, `cwd=${process.cwd()}`);
49
54
 
50
- const jsonMode = args.includes('--json');
55
+ const jsonMode = args.includes("--json");
51
56
 
52
57
  function flag(name) {
53
58
  const idx = args.indexOf(`--${name}`);
@@ -60,17 +65,17 @@ function flagList(name) {
60
65
  if (idx === -1) return [];
61
66
  const values = [];
62
67
  for (let i = idx + 1; i < args.length; i++) {
63
- if (args[i].startsWith('--')) break;
68
+ if (args[i].startsWith("--")) break;
64
69
  values.push(args[i]);
65
70
  }
66
71
  return values;
67
72
  }
68
73
 
69
74
  function print(obj) {
70
- if (typeof obj === 'string') {
71
- process.stdout.write(obj + '\n');
75
+ if (typeof obj === "string") {
76
+ process.stdout.write(obj + "\n");
72
77
  } else {
73
- process.stdout.write(JSON.stringify(obj, null, 2) + '\n');
78
+ process.stdout.write(JSON.stringify(obj, null, 2) + "\n");
74
79
  }
75
80
  }
76
81
 
@@ -86,6 +91,7 @@ Commands:
86
91
  packs List available knowledge packs
87
92
  templates List available sprint templates
88
93
  install <file> Install a pack from a file
94
+ graph [stats|related|topic|clusters|export] Knowledge graph operations
89
95
  analyze Run cross-library analytics (requires harvest)
90
96
  serve [--port 9095] Start the knowledge browser UI
91
97
  serve-mcp Start the MCP server on stdio
@@ -100,135 +106,160 @@ Examples:
100
106
 
101
107
  try {
102
108
  switch (command) {
103
- case 'list': {
109
+ case "list": {
104
110
  const collections = store.list();
105
111
  if (jsonMode) {
106
112
  print(JSON.stringify(collections));
107
113
  break;
108
114
  }
109
115
  if (collections.length === 0) {
110
- print('No stored collections. Use "silo store" to save claims or "silo packs" to see built-in packs.');
116
+ print(
117
+ 'No stored collections. Use "silo store" to save claims or "silo packs" to see built-in packs.',
118
+ );
111
119
  } else {
112
- print('Stored collections:\n');
120
+ print("Stored collections:\n");
113
121
  for (const c of collections) {
114
- print(` ${c.id} (${c.type || 'claims'}, ${c.claimCount} claims) ${c.storedAt || ''}`);
122
+ print(
123
+ ` ${c.id} (${c.type || "claims"}, ${c.claimCount} claims) ${c.storedAt || ""}`,
124
+ );
115
125
  }
116
126
  }
117
127
  break;
118
128
  }
119
129
 
120
- case 'pull': {
130
+ case "pull": {
121
131
  const source = args[1];
122
- const into = flag('into');
132
+ const into = flag("into");
123
133
  if (!source || !into) {
124
- print('Usage: silo pull <pack> --into <file>');
134
+ print("Usage: silo pull <pack> --into <file>");
125
135
  process.exit(1);
126
136
  }
127
- const dryRun = args.includes('--dry-run');
128
- const types = flagList('type');
129
- const filterIds = flag('filter');
130
- const ids = filterIds ? filterIds.split(',').map((s) => s.trim()) : undefined;
131
- const result = io.pull(source, into, { types: types.length ? types : undefined, ids, dryRun });
137
+ const dryRun = args.includes("--dry-run");
138
+ const types = flagList("type");
139
+ const filterIds = flag("filter");
140
+ const ids = filterIds
141
+ ? filterIds.split(",").map((s) => s.trim())
142
+ : undefined;
143
+ const result = io.pull(source, into, {
144
+ types: types.length ? types : undefined,
145
+ ids,
146
+ dryRun,
147
+ });
132
148
  if (dryRun) {
133
149
  print(`Dry run: would import ${result.wouldImport} claims`);
134
150
  print(result.claims);
135
151
  } else {
136
- print(`Imported ${result.imported} claims into ${into} (${result.skippedDuplicates} duplicates skipped, ${result.totalClaims} total)`);
152
+ print(
153
+ `Imported ${result.imported} claims into ${into} (${result.skippedDuplicates} duplicates skipped, ${result.totalClaims} total)`,
154
+ );
137
155
  }
138
156
  break;
139
157
  }
140
158
 
141
- case 'store': {
159
+ case "store": {
142
160
  const name = args[1];
143
- const from = flag('from');
161
+ const from = flag("from");
144
162
  if (!name || !from) {
145
- print('Usage: silo store <name> --from <file>');
163
+ print("Usage: silo store <name> --from <file>");
146
164
  process.exit(1);
147
165
  }
148
166
  const result = io.push(from, name);
149
- print(`Stored "${name}" (${result.claimCount} claims, hash: ${result.hash})`);
167
+ print(
168
+ `Stored "${name}" (${result.claimCount} claims, hash: ${result.hash})`,
169
+ );
150
170
  break;
151
171
  }
152
172
 
153
- case 'search': {
154
- const query = args.slice(1).filter((a) => !a.startsWith('--')).join(' ');
173
+ case "search": {
174
+ const query = args
175
+ .slice(1)
176
+ .filter((a) => !a.startsWith("--"))
177
+ .join(" ");
155
178
  if (!query) {
156
- print('Usage: silo search <query> [--type <type>] [--evidence <tier>]');
179
+ print("Usage: silo search <query> [--type <type>] [--evidence <tier>]");
157
180
  process.exit(1);
158
181
  }
159
- const type = flag('type');
160
- const tier = flag('tier');
182
+ const type = flag("type");
183
+ const tier = flag("tier");
161
184
  const results = search.query(query, { type, tier });
162
185
  if (jsonMode) {
163
186
  print(JSON.stringify(results));
164
187
  break;
165
188
  }
166
189
  if (results.length === 0) {
167
- print('No matches found.');
190
+ print("No matches found.");
168
191
  } else {
169
192
  print(`${results.length} result(s):\n`);
170
193
  for (const r of results) {
171
- const text = r.claim.content || r.claim.text || '';
172
- const tier = r.claim.evidence || r.claim.tier || 'unknown';
173
- print(` [${r.claim.id}] (${r.claim.type}, ${tier}) ${text.slice(0, 120)}${text.length > 120 ? '...' : ''}`);
194
+ const text = r.claim.content || r.claim.text || "";
195
+ const tier = r.claim.evidence || r.claim.tier || "unknown";
196
+ print(
197
+ ` [${r.claim.id}] (${r.claim.type}, ${tier}) ${text.slice(0, 120)}${text.length > 120 ? "..." : ""}`,
198
+ );
174
199
  print(` from: ${r.collection} score: ${r.score}\n`);
175
200
  }
176
201
  }
177
202
  break;
178
203
  }
179
204
 
180
- case 'publish': {
205
+ case "publish": {
181
206
  const name = args[1];
182
- const collections = flagList('collections');
207
+ const collections = flagList("collections");
183
208
  if (!name || collections.length === 0) {
184
- print('Usage: silo publish <name> --collections <id1> <id2> ...');
209
+ print("Usage: silo publish <name> --collections <id1> <id2> ...");
185
210
  process.exit(1);
186
211
  }
187
- const desc = flag('description') || '';
212
+ const desc = flag("description") || "";
188
213
  const result = packs.bundle(name, collections, { description: desc });
189
- print(`Published pack "${name}" (${result.claimCount} claims) -> ${result.path}`);
214
+ print(
215
+ `Published pack "${name}" (${result.claimCount} claims) -> ${result.path}`,
216
+ );
190
217
  break;
191
218
  }
192
219
 
193
- case 'packs': {
220
+ case "packs": {
194
221
  const allPacks = packs.list();
195
222
  if (jsonMode) {
196
223
  print(JSON.stringify(allPacks));
197
224
  break;
198
225
  }
199
226
  if (allPacks.length === 0) {
200
- print('No packs available.');
227
+ print("No packs available.");
201
228
  } else {
202
- print('Available packs:\n');
229
+ print("Available packs:\n");
203
230
  for (const p of allPacks) {
204
- print(` ${p.id} ${p.name} (${p.claimCount} claims, v${p.version}, ${p.source})`);
231
+ print(
232
+ ` ${p.id} ${p.name} (${p.claimCount} claims, v${p.version}, ${p.source})`,
233
+ );
205
234
  if (p.description) print(` ${p.description.slice(0, 100)}`);
206
- print('');
235
+ print("");
207
236
  }
208
237
  }
209
238
  break;
210
239
  }
211
240
 
212
- case 'templates': {
241
+ case "templates": {
213
242
  const allTemplates = templates.list();
214
243
  if (jsonMode) {
215
244
  print(JSON.stringify(allTemplates));
216
245
  break;
217
246
  }
218
247
  if (allTemplates.length === 0) {
219
- print('No templates saved yet. Use the Templates API to create them.');
248
+ print("No templates saved yet. Use the Templates API to create them.");
220
249
  } else {
221
250
  for (const t of allTemplates) {
222
- print(` ${t.id} "${t.question || t.name}" (${t.seedClaims} seed claims) [${t.tags.join(', ')}]`);
251
+ print(
252
+ ` ${t.id} "${t.question || t.name}" (${t.seedClaims} seed claims) [${t.tags.join(", ")}]`,
253
+ );
223
254
  }
224
255
  }
225
256
  break;
226
257
  }
227
258
 
228
- case 'install': {
259
+ case "install": {
229
260
  const filePath = args[1];
230
261
  if (!filePath) {
231
- print('Usage: silo install <pack-file.json>');
262
+ print("Usage: silo install <pack-file.json>");
232
263
  process.exit(1);
233
264
  }
234
265
  const result = packs.install(filePath);
@@ -236,41 +267,45 @@ try {
236
267
  break;
237
268
  }
238
269
 
239
- case 'analyze': {
240
- const { analyzeLibrary } = require('../lib/analytics.js');
270
+ case "analyze": {
271
+ const { analyzeLibrary } = require("../lib/analytics.js");
241
272
  const result = analyzeLibrary(store);
242
273
  if (jsonMode) {
243
274
  print(JSON.stringify(result));
244
275
  break;
245
276
  }
246
277
  if (!result.available) {
247
- print(`silo analyze: harvest not found.\n\nThe analyze command requires @grainulation/harvest in a sibling directory.\nExpected locations:\n ../harvest/lib/analyzer.js\n\nInstall harvest alongside silo and try again.`);
278
+ print(
279
+ `silo analyze: harvest not found.\n\nThe analyze command requires @grainulation/harvest in a sibling directory.\nExpected locations:\n ../harvest/lib/analyzer.js\n\nInstall harvest alongside silo and try again.`,
280
+ );
248
281
  process.exit(1);
249
282
  }
250
283
  if (!result.analysis) {
251
- print(`silo analyze: ${result.reason || 'no data to analyze'}`);
284
+ print(`silo analyze: ${result.reason || "no data to analyze"}`);
252
285
  break;
253
286
  }
254
- print(`Cross-library analytics (${result.collectionCount} collection(s)):\n`);
287
+ print(
288
+ `Cross-library analytics (${result.collectionCount} collection(s)):\n`,
289
+ );
255
290
  const analysis = result.analysis;
256
291
  if (analysis.typeDistribution) {
257
- print('Type distribution:');
292
+ print("Type distribution:");
258
293
  for (const [type, count] of Object.entries(analysis.typeDistribution)) {
259
294
  print(` ${type}: ${count}`);
260
295
  }
261
- print('');
296
+ print("");
262
297
  }
263
298
  if (analysis.evidenceQuality) {
264
- print('Evidence quality:');
299
+ print("Evidence quality:");
265
300
  for (const [tier, count] of Object.entries(analysis.evidenceQuality)) {
266
301
  print(` ${tier}: ${count}`);
267
302
  }
268
- print('');
303
+ print("");
269
304
  }
270
305
  // Print any other top-level keys from the analysis
271
306
  for (const [key, value] of Object.entries(analysis)) {
272
- if (key === 'typeDistribution' || key === 'evidenceQuality') continue;
273
- if (typeof value === 'object') {
307
+ if (key === "typeDistribution" || key === "evidenceQuality") continue;
308
+ if (typeof value === "object") {
274
309
  print(`${key}: ${JSON.stringify(value, null, 2)}`);
275
310
  } else {
276
311
  print(`${key}: ${value}`);
@@ -279,39 +314,130 @@ try {
279
314
  break;
280
315
  }
281
316
 
282
- case 'serve-mcp': {
283
- const serveMcp = require('../lib/serve-mcp.js');
317
+ case "graph": {
318
+ const graph = new Graph(store);
319
+ const action = args[1] || "stats";
320
+ const stats = graph.build();
321
+
322
+ if (action === "stats") {
323
+ if (jsonMode) {
324
+ print(JSON.stringify(stats));
325
+ break;
326
+ }
327
+ print(
328
+ `Knowledge graph: ${stats.nodes} nodes, ${stats.edges} edges, ${stats.sources} sources, ${stats.topics} topics, ${stats.tags} tags`,
329
+ );
330
+ } else if (action === "related") {
331
+ const claimId = args[2];
332
+ if (!claimId) {
333
+ print("Usage: silo graph related <claimId>");
334
+ process.exit(1);
335
+ }
336
+ const results = graph.related(claimId, { limit: 20 });
337
+ if (jsonMode) {
338
+ print(JSON.stringify(results));
339
+ break;
340
+ }
341
+ if (results.length === 0) {
342
+ print("No related claims found.");
343
+ break;
344
+ }
345
+ print(`${results.length} related claim(s):\n`);
346
+ for (const r of results) {
347
+ print(
348
+ ` [${r.claim.id}] (${r.relation}, w=${r.weight}) ${(r.claim.content || "").slice(0, 100)}`,
349
+ );
350
+ print(` from: ${r.source}\n`);
351
+ }
352
+ } else if (action === "topic") {
353
+ const topic = args
354
+ .slice(2)
355
+ .filter((a) => !a.startsWith("--"))
356
+ .join(" ");
357
+ if (!topic) {
358
+ print("Usage: silo graph topic <topic>");
359
+ process.exit(1);
360
+ }
361
+ const results = graph.byTopic(topic);
362
+ if (jsonMode) {
363
+ print(JSON.stringify(results));
364
+ break;
365
+ }
366
+ if (results.length === 0) {
367
+ print("No claims found for this topic.");
368
+ break;
369
+ }
370
+ print(`${results.length} claim(s) for topic "${topic}":\n`);
371
+ for (const r of results) {
372
+ print(
373
+ ` [${r.claim.id}] (${r.claim.type}) ${(r.claim.content || "").slice(0, 100)}`,
374
+ );
375
+ print(` from: ${r.source}\n`);
376
+ }
377
+ } else if (action === "clusters") {
378
+ const clusters = graph.clusters(parseInt(flag("min-size")) || 3);
379
+ if (jsonMode) {
380
+ print(JSON.stringify(clusters));
381
+ break;
382
+ }
383
+ if (clusters.length === 0) {
384
+ print("No clusters found.");
385
+ break;
386
+ }
387
+ print(`${clusters.length} cluster(s):\n`);
388
+ for (const c of clusters.slice(0, 20)) {
389
+ print(
390
+ ` "${c.topic}" — ${c.claimCount} claims, ${c.edgeCount} edges`,
391
+ );
392
+ }
393
+ } else if (action === "export") {
394
+ print(JSON.stringify(graph.toJSON(), null, 2));
395
+ } else {
396
+ print(
397
+ `Unknown graph action: ${action}. Use: stats, related, topic, clusters, export`,
398
+ );
399
+ process.exit(1);
400
+ }
401
+ break;
402
+ }
403
+
404
+ case "serve-mcp": {
405
+ const serveMcp = require("../lib/serve-mcp.js");
284
406
  serveMcp.run(process.cwd());
285
407
  break;
286
408
  }
287
409
 
288
- case 'serve': {
410
+ case "serve": {
289
411
  // Dynamic import for ESM server module -- use fork() for proper stdio
290
- const port = flag('port') || '9095';
291
- const root = flag('root') || process.cwd();
412
+ const port = flag("port") || "9095";
413
+ const root = flag("root") || process.cwd();
292
414
  const serverArgs = [];
293
- if (flag('port')) { serverArgs.push('--port', port); }
294
- if (flag('root')) { serverArgs.push('--root', root); }
295
- const { fork } = require('node:child_process');
296
- const path = require('node:path');
297
- const serverPath = path.join(__dirname, '..', 'lib', 'server.js');
415
+ if (flag("port")) {
416
+ serverArgs.push("--port", port);
417
+ }
418
+ if (flag("root")) {
419
+ serverArgs.push("--root", root);
420
+ }
421
+ const { fork } = require("node:child_process");
422
+ const path = require("node:path");
423
+ const serverPath = path.join(__dirname, "..", "lib", "server.js");
298
424
  const child = fork(serverPath, serverArgs, {
299
- stdio: 'inherit',
425
+ stdio: "inherit",
300
426
  env: process.env,
301
427
  });
302
- child.on('error', (err) => {
428
+ child.on("error", (err) => {
303
429
  process.stderr.write(`silo: error starting server: ${err.message}\n`);
304
430
  process.exit(1);
305
431
  });
306
- child.on('exit', (code) => process.exit(code || 0));
307
- process.on('SIGTERM', () => child.kill('SIGTERM'));
308
- process.on('SIGINT', () => child.kill('SIGINT'));
432
+ child.on("exit", (code) => process.exit(code || 0));
433
+ process.on("SIGTERM", () => child.kill("SIGTERM"));
434
+ process.on("SIGINT", () => child.kill("SIGINT"));
309
435
  break;
310
436
  }
311
437
 
312
- case 'help':
313
- case '--help':
314
- case '-h':
438
+ case "help":
439
+ case "--help":
440
+ case "-h":
315
441
  case undefined:
316
442
  usage();
317
443
  break;
package/lib/analytics.js CHANGED
@@ -1,4 +1,4 @@
1
- 'use strict';
1
+ "use strict";
2
2
 
3
3
  /**
4
4
  * silo -> harvest edge: cross-library analytics.
@@ -9,12 +9,12 @@
9
9
  * is not available.
10
10
  */
11
11
 
12
- const fs = require('node:fs');
13
- const path = require('node:path');
12
+ const fs = require("node:fs");
13
+ const path = require("node:path");
14
14
 
15
15
  const HARVEST_SIBLINGS = [
16
- path.join(__dirname, '..', '..', 'harvest'),
17
- path.join(__dirname, '..', '..', '..', 'harvest'),
16
+ path.join(__dirname, "..", "..", "harvest"),
17
+ path.join(__dirname, "..", "..", "..", "harvest"),
18
18
  ];
19
19
 
20
20
  /**
@@ -23,12 +23,14 @@ const HARVEST_SIBLINGS = [
23
23
  */
24
24
  function loadHarvestAnalyzer() {
25
25
  for (const dir of HARVEST_SIBLINGS) {
26
- const analyzerPath = path.join(dir, 'lib', 'analyzer.js');
26
+ const analyzerPath = path.join(dir, "lib", "analyzer.js");
27
27
  if (fs.existsSync(analyzerPath)) {
28
28
  try {
29
29
  const mod = require(analyzerPath);
30
- if (typeof mod.analyze === 'function') return mod.analyze;
31
- } catch { continue; }
30
+ if (typeof mod.analyze === "function") return mod.analyze;
31
+ } catch {
32
+ continue;
33
+ }
32
34
  }
33
35
  }
34
36
  return null;
@@ -42,12 +44,20 @@ function loadHarvestAnalyzer() {
42
44
  function analyzeLibrary(store) {
43
45
  const analyze = loadHarvestAnalyzer();
44
46
  if (!analyze) {
45
- return { available: false, reason: 'harvest analyzer not found in sibling directories' };
47
+ return {
48
+ available: false,
49
+ reason: "harvest analyzer not found in sibling directories",
50
+ };
46
51
  }
47
52
 
48
53
  const collections = store.list();
49
54
  if (collections.length === 0) {
50
- return { available: true, analysis: null, collectionCount: 0, reason: 'no collections stored' };
55
+ return {
56
+ available: true,
57
+ analysis: null,
58
+ collectionCount: 0,
59
+ reason: "no collections stored",
60
+ };
51
61
  }
52
62
 
53
63
  // Convert silo collections into the sprint format harvest expects
@@ -62,7 +72,12 @@ function analyzeLibrary(store) {
62
72
  }
63
73
 
64
74
  if (sprints.length === 0) {
65
- return { available: true, analysis: null, collectionCount: 0, reason: 'no valid claim data' };
75
+ return {
76
+ available: true,
77
+ analysis: null,
78
+ collectionCount: 0,
79
+ reason: "no valid claim data",
80
+ };
66
81
  }
67
82
 
68
83
  const analysis = analyze(sprints);