trellis 2.0.8 → 2.0.13

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 (42) hide show
  1. package/README.md +279 -116
  2. package/dist/cli/index.js +655 -4
  3. package/dist/core/index.js +471 -2
  4. package/dist/embeddings/index.js +5 -1
  5. package/dist/{index-s603ev6w.js → index-5b01h414.js} +1 -1
  6. package/dist/index-5m0g9r0y.js +1100 -0
  7. package/dist/{index-zf6htvnm.js → index-7gvjxt27.js} +166 -2
  8. package/dist/index-hybgxe40.js +1174 -0
  9. package/dist/index.js +7 -2
  10. package/dist/transformers.node-bx3q9d7k.js +33130 -0
  11. package/package.json +9 -4
  12. package/src/cli/index.ts +939 -0
  13. package/src/core/agents/harness.ts +380 -0
  14. package/src/core/agents/index.ts +18 -0
  15. package/src/core/agents/types.ts +90 -0
  16. package/src/core/index.ts +85 -2
  17. package/src/core/kernel/trellis-kernel.ts +593 -0
  18. package/src/core/ontology/builtins.ts +248 -0
  19. package/src/core/ontology/index.ts +34 -0
  20. package/src/core/ontology/registry.ts +209 -0
  21. package/src/core/ontology/types.ts +124 -0
  22. package/src/core/ontology/validator.ts +382 -0
  23. package/src/core/persist/backend.ts +10 -0
  24. package/src/core/persist/sqlite-backend.ts +298 -0
  25. package/src/core/plugins/index.ts +17 -0
  26. package/src/core/plugins/registry.ts +322 -0
  27. package/src/core/plugins/types.ts +126 -0
  28. package/src/core/query/datalog.ts +188 -0
  29. package/src/core/query/engine.ts +370 -0
  30. package/src/core/query/index.ts +34 -0
  31. package/src/core/query/parser.ts +481 -0
  32. package/src/core/query/types.ts +200 -0
  33. package/src/embeddings/auto-embed.ts +248 -0
  34. package/src/embeddings/index.ts +7 -0
  35. package/src/embeddings/model.ts +21 -4
  36. package/src/embeddings/types.ts +8 -1
  37. package/src/index.ts +9 -0
  38. package/src/sync/http-transport.ts +144 -0
  39. package/src/sync/index.ts +11 -0
  40. package/src/sync/multi-repo.ts +200 -0
  41. package/src/sync/ws-transport.ts +145 -0
  42. package/dist/index-5bhe57y9.js +0 -326
package/src/cli/index.ts CHANGED
@@ -16,6 +16,18 @@ import { Command } from 'commander';
16
16
  import chalk from 'chalk';
17
17
  import { resolve, join } from 'path';
18
18
  import { TrellisVcsEngine } from '../engine.js';
19
+ import { TrellisKernel } from '../core/kernel/trellis-kernel.js';
20
+ import { SqliteKernelBackend } from '../core/persist/sqlite-backend.js';
21
+ import { QueryEngine, parseQuery, parseSimple } from '../core/query/index.js';
22
+ import {
23
+ OntologyRegistry,
24
+ validateStore,
25
+ builtinOntologies,
26
+ } from '../core/ontology/index.js';
27
+ import { buildRAGContext } from '../embeddings/auto-embed.js';
28
+ import { VectorStore } from '../embeddings/store.js';
29
+ import { embed } from '../embeddings/model.js';
30
+ import { EmbeddingManager } from '../embeddings/search.js';
19
31
  import { importFromGit } from '../git/git-importer.js';
20
32
  import { exportToGit } from '../git/git-exporter.js';
21
33
  import {
@@ -2381,6 +2393,933 @@ program
2381
2393
  }
2382
2394
  });
2383
2395
 
2396
+ // ---------------------------------------------------------------------------
2397
+ // Kernel helper — boots a TrellisKernel from a .trellis directory
2398
+ // ---------------------------------------------------------------------------
2399
+
2400
+ function bootKernel(rootPath: string): TrellisKernel {
2401
+ const dbPath = join(rootPath, '.trellis', 'kernel.db');
2402
+ const backend = new SqliteKernelBackend(dbPath);
2403
+ const kernel = new TrellisKernel({
2404
+ backend,
2405
+ agentId: `agent:${process.env.USER ?? 'unknown'}`,
2406
+ });
2407
+ kernel.boot();
2408
+ return kernel;
2409
+ }
2410
+
2411
+ // ---------------------------------------------------------------------------
2412
+ // trellis entity
2413
+ // ---------------------------------------------------------------------------
2414
+
2415
+ const entityCmd = program
2416
+ .command('entity')
2417
+ .description('Manage graph entities (generic CRUD)');
2418
+
2419
+ entityCmd
2420
+ .command('create')
2421
+ .description('Create a new entity in the graph')
2422
+ .requiredOption('-i, --id <id>', 'Entity ID (e.g. "project:my-app")')
2423
+ .requiredOption('-t, --type <type>', 'Entity type (e.g. "Project", "User")')
2424
+ .option('-a, --attr <attrs...>', 'Attributes as key=value pairs')
2425
+ .option('-p, --path <path>', 'Repository path', '.')
2426
+ .action(async (opts: any) => {
2427
+ const rootPath = resolve(opts.path);
2428
+ requireRepo(rootPath);
2429
+
2430
+ const kernel = bootKernel(rootPath);
2431
+ try {
2432
+ const attrs: Record<string, any> = {};
2433
+ if (opts.attr) {
2434
+ for (const pair of opts.attr) {
2435
+ const eq = pair.indexOf('=');
2436
+ if (eq === -1) continue;
2437
+ const key = pair.slice(0, eq);
2438
+ let val: any = pair.slice(eq + 1);
2439
+ // Auto-coerce numbers and booleans
2440
+ if (val === 'true') val = true;
2441
+ else if (val === 'false') val = false;
2442
+ else if (!isNaN(Number(val)) && val !== '') val = Number(val);
2443
+ attrs[key] = val;
2444
+ }
2445
+ }
2446
+
2447
+ const result = await kernel.createEntity(opts.id, opts.type, attrs);
2448
+ console.log(chalk.green(`✓ Entity created: ${chalk.bold(opts.id)}`));
2449
+ console.log(` ${chalk.dim('Type:')} ${opts.type}`);
2450
+ console.log(` ${chalk.dim('Facts:')} ${result.factsDelta.added}`);
2451
+ console.log(` ${chalk.dim('Op:')} ${result.op.hash.slice(0, 32)}…`);
2452
+ } finally {
2453
+ kernel.close();
2454
+ }
2455
+ });
2456
+
2457
+ entityCmd
2458
+ .command('get')
2459
+ .description('Get an entity by ID')
2460
+ .argument('<id>', 'Entity ID')
2461
+ .option('-p, --path <path>', 'Repository path', '.')
2462
+ .option('--json', 'Output as JSON')
2463
+ .action((id: any, opts: any) => {
2464
+ const rootPath = resolve(opts.path);
2465
+ requireRepo(rootPath);
2466
+
2467
+ const kernel = bootKernel(rootPath);
2468
+ try {
2469
+ const entity = kernel.getEntity(id);
2470
+ if (!entity) {
2471
+ console.error(chalk.red(`Entity not found: ${id}`));
2472
+ process.exit(1);
2473
+ }
2474
+
2475
+ if (opts.json) {
2476
+ const obj: Record<string, any> = { id: entity.id, type: entity.type };
2477
+ for (const f of entity.facts) {
2478
+ if (f.a !== 'type') obj[f.a] = f.v;
2479
+ }
2480
+ obj._links = entity.links.map((l) => ({
2481
+ attribute: l.a,
2482
+ target: l.e2,
2483
+ source: l.e1,
2484
+ }));
2485
+ console.log(JSON.stringify(obj, null, 2));
2486
+ return;
2487
+ }
2488
+
2489
+ console.log(chalk.bold(`${entity.type}: ${entity.id}\n`));
2490
+ for (const f of entity.facts) {
2491
+ console.log(` ${chalk.dim(f.a.padEnd(20))} ${f.v}`);
2492
+ }
2493
+ if (entity.links.length > 0) {
2494
+ console.log(`\n ${chalk.bold('Links:')}`);
2495
+ for (const l of entity.links) {
2496
+ const dir = l.e1 === id ? '→' : '←';
2497
+ const other = l.e1 === id ? l.e2 : l.e1;
2498
+ console.log(` ${dir} ${chalk.dim(l.a)} ${other}`);
2499
+ }
2500
+ }
2501
+ } finally {
2502
+ kernel.close();
2503
+ }
2504
+ });
2505
+
2506
+ entityCmd
2507
+ .command('update')
2508
+ .description('Update attributes on an existing entity')
2509
+ .argument('<id>', 'Entity ID')
2510
+ .requiredOption('-a, --attr <attrs...>', 'Attributes as key=value pairs')
2511
+ .option('-p, --path <path>', 'Repository path', '.')
2512
+ .action(async (id: any, opts: any) => {
2513
+ const rootPath = resolve(opts.path);
2514
+ requireRepo(rootPath);
2515
+
2516
+ const kernel = bootKernel(rootPath);
2517
+ try {
2518
+ const updates: Record<string, any> = {};
2519
+ for (const pair of opts.attr) {
2520
+ const eq = pair.indexOf('=');
2521
+ if (eq === -1) continue;
2522
+ const key = pair.slice(0, eq);
2523
+ let val: any = pair.slice(eq + 1);
2524
+ if (val === 'true') val = true;
2525
+ else if (val === 'false') val = false;
2526
+ else if (!isNaN(Number(val)) && val !== '') val = Number(val);
2527
+ updates[key] = val;
2528
+ }
2529
+
2530
+ await kernel.updateEntity(id, updates);
2531
+ console.log(chalk.green(`✓ Updated ${chalk.bold(id)}`));
2532
+ for (const [k, v] of Object.entries(updates)) {
2533
+ console.log(` ${chalk.dim(k)} = ${v}`);
2534
+ }
2535
+ } finally {
2536
+ kernel.close();
2537
+ }
2538
+ });
2539
+
2540
+ entityCmd
2541
+ .command('delete')
2542
+ .description('Delete an entity and all its facts/links')
2543
+ .argument('<id>', 'Entity ID')
2544
+ .option('-p, --path <path>', 'Repository path', '.')
2545
+ .action(async (id: any, opts: any) => {
2546
+ const rootPath = resolve(opts.path);
2547
+ requireRepo(rootPath);
2548
+
2549
+ const kernel = bootKernel(rootPath);
2550
+ try {
2551
+ const entity = kernel.getEntity(id);
2552
+ if (!entity) {
2553
+ console.error(chalk.red(`Entity not found: ${id}`));
2554
+ process.exit(1);
2555
+ }
2556
+ await kernel.deleteEntity(id);
2557
+ console.log(chalk.green(`✓ Deleted entity ${chalk.bold(id)}`));
2558
+ } finally {
2559
+ kernel.close();
2560
+ }
2561
+ });
2562
+
2563
+ entityCmd
2564
+ .command('list')
2565
+ .description('List entities, optionally filtered by type')
2566
+ .option('-t, --type <type>', 'Filter by entity type')
2567
+ .option('-f, --filter <filters...>', 'Attribute filters as key=value')
2568
+ .option('--json', 'Output as JSON')
2569
+ .option('-p, --path <path>', 'Repository path', '.')
2570
+ .action((opts: any) => {
2571
+ const rootPath = resolve(opts.path);
2572
+ requireRepo(rootPath);
2573
+
2574
+ const kernel = bootKernel(rootPath);
2575
+ try {
2576
+ let filters: Record<string, any> | undefined;
2577
+ if (opts.filter) {
2578
+ filters = {};
2579
+ for (const pair of opts.filter) {
2580
+ const eq = pair.indexOf('=');
2581
+ if (eq === -1) continue;
2582
+ const key = pair.slice(0, eq);
2583
+ let val: any = pair.slice(eq + 1);
2584
+ if (val === 'true') val = true;
2585
+ else if (val === 'false') val = false;
2586
+ else if (!isNaN(Number(val)) && val !== '') val = Number(val);
2587
+ filters[key] = val;
2588
+ }
2589
+ }
2590
+
2591
+ const entities = kernel.listEntities(opts.type, filters);
2592
+
2593
+ if (opts.json) {
2594
+ const out = entities.map((e) => {
2595
+ const obj: Record<string, any> = { id: e.id, type: e.type };
2596
+ for (const f of e.facts) {
2597
+ if (f.a !== 'type') obj[f.a] = f.v;
2598
+ }
2599
+ return obj;
2600
+ });
2601
+ console.log(JSON.stringify(out, null, 2));
2602
+ return;
2603
+ }
2604
+
2605
+ if (entities.length === 0) {
2606
+ console.log(chalk.dim('No entities found.'));
2607
+ return;
2608
+ }
2609
+
2610
+ const typeLabel = opts.type ? ` (type: ${opts.type})` : '';
2611
+ console.log(chalk.bold(`Entities (${entities.length})${typeLabel}\n`));
2612
+ for (const e of entities) {
2613
+ const nameFact = e.facts.find((f) => f.a === 'name');
2614
+ const name = nameFact ? ` ${chalk.white(String(nameFact.v))}` : '';
2615
+ console.log(
2616
+ ` ${chalk.cyan(e.type.padEnd(16))} ${chalk.bold(e.id)}${name}`,
2617
+ );
2618
+ }
2619
+ } finally {
2620
+ kernel.close();
2621
+ }
2622
+ });
2623
+
2624
+ // ---------------------------------------------------------------------------
2625
+ // trellis fact
2626
+ // ---------------------------------------------------------------------------
2627
+
2628
+ const factCmd = program
2629
+ .command('fact')
2630
+ .description('Add or remove individual facts on entities');
2631
+
2632
+ factCmd
2633
+ .command('add')
2634
+ .description('Add a fact to an entity')
2635
+ .argument('<entity>', 'Entity ID')
2636
+ .argument('<attribute>', 'Attribute name')
2637
+ .argument('<value>', 'Value')
2638
+ .option('-p, --path <path>', 'Repository path', '.')
2639
+ .action(async (entity: any, attribute: any, value: any, opts: any) => {
2640
+ const rootPath = resolve(opts.path);
2641
+ requireRepo(rootPath);
2642
+
2643
+ const kernel = bootKernel(rootPath);
2644
+ try {
2645
+ // Auto-coerce
2646
+ let val: any = value;
2647
+ if (val === 'true') val = true;
2648
+ else if (val === 'false') val = false;
2649
+ else if (!isNaN(Number(val)) && val !== '') val = Number(val);
2650
+
2651
+ await kernel.addFact(entity, attribute, val);
2652
+ console.log(
2653
+ chalk.green(
2654
+ `✓ Added fact: ${chalk.bold(entity)}.${attribute} = ${val}`,
2655
+ ),
2656
+ );
2657
+ } finally {
2658
+ kernel.close();
2659
+ }
2660
+ });
2661
+
2662
+ factCmd
2663
+ .command('remove')
2664
+ .description('Remove a fact from an entity')
2665
+ .argument('<entity>', 'Entity ID')
2666
+ .argument('<attribute>', 'Attribute name')
2667
+ .argument('<value>', 'Value to remove')
2668
+ .option('-p, --path <path>', 'Repository path', '.')
2669
+ .action(async (entity: any, attribute: any, value: any, opts: any) => {
2670
+ const rootPath = resolve(opts.path);
2671
+ requireRepo(rootPath);
2672
+
2673
+ const kernel = bootKernel(rootPath);
2674
+ try {
2675
+ let val: any = value;
2676
+ if (val === 'true') val = true;
2677
+ else if (val === 'false') val = false;
2678
+ else if (!isNaN(Number(val)) && val !== '') val = Number(val);
2679
+
2680
+ await kernel.removeFact(entity, attribute, val);
2681
+ console.log(
2682
+ chalk.green(
2683
+ `✓ Removed fact: ${chalk.bold(entity)}.${attribute} = ${val}`,
2684
+ ),
2685
+ );
2686
+ } finally {
2687
+ kernel.close();
2688
+ }
2689
+ });
2690
+
2691
+ factCmd
2692
+ .command('query')
2693
+ .description('Query facts by entity or attribute')
2694
+ .option('-e, --entity <id>', 'Filter by entity ID')
2695
+ .option('-a, --attribute <attr>', 'Filter by attribute')
2696
+ .option('--json', 'Output as JSON')
2697
+ .option('-p, --path <path>', 'Repository path', '.')
2698
+ .action((opts: any) => {
2699
+ const rootPath = resolve(opts.path);
2700
+ requireRepo(rootPath);
2701
+
2702
+ const kernel = bootKernel(rootPath);
2703
+ try {
2704
+ const store = kernel.getStore();
2705
+ let facts;
2706
+
2707
+ if (opts.entity) {
2708
+ facts = store.getFactsByEntity(opts.entity);
2709
+ } else if (opts.attribute) {
2710
+ facts = store.getFactsByAttribute(opts.attribute);
2711
+ } else {
2712
+ facts = store.getAllFacts();
2713
+ }
2714
+
2715
+ if (opts.json) {
2716
+ console.log(JSON.stringify(facts, null, 2));
2717
+ return;
2718
+ }
2719
+
2720
+ if (facts.length === 0) {
2721
+ console.log(chalk.dim('No facts found.'));
2722
+ return;
2723
+ }
2724
+
2725
+ console.log(chalk.bold(`Facts (${facts.length})\n`));
2726
+ for (const f of facts.slice(0, 100)) {
2727
+ console.log(
2728
+ ` ${chalk.cyan(f.e.padEnd(24))} ${chalk.dim(f.a.padEnd(20))} ${f.v}`,
2729
+ );
2730
+ }
2731
+ if (facts.length > 100) {
2732
+ console.log(chalk.dim(` … +${facts.length - 100} more`));
2733
+ }
2734
+ } finally {
2735
+ kernel.close();
2736
+ }
2737
+ });
2738
+
2739
+ // ---------------------------------------------------------------------------
2740
+ // trellis link
2741
+ // ---------------------------------------------------------------------------
2742
+
2743
+ const linkCmd = program
2744
+ .command('link')
2745
+ .description('Add or remove links between entities');
2746
+
2747
+ linkCmd
2748
+ .command('add')
2749
+ .description('Add a link between two entities')
2750
+ .argument('<source>', 'Source entity ID')
2751
+ .argument('<attribute>', 'Relationship attribute')
2752
+ .argument('<target>', 'Target entity ID')
2753
+ .option('-p, --path <path>', 'Repository path', '.')
2754
+ .action(async (source: any, attribute: any, target: any, opts: any) => {
2755
+ const rootPath = resolve(opts.path);
2756
+ requireRepo(rootPath);
2757
+
2758
+ const kernel = bootKernel(rootPath);
2759
+ try {
2760
+ await kernel.addLink(source, attribute, target);
2761
+ console.log(
2762
+ chalk.green(
2763
+ `✓ Link: ${chalk.bold(source)} —[${attribute}]→ ${chalk.bold(target)}`,
2764
+ ),
2765
+ );
2766
+ } finally {
2767
+ kernel.close();
2768
+ }
2769
+ });
2770
+
2771
+ linkCmd
2772
+ .command('remove')
2773
+ .description('Remove a link between two entities')
2774
+ .argument('<source>', 'Source entity ID')
2775
+ .argument('<attribute>', 'Relationship attribute')
2776
+ .argument('<target>', 'Target entity ID')
2777
+ .option('-p, --path <path>', 'Repository path', '.')
2778
+ .action(async (source: any, attribute: any, target: any, opts: any) => {
2779
+ const rootPath = resolve(opts.path);
2780
+ requireRepo(rootPath);
2781
+
2782
+ const kernel = bootKernel(rootPath);
2783
+ try {
2784
+ await kernel.removeLink(source, attribute, target);
2785
+ console.log(
2786
+ chalk.green(
2787
+ `✓ Removed: ${chalk.bold(source)} —[${attribute}]→ ${chalk.bold(target)}`,
2788
+ ),
2789
+ );
2790
+ } finally {
2791
+ kernel.close();
2792
+ }
2793
+ });
2794
+
2795
+ linkCmd
2796
+ .command('query')
2797
+ .description('Query links for an entity')
2798
+ .option('-e, --entity <id>', 'Entity ID')
2799
+ .option('-a, --attribute <attr>', 'Relationship attribute')
2800
+ .option('--json', 'Output as JSON')
2801
+ .option('-p, --path <path>', 'Repository path', '.')
2802
+ .action((opts: any) => {
2803
+ const rootPath = resolve(opts.path);
2804
+ requireRepo(rootPath);
2805
+
2806
+ const kernel = bootKernel(rootPath);
2807
+ try {
2808
+ const store = kernel.getStore();
2809
+ let links;
2810
+
2811
+ if (opts.entity && opts.attribute) {
2812
+ links = store.getLinksByEntityAndAttribute(opts.entity, opts.attribute);
2813
+ } else if (opts.entity) {
2814
+ links = store.getLinksByEntity(opts.entity);
2815
+ } else if (opts.attribute) {
2816
+ links = store.getLinksByAttribute(opts.attribute);
2817
+ } else {
2818
+ links = store.getAllLinks();
2819
+ }
2820
+
2821
+ if (opts.json) {
2822
+ console.log(JSON.stringify(links, null, 2));
2823
+ return;
2824
+ }
2825
+
2826
+ if (links.length === 0) {
2827
+ console.log(chalk.dim('No links found.'));
2828
+ return;
2829
+ }
2830
+
2831
+ console.log(chalk.bold(`Links (${links.length})\n`));
2832
+ for (const l of links.slice(0, 100)) {
2833
+ console.log(
2834
+ ` ${chalk.cyan(l.e1)} —[${chalk.dim(l.a)}]→ ${chalk.cyan(l.e2)}`,
2835
+ );
2836
+ }
2837
+ if (links.length > 100) {
2838
+ console.log(chalk.dim(` … +${links.length - 100} more`));
2839
+ }
2840
+ } finally {
2841
+ kernel.close();
2842
+ }
2843
+ });
2844
+
2845
+ // ---------------------------------------------------------------------------
2846
+ // trellis query
2847
+ // ---------------------------------------------------------------------------
2848
+
2849
+ program
2850
+ .command('query')
2851
+ .description('Execute an EQL-S query against the graph')
2852
+ .argument(
2853
+ '<query>',
2854
+ 'EQL-S query string (or "find ?e where attr = value" shorthand)',
2855
+ )
2856
+ .option('-p, --path <path>', 'Repository path', '.')
2857
+ .option('--json', 'Output as JSON')
2858
+ .action((queryStr: string, opts: any) => {
2859
+ const rootPath = resolve(opts.path);
2860
+ requireRepo(rootPath);
2861
+
2862
+ const kernel = bootKernel(rootPath);
2863
+ try {
2864
+ const store = kernel.getStore();
2865
+ const engine = new QueryEngine(store);
2866
+
2867
+ let q;
2868
+ try {
2869
+ q = parseSimple(queryStr);
2870
+ } catch {
2871
+ try {
2872
+ q = parseQuery(queryStr);
2873
+ } catch (e: any) {
2874
+ console.error(chalk.red(`Parse error: ${e.message}`));
2875
+ process.exit(1);
2876
+ return;
2877
+ }
2878
+ }
2879
+
2880
+ const result = engine.execute(q!);
2881
+
2882
+ if (opts.json) {
2883
+ console.log(JSON.stringify(result.bindings, null, 2));
2884
+ } else {
2885
+ if (result.count === 0) {
2886
+ console.log(chalk.dim('No results.'));
2887
+ } else {
2888
+ // Determine columns
2889
+ const cols =
2890
+ result.bindings.length > 0 ? Object.keys(result.bindings[0]) : [];
2891
+
2892
+ // Print header
2893
+ console.log(chalk.bold(cols.map((c) => `?${c}`).join('\t')));
2894
+ console.log(chalk.dim('─'.repeat(cols.length * 20)));
2895
+
2896
+ // Print rows
2897
+ for (const row of result.bindings) {
2898
+ console.log(cols.map((c) => String(row[c] ?? '')).join('\t'));
2899
+ }
2900
+
2901
+ console.log(
2902
+ chalk.dim(
2903
+ `\n${result.count} result(s) in ${result.executionTime.toFixed(1)}ms`,
2904
+ ),
2905
+ );
2906
+ }
2907
+ }
2908
+ } finally {
2909
+ kernel.close();
2910
+ }
2911
+ });
2912
+
2913
+ // ---------------------------------------------------------------------------
2914
+ // trellis repl
2915
+ // ---------------------------------------------------------------------------
2916
+
2917
+ program
2918
+ .command('repl')
2919
+ .description('Interactive EQL-S query shell')
2920
+ .option('-p, --path <path>', 'Repository path', '.')
2921
+ .action(async (opts: any) => {
2922
+ const rootPath = resolve(opts.path);
2923
+ requireRepo(rootPath);
2924
+
2925
+ const kernel = bootKernel(rootPath);
2926
+ const store = kernel.getStore();
2927
+ const engine = new QueryEngine(store);
2928
+
2929
+ console.log(chalk.cyan.bold('Trellis EQL-S REPL'));
2930
+ console.log(
2931
+ chalk.dim(
2932
+ 'Type EQL-S queries or "find ?e where attr = value" shorthand.',
2933
+ ),
2934
+ );
2935
+ console.log(chalk.dim('Type .exit to quit, .help for help.\n'));
2936
+
2937
+ const readline = await import('readline');
2938
+ const rl = readline.createInterface({
2939
+ input: process.stdin,
2940
+ output: process.stdout,
2941
+ prompt: chalk.green('eql> '),
2942
+ });
2943
+
2944
+ rl.prompt();
2945
+
2946
+ rl.on('line', (line: string) => {
2947
+ const trimmed = line.trim();
2948
+ if (!trimmed) {
2949
+ rl.prompt();
2950
+ return;
2951
+ }
2952
+
2953
+ if (trimmed === '.exit' || trimmed === '.quit') {
2954
+ kernel.close();
2955
+ rl.close();
2956
+ return;
2957
+ }
2958
+
2959
+ if (trimmed === '.help') {
2960
+ console.log(`
2961
+ ${chalk.bold('EQL-S Query Syntax:')}
2962
+ ${chalk.cyan('SELECT')} ?var1 ?var2 ${chalk.cyan('WHERE')} { patterns } [${chalk.cyan('FILTER')} ...] [${chalk.cyan('ORDER BY')} ...] [${chalk.cyan('LIMIT')} n]
2963
+
2964
+ ${chalk.bold('Pattern types:')}
2965
+ ${chalk.yellow('[?e "attr" "value"]')} Fact pattern (entity, attribute, value)
2966
+ ${chalk.yellow('(?src "rel" ?tgt)')} Link pattern (source, relationship, target)
2967
+ ${chalk.yellow('NOT [?e "attr" ?v]')} Negation
2968
+ ${chalk.yellow('OR { ... } { ... }')} Disjunction
2969
+
2970
+ ${chalk.bold('Shorthand:')}
2971
+ ${chalk.yellow('find ?e where type = "Project"')}
2972
+
2973
+ ${chalk.bold('Commands:')}
2974
+ .exit / .quit Exit the REPL
2975
+ .stats Show store statistics
2976
+ .help Show this help
2977
+ `);
2978
+ rl.prompt();
2979
+ return;
2980
+ }
2981
+
2982
+ if (trimmed === '.stats') {
2983
+ const facts = store.getAllFacts();
2984
+ const links = store.getAllLinks();
2985
+ const types = new Set(
2986
+ facts.filter((f) => f.a === 'type').map((f) => f.v),
2987
+ );
2988
+ console.log(` Facts: ${facts.length}`);
2989
+ console.log(` Links: ${links.length}`);
2990
+ console.log(` Entity types: ${[...types].join(', ') || '(none)'}`);
2991
+ rl.prompt();
2992
+ return;
2993
+ }
2994
+
2995
+ try {
2996
+ let q;
2997
+ try {
2998
+ q = parseSimple(trimmed);
2999
+ } catch {
3000
+ q = parseQuery(trimmed);
3001
+ }
3002
+
3003
+ const result = engine.execute(q);
3004
+
3005
+ if (result.count === 0) {
3006
+ console.log(chalk.dim('No results.'));
3007
+ } else {
3008
+ const cols = Object.keys(result.bindings[0]);
3009
+ console.log(chalk.bold(cols.map((c) => `?${c}`).join('\t')));
3010
+ for (const row of result.bindings) {
3011
+ console.log(cols.map((c) => String(row[c] ?? '')).join('\t'));
3012
+ }
3013
+ console.log(
3014
+ chalk.dim(
3015
+ `${result.count} result(s) in ${result.executionTime.toFixed(1)}ms`,
3016
+ ),
3017
+ );
3018
+ }
3019
+ } catch (e: any) {
3020
+ console.error(chalk.red(`Error: ${e.message}`));
3021
+ }
3022
+
3023
+ rl.prompt();
3024
+ });
3025
+
3026
+ rl.on('close', () => {
3027
+ kernel.close();
3028
+ console.log(chalk.dim('Goodbye.'));
3029
+ });
3030
+ });
3031
+
3032
+ // ---------------------------------------------------------------------------
3033
+ // trellis ontology
3034
+ // ---------------------------------------------------------------------------
3035
+
3036
+ const ontologyCmd = program
3037
+ .command('ontology')
3038
+ .description('Manage and inspect ontology schemas');
3039
+
3040
+ ontologyCmd
3041
+ .command('list')
3042
+ .description('List all registered ontologies (built-in + custom)')
3043
+ .action(() => {
3044
+ const registry = new OntologyRegistry();
3045
+ for (const o of builtinOntologies) registry.register(o);
3046
+
3047
+ const schemas = registry.list();
3048
+ if (schemas.length === 0) {
3049
+ console.log(chalk.dim('No ontologies registered.'));
3050
+ return;
3051
+ }
3052
+
3053
+ console.log(chalk.bold(`Ontologies (${schemas.length})\n`));
3054
+ for (const s of schemas) {
3055
+ console.log(` ${chalk.cyan(s.id)} ${chalk.dim(`v${s.version}`)}`);
3056
+ console.log(
3057
+ ` ${s.name}${s.description ? ` — ${chalk.dim(s.description)}` : ''}`,
3058
+ );
3059
+ console.log(` Entities: ${s.entities.map((e) => e.name).join(', ')}`);
3060
+ console.log(
3061
+ ` Relations: ${s.relations.map((r) => r.name).join(', ')}`,
3062
+ );
3063
+ console.log();
3064
+ }
3065
+ });
3066
+
3067
+ ontologyCmd
3068
+ .command('inspect')
3069
+ .description('Inspect a specific ontology or entity type')
3070
+ .argument(
3071
+ '<name>',
3072
+ 'Ontology ID (e.g. "trellis:project") or entity type name (e.g. "Project")',
3073
+ )
3074
+ .action((name: string) => {
3075
+ const registry = new OntologyRegistry();
3076
+ for (const o of builtinOntologies) registry.register(o);
3077
+
3078
+ // Try as ontology ID first
3079
+ const schema = registry.get(name);
3080
+ if (schema) {
3081
+ console.log(
3082
+ chalk.bold(
3083
+ `${schema.name} ${chalk.dim(`(${schema.id} v${schema.version})`)}`,
3084
+ ),
3085
+ );
3086
+ if (schema.description) console.log(chalk.dim(schema.description));
3087
+ console.log();
3088
+
3089
+ console.log(chalk.bold('Entity Types:'));
3090
+ for (const e of schema.entities) {
3091
+ console.log(
3092
+ `\n ${chalk.cyan.bold(e.name)}${e.abstract ? chalk.dim(' (abstract)') : ''}${e.extends ? chalk.dim(` extends ${e.extends}`) : ''}`,
3093
+ );
3094
+ if (e.description) console.log(` ${chalk.dim(e.description)}`);
3095
+ for (const a of e.attributes) {
3096
+ const flags = [
3097
+ a.required ? chalk.red('required') : null,
3098
+ a.enum ? `enum[${a.enum.join('|')}]` : null,
3099
+ a.default !== undefined ? `default=${a.default}` : null,
3100
+ ]
3101
+ .filter(Boolean)
3102
+ .join(', ');
3103
+ console.log(
3104
+ ` ${chalk.yellow(a.name)}: ${a.type}${flags ? ` (${flags})` : ''}`,
3105
+ );
3106
+ }
3107
+ }
3108
+
3109
+ if (schema.relations.length > 0) {
3110
+ console.log(chalk.bold('\nRelations:'));
3111
+ for (const r of schema.relations) {
3112
+ console.log(
3113
+ ` ${chalk.yellow(r.name)}: ${r.sourceTypes.join('|')} → ${r.targetTypes.join('|')}${r.cardinality ? ` [${r.cardinality}]` : ''}`,
3114
+ );
3115
+ }
3116
+ }
3117
+ return;
3118
+ }
3119
+
3120
+ // Try as entity type name
3121
+ const def = registry.getEntityDef(name);
3122
+ if (def) {
3123
+ const ontId = registry.getEntityOntology(name);
3124
+ console.log(chalk.bold(`${def.name}`) + chalk.dim(` (from ${ontId})`));
3125
+ if (def.description) console.log(chalk.dim(def.description));
3126
+ if (def.abstract)
3127
+ console.log(chalk.dim('(abstract — cannot be instantiated)'));
3128
+ if (def.extends) console.log(chalk.dim(`extends ${def.extends}`));
3129
+
3130
+ console.log(chalk.bold('\nAttributes:'));
3131
+ for (const a of def.attributes) {
3132
+ const flags = [
3133
+ a.required ? chalk.red('required') : null,
3134
+ a.enum ? `enum[${a.enum.join('|')}]` : null,
3135
+ a.default !== undefined ? `default=${a.default}` : null,
3136
+ ]
3137
+ .filter(Boolean)
3138
+ .join(', ');
3139
+ console.log(
3140
+ ` ${chalk.yellow(a.name)}: ${a.type}${flags ? ` (${flags})` : ''}`,
3141
+ );
3142
+ if (a.description) console.log(` ${chalk.dim(a.description)}`);
3143
+ }
3144
+
3145
+ const rels = registry.getRelationsForType(name);
3146
+ if (rels.length > 0) {
3147
+ console.log(chalk.bold('\nRelations:'));
3148
+ for (const r of rels) {
3149
+ const dir = r.sourceTypes.includes(name) ? '→' : '←';
3150
+ console.log(
3151
+ ` ${chalk.yellow(r.name)} ${dir} ${r.sourceTypes.includes(name) ? r.targetTypes.join('|') : r.sourceTypes.join('|')}`,
3152
+ );
3153
+ }
3154
+ }
3155
+ return;
3156
+ }
3157
+
3158
+ console.error(chalk.red(`Unknown ontology or entity type: "${name}"`));
3159
+ console.log(
3160
+ chalk.dim(
3161
+ 'Available ontologies: ' +
3162
+ registry
3163
+ .list()
3164
+ .map((s) => s.id)
3165
+ .join(', '),
3166
+ ),
3167
+ );
3168
+ console.log(
3169
+ chalk.dim('Available types: ' + registry.listEntityTypes().join(', ')),
3170
+ );
3171
+ });
3172
+
3173
+ ontologyCmd
3174
+ .command('validate')
3175
+ .description(
3176
+ 'Validate all entities in the graph against registered ontologies',
3177
+ )
3178
+ .option('-p, --path <path>', 'Repository path', '.')
3179
+ .option('--strict', 'Treat unknown types as errors')
3180
+ .action((opts: any) => {
3181
+ const rootPath = resolve(opts.path);
3182
+ requireRepo(rootPath);
3183
+
3184
+ const kernel = bootKernel(rootPath);
3185
+ try {
3186
+ const registry = new OntologyRegistry();
3187
+ for (const o of builtinOntologies) registry.register(o);
3188
+
3189
+ const store = kernel.getStore();
3190
+ const result = validateStore(store, registry);
3191
+
3192
+ if (result.errors.length > 0) {
3193
+ console.log(chalk.red.bold(`✗ ${result.errors.length} error(s):\n`));
3194
+ for (const err of result.errors) {
3195
+ console.log(
3196
+ ` ${chalk.red('ERROR')} ${chalk.bold(err.entityId)} (${err.entityType}) → ${err.field}: ${err.message}`,
3197
+ );
3198
+ }
3199
+ }
3200
+
3201
+ if (result.warnings.length > 0) {
3202
+ console.log(
3203
+ chalk.yellow.bold(`\n⚠ ${result.warnings.length} warning(s):\n`),
3204
+ );
3205
+ for (const w of result.warnings) {
3206
+ console.log(
3207
+ ` ${chalk.yellow('WARN')} ${chalk.bold(w.entityId)} (${w.entityType}) → ${w.field}: ${w.message}`,
3208
+ );
3209
+ }
3210
+ }
3211
+
3212
+ if (result.valid && result.warnings.length === 0) {
3213
+ console.log(chalk.green('✓ All entities pass ontology validation.'));
3214
+ } else if (result.valid) {
3215
+ console.log(chalk.green('\n✓ Valid (with warnings).'));
3216
+ } else {
3217
+ console.log(chalk.red('\n✗ Validation failed.'));
3218
+ }
3219
+ } finally {
3220
+ kernel.close();
3221
+ }
3222
+ });
3223
+
3224
+ // ---------------------------------------------------------------------------
3225
+ // trellis ask
3226
+ // ---------------------------------------------------------------------------
3227
+
3228
+ program
3229
+ .command('ask')
3230
+ .description('Natural language search over the graph (semantic search)')
3231
+ .argument('<question>', 'Natural language query')
3232
+ .option('-p, --path <path>', 'Repository path', '.')
3233
+ .option('-n, --limit <n>', 'Max results', '5')
3234
+ .option('--json', 'Output as JSON')
3235
+ .option('--rag', 'Output as RAG context (for LLM consumption)')
3236
+ .action(async (question: string, opts: any) => {
3237
+ const rootPath = resolve(opts.path);
3238
+ requireRepo(rootPath);
3239
+
3240
+ const dbPath = join(rootPath, '.trellis', 'embeddings.db');
3241
+ const vectorStore = new VectorStore(dbPath);
3242
+
3243
+ try {
3244
+ const limit = parseInt(opts.limit, 10) || 5;
3245
+
3246
+ if (opts.rag) {
3247
+ const ctx = await buildRAGContext(question, vectorStore, embed, {
3248
+ maxChunks: limit,
3249
+ });
3250
+
3251
+ if (opts.json) {
3252
+ console.log(JSON.stringify(ctx, null, 2));
3253
+ } else {
3254
+ console.log(chalk.bold.cyan('RAG Context'));
3255
+ console.log(chalk.dim(`Query: ${ctx.query}`));
3256
+ console.log(
3257
+ chalk.dim(
3258
+ `Chunks: ${ctx.chunks.length} | ~${ctx.estimatedTokens} tokens\n`,
3259
+ ),
3260
+ );
3261
+ for (const c of ctx.chunks) {
3262
+ console.log(
3263
+ chalk.yellow(
3264
+ `[${c.score.toFixed(3)}] ${c.entityId} (${c.chunkType})`,
3265
+ ),
3266
+ );
3267
+ console.log(c.content);
3268
+ console.log();
3269
+ }
3270
+ }
3271
+ } else {
3272
+ const queryVector = await embed(question);
3273
+ const results = vectorStore.search(queryVector, { limit });
3274
+
3275
+ if (opts.json) {
3276
+ console.log(
3277
+ JSON.stringify(
3278
+ results.map((r) => ({
3279
+ score: r.score,
3280
+ entityId: r.chunk.entityId,
3281
+ chunkType: r.chunk.chunkType,
3282
+ content: r.chunk.content,
3283
+ })),
3284
+ null,
3285
+ 2,
3286
+ ),
3287
+ );
3288
+ } else {
3289
+ if (results.length === 0) {
3290
+ console.log(
3291
+ chalk.dim(
3292
+ 'No results. Run `trellis reindex` first to build the embedding index.',
3293
+ ),
3294
+ );
3295
+ } else {
3296
+ console.log(chalk.bold(`Results for: "${question}"\n`));
3297
+ for (const r of results) {
3298
+ const score = chalk.dim(`[${r.score.toFixed(3)}]`);
3299
+ const entity = chalk.cyan(r.chunk.entityId);
3300
+ const type = chalk.dim(`(${r.chunk.chunkType})`);
3301
+ console.log(`${score} ${entity} ${type}`);
3302
+ const preview =
3303
+ r.chunk.content.length > 200
3304
+ ? r.chunk.content.slice(0, 200) + '…'
3305
+ : r.chunk.content;
3306
+ console.log(` ${preview}\n`);
3307
+ }
3308
+ }
3309
+ }
3310
+ }
3311
+ } catch (err: any) {
3312
+ if (err.message?.includes('No transformers')) {
3313
+ console.error(chalk.red('Embedding model not available.'));
3314
+ console.error(chalk.dim('Install: bun add @huggingface/transformers'));
3315
+ } else {
3316
+ console.error(chalk.red(`Error: ${err.message}`));
3317
+ }
3318
+ } finally {
3319
+ vectorStore.close();
3320
+ }
3321
+ });
3322
+
2384
3323
  // ---------------------------------------------------------------------------
2385
3324
  // Helpers
2386
3325
  // ---------------------------------------------------------------------------