@epicai/legion 1.0.1 → 1.0.3

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 CHANGED
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## 1.0.2 — 2026-04-02
11
+
12
+ ### Changed
13
+ - Setup wizard replaced credential onboarding with zero-credential live demo (Searchcode, PubMed, Robtex, Govbase) — developers see real data before touching any secrets
14
+ - `@xenova/transformers` moved to optional peer dependency — eliminates 50 MB install for users not running local embeddings
15
+ - PRIVACY.md added to published package
16
+
17
+ ### Fixed
18
+ - Removed broken category drill-down that showed "Other (3137)" for uncategorized adapters
19
+
20
+ ---
21
+
22
+ ## 1.0.1 — 2026-04-02
23
+
24
+ ### Changed
25
+ - Setup wizard replaced multi-level category drill-down with credential auto-wire — scans `~/.epic-ai/.env` and MCP client configs to pre-select adapters whose credentials are already present
26
+
27
+ ---
28
+
10
29
  ## 1.0.0 — 2026-04-02
11
30
 
12
31
  ### Added
@@ -1,12 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * Epic AI® Legion — CLI Entry Point
4
- * `npx @epicai/legion` — setup wizard
5
- * `npx @epicai/legion --serve` — MCP server mode
6
- * `npx @epicai/legion add <name>` — add adapter
7
- * `npx @epicai/legion remove <name>` — remove adapter
8
- * `npx @epicai/legion health` — check adapter health
9
- * `npx @epicai/legion list` list all adapters
4
+ * `legion` / `npx @epicai/legion` — setup wizard
5
+ * `legion serve` / `--serve` — MCP server mode
6
+ * `legion add <name>` — add adapter and enter credentials
7
+ * `legion remove <name>` — remove an adapter
8
+ * `legion health` — check adapter status
9
+ * `legion list` show Curated + Custom adapters
10
+ * `legion search [term]` — search all available adapters
11
+ * `legion configure` — connect credentials and wire adapters
12
+ * `legion help` — show all commands
10
13
  *
11
14
  * Legion is an Intelligent Virtual Assistant (IVA) — the AI classifies intent,
12
15
  * selects adapters, calls them, and synthesizes a response through your local
package/dist/bin/setup.js CHANGED
@@ -1,12 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * Epic AI® Legion — CLI Entry Point
4
- * `npx @epicai/legion` — setup wizard
5
- * `npx @epicai/legion --serve` — MCP server mode
6
- * `npx @epicai/legion add <name>` — add adapter
7
- * `npx @epicai/legion remove <name>` — remove adapter
8
- * `npx @epicai/legion health` — check adapter health
9
- * `npx @epicai/legion list` list all adapters
4
+ * `legion` / `npx @epicai/legion` — setup wizard
5
+ * `legion serve` / `--serve` — MCP server mode
6
+ * `legion add <name>` — add adapter and enter credentials
7
+ * `legion remove <name>` — remove an adapter
8
+ * `legion health` — check adapter status
9
+ * `legion list` show Curated + Custom adapters
10
+ * `legion search [term]` — search all available adapters
11
+ * `legion configure` — connect credentials and wire adapters
12
+ * `legion help` — show all commands
10
13
  *
11
14
  * Legion is an Intelligent Virtual Assistant (IVA) — the AI classifies intent,
12
15
  * selects adapters, calls them, and synthesizes a response through your local
@@ -749,39 +752,244 @@ async function cmdHealth() {
749
752
  state.lastHealthCheck = new Date().toISOString();
750
753
  saveState(state);
751
754
  }
752
- async function cmdList(searchTerm) {
755
+ // ─── Curated adapter IDs (mirrored here for list/search — keep in sync with wizard) ──
756
+ const CURATED_IDS = [
757
+ 'com-claude-mcp-pubmed-pubmed',
758
+ 'govbase-mcp',
759
+ 'searchcode',
760
+ 'robtex',
761
+ ];
762
+ // ─── legion list ────────────────────────────────────────────
763
+ async function cmdList() {
753
764
  const pc = (await import('picocolors')).default;
754
765
  const all = await loadAllAdapters();
755
- let filtered = all;
756
- if (searchTerm) {
757
- const term = searchTerm.toLowerCase();
758
- filtered = all.filter(a => a.id.includes(term) || a.name.toLowerCase().includes(term) ||
759
- (a.description || '').toLowerCase().includes(term) ||
760
- (a.category || '').includes(term));
761
- }
762
- // Group by category
763
- const categories = new Map();
764
- for (const a of filtered) {
765
- const cat = a.category || 'other';
766
- if (!categories.has(cat))
767
- categories.set(cat, []);
768
- categories.get(cat).push(a);
766
+ const state = loadState();
767
+ // Curated tier
768
+ const curatedRows = CURATED_IDS.map(id => all.find(a => a.id === id)).filter(Boolean);
769
+ // Custom tier in state but not curated
770
+ const customIds = Object.keys(state.adapters).filter(id => !CURATED_IDS.includes(id));
771
+ const customRows = customIds.map(id => all.find(a => a.id === id) || { id, name: id, type: 'unknown' });
772
+ console.log('');
773
+ // Curated
774
+ console.log(` ${pc.bold('Curated')} ${pc.dim(`(${curatedRows.length})`)} ${pc.dim('— open data, no credentials required')}`);
775
+ console.log('');
776
+ for (const a of curatedRows) {
777
+ const toolCount = a.rest?.toolCount || a.mcp?.toolCount || 0;
778
+ const typeLabel = a.type === 'mcp' ? pc.dim('MCP') : a.type === 'both' ? pc.dim('REST+MCP') : pc.dim('REST');
779
+ console.log(` ${pc.cyan(a.id.padEnd(35))} ${typeLabel} ${String(toolCount).padStart(3)} tools ${pc.dim((a.description || '').slice(0, 50))}`);
769
780
  }
770
781
  console.log('');
771
- console.log(` Epic AI® Legion — ${filtered.length} adapters${searchTerm ? ` matching "${searchTerm}"` : ''}`);
782
+ // Custom
783
+ console.log(` ${pc.bold('Custom')} ${pc.dim(`(${customRows.length})`)} ${pc.dim('— your APIs and credentials')}`);
772
784
  console.log('');
773
- for (const [cat, adapters] of Array.from(categories.entries()).sort((a, b) => b[1].length - a[1].length)) {
774
- console.log(` ${pc.cyan(cat)} (${adapters.length})`);
775
- for (const a of adapters.slice(0, 10)) {
776
- const tools = a.rest?.toolCount ? `${a.rest.toolCount} tools` : '';
777
- const type = a.type === 'mcp' ? pc.dim('MCP') : a.type === 'both' ? pc.dim('REST+MCP') : pc.dim('REST');
778
- console.log(` ${a.id.padEnd(35)} ${type} ${tools}`);
785
+ if (customRows.length === 0) {
786
+ console.log(` ${pc.dim('None yet — run:')} ${pc.cyan('legion configure')}`);
787
+ }
788
+ else {
789
+ for (const a of customRows) {
790
+ const toolCount = a.rest?.toolCount || 0;
791
+ const typeLabel = a.type === 'mcp' ? pc.dim('MCP') : a.type === 'both' ? pc.dim('REST+MCP') : pc.dim('REST');
792
+ console.log(` ${pc.cyan(a.id.padEnd(35))} ${typeLabel} ${String(toolCount).padStart(3)} tools ${pc.dim((a.description || '').slice(0, 50))}`);
779
793
  }
780
- if (adapters.length > 10) {
781
- console.log(` ${pc.dim(`... and ${adapters.length - 10} more`)}`);
794
+ }
795
+ console.log('');
796
+ }
797
+ // ─── legion search ──────────────────────────────────────────
798
+ async function cmdSearch(term) {
799
+ const pc = (await import('picocolors')).default;
800
+ const all = await loadAllAdapters();
801
+ const state = loadState();
802
+ if (!term) {
803
+ // No term — show curated tier + hint
804
+ const curatedRows = CURATED_IDS.map(id => all.find(a => a.id === id)).filter(Boolean);
805
+ console.log('');
806
+ console.log(` ${pc.bold('Curated adapters')} ${pc.dim('— vetted, open data, no credentials required')}`);
807
+ console.log('');
808
+ for (const a of curatedRows) {
809
+ const toolCount = a.rest?.toolCount || a.mcp?.toolCount || 0;
810
+ console.log(` ${pc.cyan(a.id.padEnd(35))} ${String(toolCount).padStart(3)} tools ${pc.dim((a.description || '').slice(0, 60))}`);
782
811
  }
783
812
  console.log('');
813
+ console.log(` ${pc.dim(`Search all ${all.length} available adapters:`)} ${pc.cyan('legion search <term>')}`);
814
+ console.log('');
815
+ return;
816
+ }
817
+ const t = term.toLowerCase();
818
+ const results = all.filter(a => a.id.includes(t) ||
819
+ a.name.toLowerCase().includes(t) ||
820
+ (a.description || '').toLowerCase().includes(t) ||
821
+ (a.category || '').includes(t));
822
+ if (results.length === 0) {
823
+ console.log(`\n No adapters matched "${term}". Try a broader term.\n`);
824
+ return;
825
+ }
826
+ // Sort: curated first, then custom (in state), then rest
827
+ const customInState = new Set(Object.keys(state.adapters));
828
+ const sorted = [
829
+ ...results.filter(a => CURATED_IDS.includes(a.id)),
830
+ ...results.filter(a => !CURATED_IDS.includes(a.id) && customInState.has(a.id)),
831
+ ...results.filter(a => !CURATED_IDS.includes(a.id) && !customInState.has(a.id)),
832
+ ];
833
+ const shown = sorted.slice(0, 20);
834
+ console.log('');
835
+ console.log(` ${pc.bold(`${results.length} adapters`)} matching "${term}"${results.length > 20 ? pc.dim(' (showing top 20)') : ''}`);
836
+ console.log('');
837
+ for (const a of shown) {
838
+ const toolCount = a.rest?.toolCount || a.mcp?.toolCount || 0;
839
+ const tag = CURATED_IDS.includes(a.id)
840
+ ? pc.green('curated')
841
+ : customInState.has(a.id)
842
+ ? pc.cyan('configured')
843
+ : pc.dim('available');
844
+ console.log(` ${pc.cyan(a.id.padEnd(35))} ${String(toolCount).padStart(3)} tools [${tag}]`);
845
+ if (a.description)
846
+ console.log(` ${pc.dim((' ').repeat(35))} ${pc.dim(a.description.slice(0, 70))}`);
847
+ console.log('');
848
+ }
849
+ const unconfigured = shown.filter(a => !CURATED_IDS.includes(a.id) && !customInState.has(a.id));
850
+ if (unconfigured.length > 0) {
851
+ console.log(` ${pc.dim('Add one:')} ${pc.cyan(`legion add ${unconfigured[0].id}`)}`);
852
+ console.log('');
853
+ }
854
+ }
855
+ // ─── legion configure ───────────────────────────────────────
856
+ async function cmdConfigure() {
857
+ const p = await import('@clack/prompts');
858
+ const pc = (await import('picocolors')).default;
859
+ console.log('');
860
+ p.intro(pc.bgCyan(pc.black(' Legion Configure — Connect Your APIs ')));
861
+ const all = await loadAllAdapters();
862
+ // Step 1: Where to look for credentials
863
+ const scanTargets = await p.multiselect({
864
+ message: 'Where should Legion look for existing credentials?',
865
+ options: [
866
+ { value: 'epic-ai', label: '~/.epic-ai/.env', hint: 'Legion\'s credential store' },
867
+ { value: 'home', label: '~/.env', hint: 'home directory env file' },
868
+ { value: 'cwd', label: '.env in current directory', hint: `${process.cwd()}/.env` },
869
+ ],
870
+ initialValues: ['epic-ai'],
871
+ required: true,
872
+ });
873
+ if (p.isCancel(scanTargets)) {
874
+ p.cancel('Cancelled.');
875
+ process.exit(0);
876
+ }
877
+ // Step 2: Scan and match
878
+ const s = p.spinner();
879
+ s.start('Scanning for credentials');
880
+ const foundCreds = {};
881
+ if (scanTargets.includes('epic-ai')) {
882
+ Object.assign(foundCreds, loadCredentials());
883
+ }
884
+ if (scanTargets.includes('home')) {
885
+ const p2 = join(homedir(), '.env');
886
+ if (existsSync(p2)) {
887
+ const lines = readFileSync(p2, 'utf-8').split('\n');
888
+ for (const line of lines) {
889
+ const eq = line.indexOf('=');
890
+ if (eq > 0)
891
+ foundCreds[line.slice(0, eq)] = line.slice(eq + 1);
892
+ }
893
+ }
894
+ }
895
+ if (scanTargets.includes('cwd')) {
896
+ const p3 = join(process.cwd(), '.env');
897
+ if (existsSync(p3)) {
898
+ const lines = readFileSync(p3, 'utf-8').split('\n');
899
+ for (const line of lines) {
900
+ const eq = line.indexOf('=');
901
+ if (eq > 0)
902
+ foundCreds[line.slice(0, eq)] = line.slice(eq + 1);
903
+ }
904
+ }
905
+ }
906
+ s.stop('Scan complete');
907
+ // Match credentials to adapters
908
+ const matched = [];
909
+ for (const adapter of all) {
910
+ if (CURATED_IDS.includes(adapter.id))
911
+ continue; // skip curated — already configured
912
+ const envKey = adapter.rest?.envKey;
913
+ if (envKey && foundCreds[envKey]) {
914
+ matched.push({ adapter, key: envKey });
915
+ }
916
+ else if (adapter.mcp?.envKeys) {
917
+ for (const k of adapter.mcp.envKeys) {
918
+ if (foundCreds[k]) {
919
+ matched.push({ adapter, key: k });
920
+ break;
921
+ }
922
+ }
923
+ }
784
924
  }
925
+ if (matched.length === 0) {
926
+ p.log.info('No matching credentials found in scanned locations.');
927
+ }
928
+ else {
929
+ p.note(matched.map(m => ` ${pc.green(m.key.padEnd(30))} → ${pc.cyan(m.adapter.name)}`).join('\n'), `Found ${matched.length} credential${matched.length !== 1 ? 's' : ''}`);
930
+ // Step 3: Confirm which to wire
931
+ const toWire = await p.multiselect({
932
+ message: 'Wire these adapters?',
933
+ options: matched.map(m => ({
934
+ value: m.adapter.id,
935
+ label: m.adapter.name,
936
+ hint: `${m.key} → ${m.adapter.description?.slice(0, 50) || m.adapter.id}`,
937
+ })),
938
+ initialValues: matched.map(m => m.adapter.id),
939
+ required: false,
940
+ });
941
+ if (p.isCancel(toWire)) {
942
+ p.cancel('Cancelled.');
943
+ process.exit(0);
944
+ }
945
+ // Step 4: Write to state and config
946
+ const state = loadState();
947
+ const config = loadConfig() || { selectedAdapters: [], secretsProvider: 'manual', aiClient: 'unknown' };
948
+ for (const id of toWire) {
949
+ const m = matched.find(x => x.adapter.id === id);
950
+ // Copy credential to ~/.epic-ai/.env if it came from elsewhere
951
+ writeCredential(m.key, foundCreds[m.key]);
952
+ state.adapters[id] = { type: m.adapter.type || 'unknown', status: 'configured', toolCount: m.adapter.rest?.toolCount || 0, lastVerified: null };
953
+ if (!config.selectedAdapters.includes(id))
954
+ config.selectedAdapters.push(id);
955
+ }
956
+ saveState(state);
957
+ saveConfig(config);
958
+ if (toWire.length > 0) {
959
+ p.log.success(`${toWire.length} adapter${toWire.length !== 1 ? 's' : ''} configured.`);
960
+ }
961
+ }
962
+ // Step 5: Add more manually?
963
+ const addMore = await p.confirm({ message: 'Add adapters manually?', initialValue: false });
964
+ if (!p.isCancel(addMore) && addMore) {
965
+ const name = await p.text({ message: 'Adapter ID (run "legion search <term>" to find one):' });
966
+ if (!p.isCancel(name) && name) {
967
+ await cmdAdd(name);
968
+ }
969
+ }
970
+ p.outro(`${pc.green('Done.')} Run ${pc.cyan('legion list')} to see your configured adapters.`);
971
+ }
972
+ // ─── legion help ────────────────────────────────────────────
973
+ async function cmdHelp() {
974
+ const pc = (await import('picocolors')).default;
975
+ console.log('');
976
+ console.log(` ${pc.bold('Epic AI® Legion')} — Intelligent Virtual Assistant (IVA)`);
977
+ console.log('');
978
+ console.log(` ${pc.bold('Commands:')}`);
979
+ console.log('');
980
+ console.log(` ${pc.cyan('legion')} run the setup wizard`);
981
+ console.log(` ${pc.cyan('legion query "<question>"')} route a question to your adapters via your AI client`);
982
+ console.log(` ${pc.cyan('legion list')} show Curated + Custom adapters`);
983
+ console.log(` ${pc.cyan('legion search [term]')} search all available adapters`);
984
+ console.log(` ${pc.cyan('legion add <id>')} add an adapter and enter credentials`);
985
+ console.log(` ${pc.cyan('legion remove <id>')} remove an adapter`);
986
+ console.log(` ${pc.cyan('legion configure')} connect your APIs and credentials`);
987
+ console.log(` ${pc.cyan('legion health')} check adapter status`);
988
+ console.log(` ${pc.cyan('legion serve')} start as MCP server (used by AI clients)`);
989
+ console.log(` ${pc.cyan('legion help')} show this help`);
990
+ console.log('');
991
+ console.log(` ${pc.dim('Docs:')} https://legion.epic-ai.io`);
992
+ console.log('');
785
993
  }
786
994
  // ─── Setup Wizard ───────────────────────────────────────────
787
995
  async function runSetupWizard() {
@@ -918,314 +1126,107 @@ async function runSetupWizard() {
918
1126
  p.log.success(`Using ${system.localBackend} on port ${system.localPort}`);
919
1127
  configuredClients.push('local');
920
1128
  }
921
- // Step 3: Secrets provider
922
- const secretsChoice = await p.select({
923
- message: 'Where are your API and MCP credentials stored?',
924
- options: [
925
- { value: 'vault', label: 'HashiCorp Vault' },
926
- { value: 'aws-sm', label: 'AWS Secrets Manager' },
927
- { value: 'azure-kv', label: 'Azure Key Vault' },
928
- { value: '1password', label: '1Password CLI' },
929
- { value: 'doppler', label: 'Doppler' },
930
- { value: 'env', label: 'Environment variables', hint: 'already exported in shell' },
931
- { value: 'manual', label: 'I\'ll enter them manually' },
932
- ],
933
- });
934
- if (p.isCancel(secretsChoice)) {
935
- p.cancel('Setup cancelled.');
936
- process.exit(0);
937
- }
938
- let vaultAddr = '';
939
- let vaultToken = '';
940
- let vaultConnected = false;
941
- if (secretsChoice === 'vault') {
942
- const envAddr = process.env.VAULT_ADDR;
943
- const envToken = process.env.VAULT_TOKEN;
944
- if (envAddr && envToken) {
945
- p.log.success(`Detected VAULT_ADDR=${envAddr}`);
946
- vaultAddr = envAddr;
947
- vaultToken = envToken;
948
- }
949
- else {
950
- const addr = await p.text({
951
- message: 'Vault address',
952
- placeholder: 'https://vault.example.com:8200',
953
- validate: (v) => { if (!v)
954
- return 'Required'; if (!v.startsWith('http'))
955
- return 'Must start with http:// or https://'; },
956
- });
957
- if (p.isCancel(addr)) {
958
- p.cancel('Setup cancelled.');
959
- process.exit(0);
960
- }
961
- const token = await p.password({ message: 'Vault token', validate: (v) => { if (!v)
962
- return 'Required'; } });
963
- if (p.isCancel(token)) {
964
- p.cancel('Setup cancelled.');
965
- process.exit(0);
966
- }
967
- vaultAddr = addr;
968
- vaultToken = token;
969
- }
970
- s.start('Connecting to Vault');
971
- try {
972
- const controller = new AbortController();
973
- const t = setTimeout(() => controller.abort(), 5000);
974
- const resp = await fetch(`${vaultAddr}/v1/sys/health`, { headers: { 'X-Vault-Token': vaultToken }, signal: controller.signal });
975
- clearTimeout(t);
976
- if (resp.ok) {
977
- vaultConnected = true;
978
- s.stop(`Connected to Vault at ${vaultAddr}`);
979
- writeCredential('VAULT_ADDR', vaultAddr);
980
- writeCredential('VAULT_TOKEN', vaultToken);
981
- }
982
- else {
983
- s.stop(`Vault returned ${resp.status} — check your token`);
984
- }
985
- }
986
- catch {
987
- s.stop(`Cannot reach Vault at ${vaultAddr}`);
988
- }
989
- }
990
- if (secretsChoice === '1password') {
991
- try {
992
- execSync('op --version', { stdio: 'pipe' });
993
- p.log.success('1Password CLI detected');
994
- }
995
- catch {
996
- p.log.warning('1Password CLI not found — install from https://1password.com/downloads/command-line');
997
- }
998
- }
999
- if (secretsChoice === 'doppler') {
1000
- try {
1001
- execSync('doppler --version', { stdio: 'pipe' });
1002
- p.log.success('Doppler CLI detected');
1003
- }
1004
- catch {
1005
- p.log.warning('Doppler CLI not found — install from https://docs.doppler.com/docs/install-cli');
1006
- }
1007
- }
1008
- // Step 4: Auto-wire adapters from credentials
1009
- const selectedAdapterIds = [];
1010
- const existingCreds = loadCredentials();
1011
- // Match adapters whose required env keys are already present
1012
- const autoMatched = [];
1013
- for (const adapter of allAdapters) {
1014
- const restKey = adapter.rest?.envKey;
1015
- const mcpKeys = adapter.mcp?.envKeys || [];
1016
- const allKeys = [...(restKey ? [restKey] : []), ...mcpKeys];
1017
- if (allKeys.length > 0 && allKeys.every(k => !!existingCreds[k])) {
1018
- autoMatched.push(adapter);
1019
- }
1020
- }
1021
- if (autoMatched.length > 0) {
1022
- p.note(autoMatched.map(a => `${pc.green('✓')} ${a.name || a.id}`).join('\n'), `${autoMatched.length} adapter${autoMatched.length !== 1 ? 's' : ''} matched from your credentials`);
1023
- selectedAdapterIds.push(...autoMatched.map(a => a.id));
1024
- }
1025
- else {
1026
- p.log.info('No adapters matched from credentials — use ' + pc.cyan('npx @epicai/legion add <name>') + ' to connect adapters.');
1027
- }
1028
- // Optional: search for additional adapters
1029
- const addMore = await p.confirm({
1030
- message: 'Search for additional adapters to configure?',
1031
- initialValue: false,
1032
- });
1033
- if (!p.isCancel(addMore) && addMore) {
1034
- let searching = true;
1035
- while (searching) {
1036
- const searchTerm = await p.text({
1037
- message: 'Search adapters (name or keyword)',
1038
- placeholder: 'e.g. grafana, paypal, github',
1039
- });
1040
- if (p.isCancel(searchTerm) || !searchTerm)
1041
- break;
1042
- const term = searchTerm.toLowerCase();
1043
- const matches = allAdapters.filter(a => !selectedAdapterIds.includes(a.id) && (a.id.includes(term) ||
1044
- (a.name || '').toLowerCase().includes(term) ||
1045
- (a.description || '').toLowerCase().includes(term) ||
1046
- (a.category || '').includes(term)));
1047
- if (matches.length === 0) {
1048
- p.log.warning(`No adapters found for "${searchTerm}"`);
1049
- }
1050
- else {
1051
- const options = matches.slice(0, 50).map(a => ({
1052
- value: a.id,
1053
- label: a.name || a.id,
1054
- hint: a.description?.slice(0, 60),
1055
- }));
1056
- const picked = await p.multiselect({
1057
- message: `${matches.length} match${matches.length !== 1 ? 'es' : ''} — select to add`,
1058
- options,
1059
- required: false,
1060
- });
1061
- if (!p.isCancel(picked)) {
1062
- selectedAdapterIds.push(...picked);
1063
- }
1064
- }
1065
- const cont = await p.confirm({ message: 'Search for more?', initialValue: false });
1066
- if (p.isCancel(cont) || !cont)
1067
- searching = false;
1068
- }
1069
- }
1070
- // Step 5: Credentials
1071
- if (selectedAdapterIds.length > 0) {
1072
- if (secretsChoice === 'vault' && vaultConnected) {
1073
- s.start('Pulling credentials from Vault');
1074
- let pulled = 0;
1075
- for (const adapterId of selectedAdapterIds) {
1076
- try {
1077
- const controller = new AbortController();
1078
- const t = setTimeout(() => controller.abort(), 3000);
1079
- const resp = await fetch(`${vaultAddr}/v1/secret/data/${adapterId}`, {
1080
- headers: { 'X-Vault-Token': vaultToken },
1081
- signal: controller.signal,
1082
- });
1083
- clearTimeout(t);
1084
- if (resp.ok) {
1085
- const data = await resp.json();
1086
- const secrets = data?.data?.data;
1087
- if (secrets) {
1088
- for (const [k, v] of Object.entries(secrets)) {
1089
- writeCredential(k, v);
1090
- }
1091
- pulled++;
1092
- }
1093
- }
1094
- }
1095
- catch { /* skip */ }
1096
- }
1097
- s.stop(`Pulled credentials for ${pulled} of ${selectedAdapterIds.length} adapters`);
1098
- }
1099
- else if (secretsChoice === '1password') {
1100
- s.start('Pulling credentials from 1Password');
1101
- let pulled = 0;
1102
- for (const adapterId of selectedAdapterIds) {
1103
- try {
1104
- const result = execSync(`op item get "${adapterId}" --format json 2>/dev/null`, { encoding: 'utf-8', timeout: 5000 });
1105
- const item = JSON.parse(result);
1106
- const fields = item.fields || [];
1107
- for (const field of fields) {
1108
- if (field.value && field.label) {
1109
- const envKey = `${adapterId.toUpperCase().replace(/-/g, '_')}_${field.label.toUpperCase().replace(/\s+/g, '_')}`;
1110
- writeCredential(envKey, field.value);
1111
- }
1112
- }
1113
- pulled++;
1114
- }
1115
- catch { /* skip */ }
1116
- }
1117
- s.stop(`Pulled credentials for ${pulled} of ${selectedAdapterIds.length} adapters`);
1118
- }
1119
- else if (secretsChoice === 'doppler') {
1120
- s.start('Pulling credentials from Doppler');
1121
- try {
1122
- const result = execSync('doppler secrets download --no-file --format json 2>/dev/null', { encoding: 'utf-8', timeout: 10000 });
1123
- const secrets = JSON.parse(result);
1124
- for (const [k, v] of Object.entries(secrets)) {
1125
- if (typeof v === 'string')
1126
- writeCredential(k, v);
1127
- }
1128
- s.stop('Credentials pulled from Doppler');
1129
- }
1130
- catch {
1131
- s.stop('Doppler pull failed — configure with: doppler setup');
1132
- }
1133
- }
1134
- else if (secretsChoice === 'manual') {
1135
- for (const adapterId of selectedAdapterIds) {
1136
- const adapter = allAdapters.find(a => a.id === adapterId);
1137
- if (!adapter)
1138
- continue;
1139
- const envKey = adapter.rest?.envKey || `${adapterId.toUpperCase().replace(/-/g, '_')}_API_KEY`;
1140
- const key = await p.password({ message: `${envKey} for ${adapter.name || adapterId}` });
1141
- if (!p.isCancel(key) && key) {
1142
- writeCredential(envKey, key);
1143
- }
1144
- }
1145
- }
1146
- else if (secretsChoice === 'env') {
1147
- p.log.info('Using credentials from environment variables');
1148
- }
1149
- // Step 6: Install stdio dependencies
1150
- for (const adapterId of selectedAdapterIds) {
1151
- const adapter = allAdapters.find(a => a.id === adapterId);
1152
- if (!adapter?.mcp?.packageName || adapter.mcp.transport !== 'stdio')
1153
- continue;
1154
- s.start(`Installing ${adapter.name || adapterId}`);
1155
- try {
1156
- execSync(`npm install -g --ignore-scripts ${adapter.mcp.packageName}`, { stdio: 'pipe', timeout: 60000 });
1157
- // Verify artifact integrity
1158
- try {
1159
- const { ArtifactVerifier } = await import('../trust/ArtifactVerifier.js');
1160
- const verifier = new ArtifactVerifier({ verifyDigests: true });
1161
- const pkgPath = execSync(`npm root -g`, { encoding: 'utf-8', timeout: 5000 }).trim();
1162
- const mainFile = join(pkgPath, adapter.mcp.packageName, 'package.json');
1163
- const result = await verifier.verify(adapter.mcp.packageName, mainFile);
1164
- if (!result.valid) {
1165
- s.stop(`${pc.red('✗')} ${adapter.name || adapterId} — integrity check failed`);
1166
- }
1167
- else {
1168
- s.stop(`${pc.green('✓')} ${adapter.name || adapterId} installed — verified`);
1169
- }
1170
- }
1171
- catch {
1172
- s.stop(`${pc.green('✓')} ${adapter.name || adapterId} installed`);
1173
- }
1174
- }
1175
- catch {
1176
- s.stop(`${pc.yellow('!')} ${adapter.name || adapterId} — run manually: npm install -g ${adapter.mcp.packageName}`);
1177
- }
1178
- }
1179
- // Step 7: Verify
1180
- s.start('Verifying connections');
1181
- const creds = loadCredentials();
1182
- let verified = 0;
1183
- const results = [];
1184
- for (const adapterId of selectedAdapterIds) {
1185
- const adapter = allAdapters.find(a => a.id === adapterId);
1186
- if (!adapter)
1187
- continue;
1188
- const hasKey = adapter.rest?.envKey ? !!creds[adapter.rest.envKey] : false;
1189
- if (hasKey) {
1190
- verified++;
1191
- results.push(`${pc.green('✓')} ${adapter.name || adapterId} ${adapter.rest?.toolCount || 0} tools`);
1192
- }
1193
- else {
1194
- results.push(`${pc.yellow('○')} ${adapter.name || adapterId} credentials needed`);
1195
- }
1196
- }
1197
- s.stop('Verification complete');
1198
- if (results.length > 0) {
1199
- p.note(results.join('\n'), `${verified} of ${selectedAdapterIds.length} adapters ready`);
1200
- }
1201
- }
1202
- // Step 8: Save state and config
1129
+ // Step 3: Auto-configure all curated (vetted zero-credential) adapters
1130
+ // IMPORTANT: Only add adapters to CURATED after manual vetting — confirm they
1131
+ // return real data, contain no adult/inappropriate content, and are stable.
1132
+ const CURATED = [
1133
+ {
1134
+ id: 'com-claude-mcp-pubmed-pubmed',
1135
+ name: 'PubMed',
1136
+ desc: 'Search 36 million biomedical research papers',
1137
+ tools: 7,
1138
+ demoQuery: 'Recent clinical trials on GLP-1 drugs for obesity',
1139
+ exampleQuery: 'legion query "recent clinical trials on GLP-1 drugs for obesity"',
1140
+ },
1141
+ {
1142
+ id: 'govbase-mcp',
1143
+ name: 'Govbase',
1144
+ desc: 'Government data — legislators, bills, committees',
1145
+ tools: 10,
1146
+ demoQuery: 'Who chairs the Senate Armed Services Committee?',
1147
+ exampleQuery: 'legion query "who chairs the Senate Armed Services Committee?"',
1148
+ },
1149
+ {
1150
+ id: 'searchcode',
1151
+ name: 'Searchcode',
1152
+ desc: 'Search 75 billion lines of open source code',
1153
+ tools: 6,
1154
+ demoQuery: 'Open source implementations of rate limiting in Go',
1155
+ exampleQuery: 'legion query "open source implementations of rate limiting in Go"',
1156
+ },
1157
+ {
1158
+ id: 'robtex',
1159
+ name: 'Robtex',
1160
+ desc: 'Network intelligence — DNS, IP, ASN lookups',
1161
+ tools: 45,
1162
+ demoQuery: 'DNS records and ASN for cloudflare.com',
1163
+ exampleQuery: 'legion query "DNS records and ASN for cloudflare.com"',
1164
+ },
1165
+ ];
1166
+ const s2 = p.spinner();
1167
+ s2.start('Connecting curated data sources');
1203
1168
  const state = loadState();
1204
- for (const id of selectedAdapterIds) {
1205
- const adapter = allAdapters.find(a => a.id === id);
1206
- state.adapters[id] = {
1207
- type: adapter?.type || 'unknown',
1169
+ const curatedAdapterEntries = CURATED.map(c => allAdapters.find(a => a.id === c.id)).filter(Boolean);
1170
+ for (const c of CURATED) {
1171
+ const adapter = allAdapters.find(a => a.id === c.id);
1172
+ state.adapters[c.id] = {
1173
+ type: adapter?.type || 'mcp',
1208
1174
  status: 'configured',
1209
- toolCount: adapter?.rest?.toolCount || 0,
1175
+ toolCount: c.tools,
1210
1176
  lastVerified: null,
1211
1177
  };
1212
1178
  }
1213
1179
  saveState(state);
1214
1180
  saveConfig({
1215
- selectedAdapters: selectedAdapterIds,
1216
- secretsProvider: secretsChoice,
1181
+ selectedAdapters: CURATED.map(c => c.id),
1182
+ secretsProvider: 'manual',
1217
1183
  aiClient: configuredClients.join(','),
1218
1184
  localBackend: system.localBackend || undefined,
1219
1185
  });
1220
- // Outro
1186
+ s2.stop('Curated data sources connected');
1187
+ p.note(CURATED.map(c => `${pc.green('✓')} ${c.name.padEnd(14)} ${String(c.tools).padStart(2)} tools ${pc.dim(c.desc)}`).join('\n'), `Curated (${CURATED.length}) — no credentials required`);
1188
+ // Step 4: Routing demo — prove intelligence in-process, no network calls
1189
+ const { ToolPreFilter } = await import('../federation/ToolPreFilter.js');
1190
+ function buildDemoTools(adapters) {
1191
+ const tools = [];
1192
+ for (const adapter of adapters) {
1193
+ const toolNames = adapter.rest?.toolNames || adapter.mcp?.toolNames || [];
1194
+ if (toolNames.length === 0) {
1195
+ tools.push({ name: `${adapter.id}:default`, description: `${adapter.name} — ${adapter.description || adapter.id}`, parameters: { type: 'object', properties: {} }, server: adapter.id, tier: 'orchestrated' });
1196
+ }
1197
+ else {
1198
+ for (const t of toolNames) {
1199
+ tools.push({ name: `${adapter.id}:${t}`, description: `${adapter.name} — ${t.replace(/_/g, ' ')}`, parameters: { type: 'object', properties: {} }, server: adapter.id, tier: 'orchestrated' });
1200
+ }
1201
+ }
1202
+ }
1203
+ return tools;
1204
+ }
1205
+ const demoFilter = new ToolPreFilter();
1206
+ demoFilter.index(buildDemoTools(curatedAdapterEntries));
1207
+ const routingLines = [];
1208
+ for (const c of CURATED) {
1209
+ const matches = await demoFilter.select(c.demoQuery, { maxTools: 3, maxPerServer: 2 });
1210
+ const topId = matches[0]?.server;
1211
+ const routed = topId === c.id;
1212
+ const arrow = routed ? pc.green('→') : pc.yellow('→');
1213
+ const adapterLabel = routed ? pc.green(c.name) : pc.yellow(topId || '?');
1214
+ routingLines.push(` ${pc.dim(`"${c.demoQuery.slice(0, 48)}${c.demoQuery.length > 48 ? '…' : ''}"`)}`);
1215
+ routingLines.push(` ${arrow} ${adapterLabel}`);
1216
+ routingLines.push('');
1217
+ }
1218
+ p.note(routingLines.join('\n').trimEnd(), 'Routing intelligence');
1219
+ // Step 5: Outro — hand off to shell
1221
1220
  p.note([
1222
- `Add adapters: ${pc.cyan('npx @epicai/legion add <name>')}`,
1223
- `Remove adapters: ${pc.cyan('npx @epicai/legion remove <name>')}`,
1224
- `Health check: ${pc.cyan('npx @epicai/legion health')}`,
1225
- `Update adapters: ${pc.cyan('npx @epicai/legion update')}`,
1226
- `List adapters: ${pc.cyan('npx @epicai/legion list')}`,
1227
- ].join('\n'), 'Manage your adapters');
1228
- p.outro(`${pc.green('Legion is ready.')} Your credentials never leave this machine.`);
1221
+ pc.bold('Try these yourself:'),
1222
+ '',
1223
+ ...CURATED.map(c => ` ${pc.cyan(c.exampleQuery)}`),
1224
+ ].join('\n'), 'Test it');
1225
+ p.note([
1226
+ ` ${pc.cyan('legion configure')} connect your APIs and credentials`,
1227
+ ` ${pc.cyan('legion help')} see all commands`,
1228
+ ].join('\n'), 'When you\'re ready to connect your own APIs');
1229
+ p.outro(`${pc.green('Legion is ready.')} Your data never leaves this machine.`);
1229
1230
  }
1230
1231
  // ─── Main router ────────────────────────────────────────────
1231
1232
  async function main() {
@@ -1238,14 +1239,14 @@ async function main() {
1238
1239
  switch (command) {
1239
1240
  case 'add':
1240
1241
  if (!args[1]) {
1241
- console.error('Usage: npx @epicai/legion add <adapter-name>');
1242
+ console.error('Usage: legion add <adapter-id> (run "legion search <term>" to find one)');
1242
1243
  process.exit(1);
1243
1244
  }
1244
1245
  await cmdAdd(args[1]);
1245
1246
  break;
1246
1247
  case 'remove':
1247
1248
  if (!args[1]) {
1248
- console.error('Usage: npx @epicai/legion remove <adapter-name>');
1249
+ console.error('Usage: legion remove <adapter-id>');
1249
1250
  process.exit(1);
1250
1251
  }
1251
1252
  await cmdRemove(args[1]);
@@ -1254,7 +1255,24 @@ async function main() {
1254
1255
  await cmdHealth();
1255
1256
  break;
1256
1257
  case 'list':
1257
- await cmdList(args[1]);
1258
+ await cmdList();
1259
+ break;
1260
+ case 'search':
1261
+ await cmdSearch(args[1]);
1262
+ break;
1263
+ case 'configure':
1264
+ await cmdConfigure();
1265
+ break;
1266
+ case 'help':
1267
+ case '--help':
1268
+ case '-h':
1269
+ await cmdHelp();
1270
+ break;
1271
+ case 'query':
1272
+ console.log('\n legion query routes through your AI client (Claude Code, Cursor, etc.)');
1273
+ console.log(' Start Legion as an MCP server: legion serve');
1274
+ console.log(' Then ask your AI client: "use legion_query to find..."');
1275
+ console.log('\n Run: legion help\n');
1258
1276
  break;
1259
1277
  case 'serve':
1260
1278
  await startMcpServer();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@epicai/legion",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Epic AI® Legion — 35,020 tools. One self-hosted MCP server. Intelligent Virtual Assistant (IVA) integration layer for AI agents.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -121,6 +121,9 @@
121
121
  "vitest": "^3.0.0"
122
122
  },
123
123
  "scripts": {
124
+ "release:patch": "npm run build && npm version patch && npm publish --access public",
125
+ "release:minor": "npm run build && npm version minor && npm publish --access public",
126
+ "release:major": "npm run build && npm version major && npm publish --access public",
124
127
  "build": "tsc",
125
128
  "postbuild": "npx tsx ../epic-ai/src/fabrique/generate-adapter-catalog.ts",
126
129
  "pretest": "tsc",