@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 +19 -0
- package/dist/bin/setup.d.ts +9 -6
- package/dist/bin/setup.js +348 -330
- package/package.json +4 -1
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
|
package/dist/bin/setup.d.ts
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`
|
|
5
|
-
* `
|
|
6
|
-
* `
|
|
7
|
-
* `
|
|
8
|
-
* `
|
|
9
|
-
* `
|
|
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`
|
|
5
|
-
* `
|
|
6
|
-
* `
|
|
7
|
-
* `
|
|
8
|
-
* `
|
|
9
|
-
* `
|
|
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
|
-
|
|
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
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
//
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
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
|
-
|
|
782
|
+
// Custom
|
|
783
|
+
console.log(` ${pc.bold('Custom')} ${pc.dim(`(${customRows.length})`)} ${pc.dim('— your APIs and credentials')}`);
|
|
772
784
|
console.log('');
|
|
773
|
-
|
|
774
|
-
console.log(`
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
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
|
-
|
|
781
|
-
|
|
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:
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
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
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
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:
|
|
1175
|
+
toolCount: c.tools,
|
|
1210
1176
|
lastVerified: null,
|
|
1211
1177
|
};
|
|
1212
1178
|
}
|
|
1213
1179
|
saveState(state);
|
|
1214
1180
|
saveConfig({
|
|
1215
|
-
selectedAdapters:
|
|
1216
|
-
secretsProvider:
|
|
1181
|
+
selectedAdapters: CURATED.map(c => c.id),
|
|
1182
|
+
secretsProvider: 'manual',
|
|
1217
1183
|
aiClient: configuredClients.join(','),
|
|
1218
1184
|
localBackend: system.localBackend || undefined,
|
|
1219
1185
|
});
|
|
1220
|
-
|
|
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
|
-
|
|
1223
|
-
|
|
1224
|
-
`
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
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:
|
|
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:
|
|
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(
|
|
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.
|
|
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",
|