| … | … | @@ -104,6 +104,14 @@ Metadata (Device Pairing): |
| 104 | 104 | meta invite Get invite key for device pairing |
| 105 | 105 | meta join <key> Pair with remote device's MetadataBase |
| 106 | 106 | |
| 107 | Graph (MI Clustering): |
| 108 | graph clusters Show current clusters (default algo) |
| 109 | graph edges Show MI edges between rooms |
| 110 | graph compute Compute with default algo (members-only) |
| 111 | graph compute --algo=<f> Compute with algo file (JSON config) |
| 112 | graph compute --features=topics --threshold=0.3 |
| 113 | Compute with inline params |
| 114 | |
| 107 | 115 | System: |
| 108 | 116 | status Daemon status (uptime, rooms, identity) |
| 109 | 117 | version Keet version info |
| … | … | @@ -285,6 +293,9 @@ try { |
| 285 | 293 | case 'meta': |
| 286 | 294 | await clientMeta(args.slice(1)) |
| 287 | 295 | break |
| 296 | case 'graph': |
| 297 | await clientGraph(args.slice(1)) |
| 298 | break |
| 288 | 299 | default: |
| 289 | 300 | console.error(`Unknown command: ${command}`) |
| 290 | 301 | printHelp() |
| … | … | @@ -794,4 +805,163 @@ async function clientMeta (subArgs) { |
| 794 | 805 | console.error('Usage: meta [key|invite|join <key>]') |
| 795 | 806 | } |
| 796 | 807 | |
| 808 | async function clientGraph (subArgs) { |
| 809 | // Parse --algo=<path>, --features=X, --threshold=N, --minSize=N from subArgs |
| 810 | let sub = null |
| 811 | let algoPath = null |
| 812 | let inlineFeatures = null |
| 813 | let inlineThreshold = null |
| 814 | let inlineMinSize = null |
| 815 | let force = false |
| 816 | |
| 817 | const positional = [] |
| 818 | for (const arg of subArgs) { |
| 819 | if (arg.startsWith('--algo=')) { |
| 820 | algoPath = arg.slice(7) |
| 821 | } else if (arg.startsWith('--features=')) { |
| 822 | inlineFeatures = arg.slice(11) |
| 823 | } else if (arg.startsWith('--threshold=')) { |
| 824 | inlineThreshold = parseFloat(arg.slice(12)) |
| 825 | } else if (arg.startsWith('--minSize=')) { |
| 826 | inlineMinSize = parseInt(arg.slice(10)) |
| 827 | } else if (arg === '--force') { |
| 828 | force = true |
| 829 | } else { |
| 830 | positional.push(arg) |
| 831 | } |
| 832 | } |
| 833 | |
| 834 | sub = positional[0] || 'clusters' |
| 835 | |
| 836 | if (sub === 'compute') { |
| 837 | const params = { force } |
| 838 | |
| 839 | if (algoPath) { |
| 840 | // Load algo file |
| 841 | try { |
| 842 | const fs = await import('fs') |
| 843 | const raw = fs.readFileSync(algoPath, 'utf8') |
| 844 | params.algo = JSON.parse(raw) |
| 845 | } catch (err) { |
| 846 | console.error('[keet-cli] Failed to load algo file:', algoPath, '-', err.message) |
| 847 | return |
| 848 | } |
| 849 | } else if (inlineFeatures) { |
| 850 | // Build inline algo from flags |
| 851 | const features = inlineFeatures.split(',').map(t => ({ type: t.trim(), weight: 1.0 })) |
| 852 | params.algo = { |
| 853 | name: 'inline-' + inlineFeatures, |
| 854 | features, |
| 855 | threshold: inlineThreshold ?? 0.5, |
| 856 | minSize: inlineMinSize ?? 2 |
| 857 | } |
| 858 | } |
| 859 | |
| 860 | const result = await rpc('graph.compute', params) |
| 861 | if (!result.ok) { |
| 862 | console.error('[keet-cli] Error:', result.error) |
| 863 | return |
| 864 | } |
| 865 | printGraphResult(result.data) |
| 866 | return |
| 867 | } |
| 868 | |
| 869 | if (sub === 'clusters') { |
| 870 | const params = {} |
| 871 | if (algoPath) { |
| 872 | try { |
| 873 | const fs = await import('fs') |
| 874 | params.algo = JSON.parse(fs.readFileSync(algoPath, 'utf8')) |
| 875 | } catch (err) { |
| 876 | console.error('[keet-cli] Failed to load algo file:', err.message) |
| 877 | return |
| 878 | } |
| 879 | } |
| 880 | const result = await rpc('graph.clusters', params) |
| 881 | if (!result.ok) { |
| 882 | console.error('[keet-cli] Error:', result.error) |
| 883 | return |
| 884 | } |
| 885 | const clusters = Array.isArray(result.data) ? result.data : [] |
| 886 | if (clusters.length === 0) { |
| 887 | console.log('[keet-cli] No clusters found.') |
| 888 | return |
| 889 | } |
| 890 | console.log(`[keet-cli] ${clusters.length} cluster(s):`) |
| 891 | for (const c of clusters) { |
| 892 | const label = c.label ? ` [${c.label}]` : '' |
| 893 | const topics = c.topics?.length ? ` topics: ${c.topics.join(', ')}` : '' |
| 894 | console.log(` Cluster ${c.id}${label} — ${c.rooms.length} rooms${topics}`) |
| 895 | for (const r of c.rooms) { |
| 896 | console.log(` ${r}`) |
| 897 | } |
| 898 | } |
| 899 | return |
| 900 | } |
| 901 | |
| 902 | if (sub === 'edges') { |
| 903 | const params = {} |
| 904 | if (algoPath) { |
| 905 | try { |
| 906 | const fs = await import('fs') |
| 907 | params.algo = JSON.parse(fs.readFileSync(algoPath, 'utf8')) |
| 908 | } catch (err) { |
| 909 | console.error('[keet-cli] Failed to load algo file:', err.message) |
| 910 | return |
| 911 | } |
| 912 | } |
| 913 | const result = await rpc('graph.edges', params) |
| 914 | if (!result.ok) { |
| 915 | console.error('[keet-cli] Error:', result.error) |
| 916 | return |
| 917 | } |
| 918 | const edges = Array.isArray(result.data) ? result.data : [] |
| 919 | if (edges.length === 0) { |
| 920 | console.log('[keet-cli] No MI edges.') |
| 921 | return |
| 922 | } |
| 923 | console.log(`[keet-cli] ${edges.length} edge(s):`) |
| 924 | for (const e of edges) { |
| 925 | console.log(` ${e.from.slice(0, 8)}..${e.to.slice(0, 8)} MI: ${e.mi} shared: ${e.shared}`) |
| 926 | } |
| 927 | return |
| 928 | } |
| 929 | |
| 930 | console.error('Usage: graph [clusters|edges|compute] [--algo=<file>] [--features=X] [--threshold=N]') |
| 931 | } |
| 932 | |
| 933 | function printGraphResult (data) { |
| 934 | const algo = data.algo || 'default' |
| 935 | console.log(`[keet-cli] Graph computed (algo: ${algo})`) |
| 936 | |
| 937 | const clusters = data.clusters || [] |
| 938 | const edges = data.edges || [] |
| 939 | const suggestions = data.suggestions || [] |
| 940 | |
| 941 | console.log(` ${clusters.length} cluster(s), ${edges.length} edge(s), ${suggestions.length} suggestion(s)`) |
| 942 | |
| 943 | if (clusters.length > 0) { |
| 944 | console.log('') |
| 945 | console.log(' Clusters:') |
| 946 | for (const c of clusters) { |
| 947 | const label = c.label ? ` [${c.label}]` : '' |
| 948 | const topics = c.topics?.length ? ` topics: ${c.topics.join(', ')}` : '' |
| 949 | console.log(` Cluster ${c.id}${label} — ${c.rooms.length} rooms${topics}`) |
| 950 | for (const r of c.rooms) { |
| 951 | console.log(` ${r}`) |
| 952 | } |
| 953 | } |
| 954 | } |
| 955 | |
| 956 | if (edges.length > 0) { |
| 957 | console.log('') |
| 958 | console.log(' Top edges:') |
| 959 | const top = edges.slice(0, 10) |
| 960 | for (const e of top) { |
| 961 | console.log(` ${e.from.slice(0, 8)}..${e.to.slice(0, 8)} MI: ${e.mi} shared: ${e.shared}`) |
| 962 | } |
| 963 | if (edges.length > 10) console.log(` ... and ${edges.length - 10} more`) |
| 964 | } |
| 965 | } |
| 966 | |
| 797 | 967 | // (UDS client helpers moved above client switch to avoid TDZ) |