@yangfei_93sky/biocli 0.2.0
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/LICENSE +21 -0
- package/README.md +197 -0
- package/dist/batch.d.ts +20 -0
- package/dist/batch.js +69 -0
- package/dist/build-manifest.d.ts +38 -0
- package/dist/build-manifest.js +186 -0
- package/dist/cache.d.ts +28 -0
- package/dist/cache.js +126 -0
- package/dist/cli-manifest.json +1500 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +336 -0
- package/dist/clis/_shared/common.d.ts +8 -0
- package/dist/clis/_shared/common.js +13 -0
- package/dist/clis/_shared/eutils.d.ts +9 -0
- package/dist/clis/_shared/eutils.js +9 -0
- package/dist/clis/_shared/organism-db.d.ts +23 -0
- package/dist/clis/_shared/organism-db.js +58 -0
- package/dist/clis/_shared/xml-helpers.d.ts +58 -0
- package/dist/clis/_shared/xml-helpers.js +266 -0
- package/dist/clis/aggregate/enrichment.d.ts +7 -0
- package/dist/clis/aggregate/enrichment.js +105 -0
- package/dist/clis/aggregate/gene-dossier.d.ts +13 -0
- package/dist/clis/aggregate/gene-dossier.js +248 -0
- package/dist/clis/aggregate/gene-profile.d.ts +16 -0
- package/dist/clis/aggregate/gene-profile.js +305 -0
- package/dist/clis/aggregate/literature-brief.d.ts +7 -0
- package/dist/clis/aggregate/literature-brief.js +79 -0
- package/dist/clis/aggregate/variant-dossier.d.ts +11 -0
- package/dist/clis/aggregate/variant-dossier.js +161 -0
- package/dist/clis/aggregate/variant-interpret.d.ts +10 -0
- package/dist/clis/aggregate/variant-interpret.js +210 -0
- package/dist/clis/aggregate/workflow-prepare.d.ts +12 -0
- package/dist/clis/aggregate/workflow-prepare.js +228 -0
- package/dist/clis/aggregate/workflow-scout.d.ts +13 -0
- package/dist/clis/aggregate/workflow-scout.js +175 -0
- package/dist/clis/clinvar/search.d.ts +8 -0
- package/dist/clis/clinvar/search.js +61 -0
- package/dist/clis/clinvar/variant.d.ts +7 -0
- package/dist/clis/clinvar/variant.js +53 -0
- package/dist/clis/enrichr/analyze.d.ts +7 -0
- package/dist/clis/enrichr/analyze.js +48 -0
- package/dist/clis/ensembl/lookup.d.ts +6 -0
- package/dist/clis/ensembl/lookup.js +38 -0
- package/dist/clis/ensembl/vep.d.ts +7 -0
- package/dist/clis/ensembl/vep.js +86 -0
- package/dist/clis/ensembl/xrefs.d.ts +6 -0
- package/dist/clis/ensembl/xrefs.js +36 -0
- package/dist/clis/gene/fetch.d.ts +10 -0
- package/dist/clis/gene/fetch.js +96 -0
- package/dist/clis/gene/info.d.ts +7 -0
- package/dist/clis/gene/info.js +37 -0
- package/dist/clis/gene/search.d.ts +7 -0
- package/dist/clis/gene/search.js +71 -0
- package/dist/clis/geo/dataset.d.ts +7 -0
- package/dist/clis/geo/dataset.js +55 -0
- package/dist/clis/geo/download.d.ts +17 -0
- package/dist/clis/geo/download.js +115 -0
- package/dist/clis/geo/samples.d.ts +7 -0
- package/dist/clis/geo/samples.js +57 -0
- package/dist/clis/geo/search.d.ts +8 -0
- package/dist/clis/geo/search.js +66 -0
- package/dist/clis/kegg/convert.d.ts +7 -0
- package/dist/clis/kegg/convert.js +37 -0
- package/dist/clis/kegg/disease.d.ts +6 -0
- package/dist/clis/kegg/disease.js +57 -0
- package/dist/clis/kegg/link.d.ts +7 -0
- package/dist/clis/kegg/link.js +36 -0
- package/dist/clis/kegg/pathway.d.ts +6 -0
- package/dist/clis/kegg/pathway.js +37 -0
- package/dist/clis/pubmed/abstract.d.ts +7 -0
- package/dist/clis/pubmed/abstract.js +42 -0
- package/dist/clis/pubmed/cited-by.d.ts +7 -0
- package/dist/clis/pubmed/cited-by.js +77 -0
- package/dist/clis/pubmed/fetch.d.ts +6 -0
- package/dist/clis/pubmed/fetch.js +36 -0
- package/dist/clis/pubmed/info.yaml +22 -0
- package/dist/clis/pubmed/related.d.ts +7 -0
- package/dist/clis/pubmed/related.js +81 -0
- package/dist/clis/pubmed/search.d.ts +8 -0
- package/dist/clis/pubmed/search.js +63 -0
- package/dist/clis/snp/lookup.d.ts +7 -0
- package/dist/clis/snp/lookup.js +57 -0
- package/dist/clis/sra/download.d.ts +18 -0
- package/dist/clis/sra/download.js +217 -0
- package/dist/clis/sra/run.d.ts +8 -0
- package/dist/clis/sra/run.js +77 -0
- package/dist/clis/sra/search.d.ts +8 -0
- package/dist/clis/sra/search.js +83 -0
- package/dist/clis/string/enrichment.d.ts +7 -0
- package/dist/clis/string/enrichment.js +50 -0
- package/dist/clis/string/network.d.ts +7 -0
- package/dist/clis/string/network.js +47 -0
- package/dist/clis/string/partners.d.ts +4 -0
- package/dist/clis/string/partners.js +44 -0
- package/dist/clis/taxonomy/lookup.d.ts +8 -0
- package/dist/clis/taxonomy/lookup.js +54 -0
- package/dist/clis/uniprot/fetch.d.ts +7 -0
- package/dist/clis/uniprot/fetch.js +82 -0
- package/dist/clis/uniprot/search.d.ts +6 -0
- package/dist/clis/uniprot/search.js +65 -0
- package/dist/clis/uniprot/sequence.d.ts +7 -0
- package/dist/clis/uniprot/sequence.js +51 -0
- package/dist/commander-adapter.d.ts +27 -0
- package/dist/commander-adapter.js +286 -0
- package/dist/completion.d.ts +19 -0
- package/dist/completion.js +117 -0
- package/dist/config.d.ts +57 -0
- package/dist/config.js +94 -0
- package/dist/databases/enrichr.d.ts +28 -0
- package/dist/databases/enrichr.js +131 -0
- package/dist/databases/ensembl.d.ts +14 -0
- package/dist/databases/ensembl.js +106 -0
- package/dist/databases/index.d.ts +45 -0
- package/dist/databases/index.js +49 -0
- package/dist/databases/kegg.d.ts +26 -0
- package/dist/databases/kegg.js +136 -0
- package/dist/databases/ncbi.d.ts +28 -0
- package/dist/databases/ncbi.js +144 -0
- package/dist/databases/string-db.d.ts +19 -0
- package/dist/databases/string-db.js +105 -0
- package/dist/databases/uniprot.d.ts +13 -0
- package/dist/databases/uniprot.js +110 -0
- package/dist/discovery.d.ts +32 -0
- package/dist/discovery.js +235 -0
- package/dist/doctor.d.ts +19 -0
- package/dist/doctor.js +151 -0
- package/dist/errors.d.ts +68 -0
- package/dist/errors.js +105 -0
- package/dist/execution.d.ts +15 -0
- package/dist/execution.js +178 -0
- package/dist/hooks.d.ts +48 -0
- package/dist/hooks.js +58 -0
- package/dist/main.d.ts +13 -0
- package/dist/main.js +31 -0
- package/dist/ncbi-fetch.d.ts +10 -0
- package/dist/ncbi-fetch.js +10 -0
- package/dist/output.d.ts +18 -0
- package/dist/output.js +394 -0
- package/dist/pipeline/executor.d.ts +22 -0
- package/dist/pipeline/executor.js +40 -0
- package/dist/pipeline/index.d.ts +6 -0
- package/dist/pipeline/index.js +6 -0
- package/dist/pipeline/registry.d.ts +16 -0
- package/dist/pipeline/registry.js +31 -0
- package/dist/pipeline/steps/fetch.d.ts +21 -0
- package/dist/pipeline/steps/fetch.js +160 -0
- package/dist/pipeline/steps/transform.d.ts +26 -0
- package/dist/pipeline/steps/transform.js +92 -0
- package/dist/pipeline/steps/xml-parse.d.ts +12 -0
- package/dist/pipeline/steps/xml-parse.js +27 -0
- package/dist/pipeline/template.d.ts +35 -0
- package/dist/pipeline/template.js +312 -0
- package/dist/rate-limiter.d.ts +56 -0
- package/dist/rate-limiter.js +120 -0
- package/dist/registry-api.d.ts +15 -0
- package/dist/registry-api.js +13 -0
- package/dist/registry.d.ts +90 -0
- package/dist/registry.js +100 -0
- package/dist/schema.d.ts +80 -0
- package/dist/schema.js +72 -0
- package/dist/spinner.d.ts +19 -0
- package/dist/spinner.js +37 -0
- package/dist/types.d.ts +101 -0
- package/dist/types.js +27 -0
- package/dist/utils.d.ts +16 -0
- package/dist/utils.js +40 -0
- package/dist/validate.d.ts +29 -0
- package/dist/validate.js +136 -0
- package/dist/verify.d.ts +20 -0
- package/dist/verify.js +131 -0
- package/dist/version.d.ts +13 -0
- package/dist/version.js +36 -0
- package/dist/xml-parser.d.ts +19 -0
- package/dist/xml-parser.js +119 -0
- package/dist/yaml-schema.d.ts +40 -0
- package/dist/yaml-schema.js +62 -0
- package/package.json +68 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sra/run — Get SRA run details by accession.
|
|
3
|
+
*
|
|
4
|
+
* Searches for a single SRA accession (SRR, SRX, SRP, etc.) and
|
|
5
|
+
* retrieves detailed run metadata via esummary (JSON). Parses the
|
|
6
|
+
* embedded XML strings in expxml/runs fields.
|
|
7
|
+
*/
|
|
8
|
+
import { cli, Strategy } from '../../registry.js';
|
|
9
|
+
import { CliError } from '../../errors.js';
|
|
10
|
+
import { buildEutilsUrl } from '../_shared/eutils.js';
|
|
11
|
+
// ── SRA XML extraction helpers ───────────────────────────────────────────────
|
|
12
|
+
/** Extract text content of an XML tag (e.g. <Title>foo</Title> -> "foo"). */
|
|
13
|
+
function extractXmlTag(xml, tag) {
|
|
14
|
+
const m = xml.match(new RegExp(`<${tag}[^>]*>([^<]*)</${tag}>`));
|
|
15
|
+
return m ? m[1].trim() : '';
|
|
16
|
+
}
|
|
17
|
+
/** Extract an attribute value from an XML tag (e.g. <Run acc="SRR123"> -> "SRR123"). */
|
|
18
|
+
function extractXmlAttr(xml, tag, attr) {
|
|
19
|
+
const m = xml.match(new RegExp(`<${tag}[^>]*${attr}="([^"]*)"[^>]*>`));
|
|
20
|
+
return m ? m[1].trim() : '';
|
|
21
|
+
}
|
|
22
|
+
cli({
|
|
23
|
+
site: 'sra',
|
|
24
|
+
name: 'run',
|
|
25
|
+
description: 'Get SRA run details by accession',
|
|
26
|
+
database: 'sra',
|
|
27
|
+
strategy: Strategy.PUBLIC,
|
|
28
|
+
args: [
|
|
29
|
+
{ name: 'accession', positional: true, required: true, help: 'SRA accession (e.g. SRR1234567, SRX1234567)' },
|
|
30
|
+
],
|
|
31
|
+
columns: ['accession', 'title', 'platform', 'organism', 'strategy', 'source', 'layout', 'date'],
|
|
32
|
+
func: async (ctx, args) => {
|
|
33
|
+
const acc = String(args.accession);
|
|
34
|
+
// Step 1: esearch by accession
|
|
35
|
+
const searchResult = await ctx.fetchJson(buildEutilsUrl('esearch.fcgi', {
|
|
36
|
+
db: 'sra',
|
|
37
|
+
term: `${acc}[Accession]`,
|
|
38
|
+
retmode: 'json',
|
|
39
|
+
}));
|
|
40
|
+
const result = searchResult;
|
|
41
|
+
const esearchResult = result?.esearchresult;
|
|
42
|
+
const ids = esearchResult?.idlist ?? [];
|
|
43
|
+
if (!ids.length) {
|
|
44
|
+
throw new CliError('NOT_FOUND', `SRA entry ${acc} not found`, 'Check that the accession is correct (e.g. SRR1234567, SRX1234567)');
|
|
45
|
+
}
|
|
46
|
+
// Step 2: esummary for full details
|
|
47
|
+
const summaryResult = await ctx.fetchJson(buildEutilsUrl('esummary.fcgi', {
|
|
48
|
+
db: 'sra',
|
|
49
|
+
id: ids[0],
|
|
50
|
+
retmode: 'json',
|
|
51
|
+
}));
|
|
52
|
+
const summary = summaryResult;
|
|
53
|
+
const resultObj = summary?.result;
|
|
54
|
+
const item = (resultObj?.[ids[0]] ?? {});
|
|
55
|
+
const expXml = String(item.expxml ?? '');
|
|
56
|
+
const runsXml = String(item.runs ?? '');
|
|
57
|
+
// Determine sequencing layout from embedded XML
|
|
58
|
+
let layout = '';
|
|
59
|
+
if (expXml.includes('PAIRED'))
|
|
60
|
+
layout = 'PAIRED';
|
|
61
|
+
else if (expXml.includes('SINGLE'))
|
|
62
|
+
layout = 'SINGLE';
|
|
63
|
+
return [{
|
|
64
|
+
accession: extractXmlAttr(runsXml, 'Run', 'acc') || acc,
|
|
65
|
+
title: extractXmlTag(expXml, 'Title'),
|
|
66
|
+
platform: extractXmlAttr(expXml, 'Platform', 'instrument_model')
|
|
67
|
+
|| extractXmlTag(expXml, 'Platform'),
|
|
68
|
+
organism: extractXmlAttr(expXml, 'Organism', 'taxname')
|
|
69
|
+
|| extractXmlTag(expXml, 'Organism'),
|
|
70
|
+
strategy: extractXmlTag(expXml, 'Library_strategy')
|
|
71
|
+
|| extractXmlAttr(expXml, 'Library_descriptor', 'LIBRARY_STRATEGY'),
|
|
72
|
+
source: extractXmlTag(expXml, 'Library_source'),
|
|
73
|
+
layout,
|
|
74
|
+
date: String(item.createdate ?? ''),
|
|
75
|
+
}];
|
|
76
|
+
},
|
|
77
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sra/search — Search SRA sequencing runs.
|
|
3
|
+
*
|
|
4
|
+
* Uses the two-step esearch + esummary pattern against db=sra.
|
|
5
|
+
* SRA esummary JSON embeds XML strings in expxml and runs fields,
|
|
6
|
+
* so we extract key metadata using regex helpers.
|
|
7
|
+
*/
|
|
8
|
+
import { cli, Strategy } from '../../registry.js';
|
|
9
|
+
import { CliError } from '../../errors.js';
|
|
10
|
+
import { buildEutilsUrl } from '../_shared/eutils.js';
|
|
11
|
+
import { clamp } from '../_shared/common.js';
|
|
12
|
+
import { withMeta } from '../../types.js';
|
|
13
|
+
// ── SRA XML extraction helpers ───────────────────────────────────────────────
|
|
14
|
+
/** Extract text content of an XML tag (e.g. <Title>foo</Title> -> "foo"). */
|
|
15
|
+
function extractXmlTag(xml, tag) {
|
|
16
|
+
const m = xml.match(new RegExp(`<${tag}[^>]*>([^<]*)</${tag}>`));
|
|
17
|
+
return m ? m[1].trim() : '';
|
|
18
|
+
}
|
|
19
|
+
/** Extract an attribute value from an XML tag (e.g. <Run acc="SRR123"> -> "SRR123"). */
|
|
20
|
+
function extractXmlAttr(xml, tag, attr) {
|
|
21
|
+
const m = xml.match(new RegExp(`<${tag}[^>]*${attr}="([^"]*)"[^>]*>`));
|
|
22
|
+
return m ? m[1].trim() : '';
|
|
23
|
+
}
|
|
24
|
+
cli({
|
|
25
|
+
site: 'sra',
|
|
26
|
+
name: 'search',
|
|
27
|
+
description: 'Search SRA sequencing runs',
|
|
28
|
+
database: 'sra',
|
|
29
|
+
strategy: Strategy.PUBLIC,
|
|
30
|
+
args: [
|
|
31
|
+
{ name: 'query', positional: true, required: true, help: 'Search query (e.g. "RNA-seq human liver")' },
|
|
32
|
+
{ name: 'limit', type: 'int', default: 10, help: 'Max results (1-200)' },
|
|
33
|
+
],
|
|
34
|
+
columns: ['accession', 'title', 'platform', 'organism', 'samples', 'date'],
|
|
35
|
+
func: async (ctx, args) => {
|
|
36
|
+
const limit = clamp(Number(args.limit), 1, 200);
|
|
37
|
+
// Step 1: esearch to get SRA IDs
|
|
38
|
+
const searchResult = await ctx.fetchJson(buildEutilsUrl('esearch.fcgi', {
|
|
39
|
+
db: 'sra',
|
|
40
|
+
term: String(args.query),
|
|
41
|
+
retmax: String(limit),
|
|
42
|
+
retmode: 'json',
|
|
43
|
+
}));
|
|
44
|
+
const query = String(args.query);
|
|
45
|
+
const result = searchResult;
|
|
46
|
+
const esearchResult = result?.esearchresult;
|
|
47
|
+
const ids = esearchResult?.idlist ?? [];
|
|
48
|
+
const totalCount = Number(esearchResult?.count ?? 0);
|
|
49
|
+
if (!ids.length) {
|
|
50
|
+
throw new CliError('NOT_FOUND', 'No SRA entries found', 'Try different search terms');
|
|
51
|
+
}
|
|
52
|
+
// Step 2: esummary — SRA embeds XML strings in expxml and runs fields
|
|
53
|
+
const summaryResult = await ctx.fetchJson(buildEutilsUrl('esummary.fcgi', {
|
|
54
|
+
db: 'sra',
|
|
55
|
+
id: ids.join(','),
|
|
56
|
+
retmode: 'json',
|
|
57
|
+
}));
|
|
58
|
+
const summary = summaryResult;
|
|
59
|
+
const resultObj = summary?.result;
|
|
60
|
+
const uids = resultObj?.uids ?? [];
|
|
61
|
+
const rows = uids.map(uid => {
|
|
62
|
+
const item = (resultObj?.[uid] ?? {});
|
|
63
|
+
const expXml = String(item.expxml ?? '');
|
|
64
|
+
const runsXml = String(item.runs ?? '');
|
|
65
|
+
// Extract key fields from embedded XML using regex
|
|
66
|
+
const organism = extractXmlAttr(expXml, 'Organism', 'taxname')
|
|
67
|
+
|| extractXmlTag(expXml, 'Organism');
|
|
68
|
+
const platform = extractXmlAttr(expXml, 'Platform', 'instrument_model')
|
|
69
|
+
|| extractXmlTag(expXml, 'Platform');
|
|
70
|
+
const title = extractXmlTag(expXml, 'Title');
|
|
71
|
+
const accession = extractXmlAttr(runsXml, 'Run', 'acc') || `SRA${uid}`;
|
|
72
|
+
return {
|
|
73
|
+
accession,
|
|
74
|
+
title: title.length > 80 ? title.slice(0, 80) + '...' : title,
|
|
75
|
+
platform,
|
|
76
|
+
organism,
|
|
77
|
+
samples: String(item.total_runs ?? ''),
|
|
78
|
+
date: String(item.createdate ?? ''),
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
return withMeta(rows, { totalCount, query });
|
|
82
|
+
},
|
|
83
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* string/enrichment — Functional enrichment analysis via STRING.
|
|
3
|
+
*
|
|
4
|
+
* Given a set of proteins, returns enriched GO terms, KEGG pathways,
|
|
5
|
+
* Reactome pathways, etc.
|
|
6
|
+
*/
|
|
7
|
+
import { cli, Strategy } from '../../registry.js';
|
|
8
|
+
import { CliError } from '../../errors.js';
|
|
9
|
+
import { buildStringUrl, encodeStringIds } from '../../databases/string-db.js';
|
|
10
|
+
import { withMeta } from '../../types.js';
|
|
11
|
+
cli({
|
|
12
|
+
site: 'string',
|
|
13
|
+
name: 'enrichment',
|
|
14
|
+
description: 'Functional enrichment analysis for a gene set',
|
|
15
|
+
database: 'string',
|
|
16
|
+
strategy: Strategy.PUBLIC,
|
|
17
|
+
args: [
|
|
18
|
+
{ name: 'proteins', positional: true, required: true, help: 'Comma-separated protein/gene names (e.g. TP53,BRCA1,EGFR,MYC)' },
|
|
19
|
+
{ name: 'species', type: 'int', default: 9606, help: 'NCBI taxonomy ID (default: 9606 human)' },
|
|
20
|
+
],
|
|
21
|
+
columns: ['category', 'term', 'description', 'fdr', 'genes'],
|
|
22
|
+
func: async (ctx, args) => {
|
|
23
|
+
const proteins = String(args.proteins).split(',').map(s => s.trim()).filter(Boolean);
|
|
24
|
+
if (proteins.length < 2) {
|
|
25
|
+
throw new CliError('ARGUMENT', 'At least 2 proteins required for enrichment', 'Example: biocli string enrichment TP53,BRCA1,EGFR,MYC');
|
|
26
|
+
}
|
|
27
|
+
const species = String(args.species);
|
|
28
|
+
const data = await ctx.fetchJson(buildStringUrl('enrichment', {
|
|
29
|
+
identifiers: encodeStringIds(proteins),
|
|
30
|
+
species,
|
|
31
|
+
}));
|
|
32
|
+
if (!Array.isArray(data) || !data.length) {
|
|
33
|
+
throw new CliError('NOT_FOUND', 'No enrichment results', 'Try adding more proteins or checking names');
|
|
34
|
+
}
|
|
35
|
+
const rows = data.map(item => {
|
|
36
|
+
const inputGenes = item.inputGenes;
|
|
37
|
+
const geneList = Array.isArray(inputGenes) ? inputGenes.join(',') : String(inputGenes ?? '');
|
|
38
|
+
return {
|
|
39
|
+
category: String(item.category ?? ''),
|
|
40
|
+
term: String(item.term ?? ''),
|
|
41
|
+
description: String(item.description ?? ''),
|
|
42
|
+
fdr: Number(item.fdr ?? 1).toExponential(2),
|
|
43
|
+
genes: geneList,
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
// Sort by FDR (most significant first)
|
|
47
|
+
rows.sort((a, b) => parseFloat(a.fdr) - parseFloat(b.fdr));
|
|
48
|
+
return withMeta(rows, { totalCount: rows.length, query: proteins.join(',') });
|
|
49
|
+
},
|
|
50
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* string/network — Get protein-protein interaction network from STRING.
|
|
3
|
+
*
|
|
4
|
+
* Accepts multiple proteins (comma-separated) and returns all interactions
|
|
5
|
+
* between them.
|
|
6
|
+
*/
|
|
7
|
+
import { cli, Strategy } from '../../registry.js';
|
|
8
|
+
import { CliError } from '../../errors.js';
|
|
9
|
+
import { buildStringUrl, encodeStringIds } from '../../databases/string-db.js';
|
|
10
|
+
import { withMeta } from '../../types.js';
|
|
11
|
+
cli({
|
|
12
|
+
site: 'string',
|
|
13
|
+
name: 'network',
|
|
14
|
+
description: 'Get protein interaction network',
|
|
15
|
+
database: 'string',
|
|
16
|
+
strategy: Strategy.PUBLIC,
|
|
17
|
+
args: [
|
|
18
|
+
{ name: 'proteins', positional: true, required: true, help: 'Comma-separated protein/gene names (e.g. TP53,BRCA1,EGFR)' },
|
|
19
|
+
{ name: 'species', type: 'int', default: 9606, help: 'NCBI taxonomy ID (default: 9606 human)' },
|
|
20
|
+
{ name: 'score', type: 'int', default: 400, help: 'Minimum combined score (0-1000)' },
|
|
21
|
+
],
|
|
22
|
+
columns: ['proteinA', 'proteinB', 'score', 'experimentalScore', 'databaseScore'],
|
|
23
|
+
func: async (ctx, args) => {
|
|
24
|
+
const proteins = String(args.proteins).split(',').map(s => s.trim()).filter(Boolean);
|
|
25
|
+
if (proteins.length < 2) {
|
|
26
|
+
throw new CliError('ARGUMENT', 'At least 2 proteins required for a network', 'Example: biocli string network TP53,BRCA1,EGFR');
|
|
27
|
+
}
|
|
28
|
+
const species = String(args.species);
|
|
29
|
+
const score = String(args.score);
|
|
30
|
+
const data = await ctx.fetchJson(buildStringUrl('network', {
|
|
31
|
+
identifiers: encodeStringIds(proteins),
|
|
32
|
+
species,
|
|
33
|
+
required_score: score,
|
|
34
|
+
}));
|
|
35
|
+
if (!Array.isArray(data) || !data.length) {
|
|
36
|
+
throw new CliError('NOT_FOUND', `No interactions found between ${proteins.join(', ')}`, 'Try lowering --score or adding more proteins');
|
|
37
|
+
}
|
|
38
|
+
const rows = data.map(item => ({
|
|
39
|
+
proteinA: String(item.preferredName_A ?? ''),
|
|
40
|
+
proteinB: String(item.preferredName_B ?? ''),
|
|
41
|
+
score: Number(item.score ?? 0),
|
|
42
|
+
experimentalScore: Number(item.escore ?? 0),
|
|
43
|
+
databaseScore: Number(item.dscore ?? 0),
|
|
44
|
+
}));
|
|
45
|
+
return withMeta(rows, { totalCount: rows.length, query: proteins.join(',') });
|
|
46
|
+
},
|
|
47
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* string/partners — Find interaction partners for a protein in STRING.
|
|
3
|
+
*/
|
|
4
|
+
import { cli, Strategy } from '../../registry.js';
|
|
5
|
+
import { CliError } from '../../errors.js';
|
|
6
|
+
import { buildStringUrl } from '../../databases/string-db.js';
|
|
7
|
+
import { withMeta } from '../../types.js';
|
|
8
|
+
cli({
|
|
9
|
+
site: 'string',
|
|
10
|
+
name: 'partners',
|
|
11
|
+
description: 'Find protein interaction partners',
|
|
12
|
+
database: 'string',
|
|
13
|
+
strategy: Strategy.PUBLIC,
|
|
14
|
+
args: [
|
|
15
|
+
{ name: 'protein', positional: true, required: true, help: 'Protein/gene name (e.g. TP53)' },
|
|
16
|
+
{ name: 'limit', type: 'int', default: 10, help: 'Max partners (1-50)' },
|
|
17
|
+
{ name: 'species', type: 'int', default: 9606, help: 'NCBI taxonomy ID (default: 9606 human)' },
|
|
18
|
+
{ name: 'score', type: 'int', default: 400, help: 'Minimum combined score (0-1000)' },
|
|
19
|
+
],
|
|
20
|
+
columns: ['partnerA', 'partnerB', 'score', 'experimentalScore', 'databaseScore'],
|
|
21
|
+
func: async (ctx, args) => {
|
|
22
|
+
const protein = String(args.protein);
|
|
23
|
+
const species = String(args.species);
|
|
24
|
+
const limit = Math.max(1, Math.min(Number(args.limit), 50));
|
|
25
|
+
const score = String(args.score);
|
|
26
|
+
const data = await ctx.fetchJson(buildStringUrl('interaction_partners', {
|
|
27
|
+
identifiers: protein,
|
|
28
|
+
species,
|
|
29
|
+
limit: String(limit),
|
|
30
|
+
required_score: score,
|
|
31
|
+
}));
|
|
32
|
+
if (!Array.isArray(data) || !data.length) {
|
|
33
|
+
throw new CliError('NOT_FOUND', `No interaction partners found for ${protein}`, 'Check the protein name or lower --score threshold');
|
|
34
|
+
}
|
|
35
|
+
const rows = data.map(item => ({
|
|
36
|
+
partnerA: String(item.preferredName_A ?? ''),
|
|
37
|
+
partnerB: String(item.preferredName_B ?? ''),
|
|
38
|
+
score: Number(item.score ?? 0),
|
|
39
|
+
experimentalScore: Number(item.escore ?? 0),
|
|
40
|
+
databaseScore: Number(item.dscore ?? 0),
|
|
41
|
+
}));
|
|
42
|
+
return withMeta(rows, { totalCount: rows.length, query: protein });
|
|
43
|
+
},
|
|
44
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* taxonomy/lookup — Look up NCBI Taxonomy by name or ID.
|
|
3
|
+
*
|
|
4
|
+
* Uses esearch (when given a name) to find taxonomy IDs,
|
|
5
|
+
* then efetch (XML) to retrieve full taxonomic metadata.
|
|
6
|
+
* If given a numeric ID, skips directly to efetch.
|
|
7
|
+
*/
|
|
8
|
+
import { cli, Strategy } from '../../registry.js';
|
|
9
|
+
import { CliError } from '../../errors.js';
|
|
10
|
+
import { buildEutilsUrl } from '../_shared/eutils.js';
|
|
11
|
+
cli({
|
|
12
|
+
site: 'taxonomy',
|
|
13
|
+
name: 'lookup',
|
|
14
|
+
description: 'Look up NCBI Taxonomy by name or ID',
|
|
15
|
+
database: 'taxonomy',
|
|
16
|
+
strategy: Strategy.PUBLIC,
|
|
17
|
+
args: [
|
|
18
|
+
{ name: 'query', positional: true, required: true, help: 'Organism name or Taxonomy ID (e.g. "Homo sapiens", 9606)' },
|
|
19
|
+
{ name: 'limit', type: 'int', default: 5, help: 'Max results' },
|
|
20
|
+
],
|
|
21
|
+
columns: ['taxId', 'name', 'commonName', 'rank', 'division', 'lineage'],
|
|
22
|
+
func: async (ctx, args) => {
|
|
23
|
+
const query = String(args.query);
|
|
24
|
+
const limit = Math.max(1, Math.min(Number(args.limit), 20));
|
|
25
|
+
// If numeric, treat as taxonomy ID; otherwise search by name
|
|
26
|
+
let ids;
|
|
27
|
+
if (/^\d+$/.test(query)) {
|
|
28
|
+
ids = [query];
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
const searchResult = await ctx.fetchJson(buildEutilsUrl('esearch.fcgi', {
|
|
32
|
+
db: 'taxonomy', term: query, retmax: String(limit), retmode: 'json',
|
|
33
|
+
}));
|
|
34
|
+
ids = searchResult?.esearchresult?.idlist ?? [];
|
|
35
|
+
if (!ids.length)
|
|
36
|
+
throw new CliError('NOT_FOUND', `Taxonomy entry "${query}" not found`);
|
|
37
|
+
}
|
|
38
|
+
// efetch for taxonomy returns XML
|
|
39
|
+
const xmlData = await ctx.fetchXml(buildEutilsUrl('efetch.fcgi', {
|
|
40
|
+
db: 'taxonomy', id: ids.join(','), retmode: 'xml',
|
|
41
|
+
}));
|
|
42
|
+
// Parse taxonomy XML: TaxaSet > Taxon
|
|
43
|
+
const taxaSet = xmlData?.TaxaSet?.Taxon;
|
|
44
|
+
const taxa = Array.isArray(taxaSet) ? taxaSet : taxaSet ? [taxaSet] : [];
|
|
45
|
+
return taxa.map((taxon) => ({
|
|
46
|
+
taxId: String(taxon?.TaxId ?? ''),
|
|
47
|
+
name: String(taxon?.ScientificName ?? ''),
|
|
48
|
+
commonName: String(taxon?.OtherNames?.CommonName ?? taxon?.OtherNames?.GenbankCommonName ?? ''),
|
|
49
|
+
rank: String(taxon?.Rank ?? ''),
|
|
50
|
+
division: String(taxon?.Division ?? ''),
|
|
51
|
+
lineage: String(taxon?.Lineage ?? '').split('; ').slice(-4).join(' > '),
|
|
52
|
+
}));
|
|
53
|
+
},
|
|
54
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* uniprot/fetch — Fetch a UniProt entry by accession.
|
|
3
|
+
*
|
|
4
|
+
* Returns detailed protein information including function, GO terms,
|
|
5
|
+
* subcellular location, domains, and cross-references.
|
|
6
|
+
*/
|
|
7
|
+
import { cli, Strategy } from '../../registry.js';
|
|
8
|
+
import { CliError } from '../../errors.js';
|
|
9
|
+
import { buildUniprotUrl } from '../../databases/uniprot.js';
|
|
10
|
+
cli({
|
|
11
|
+
site: 'uniprot',
|
|
12
|
+
name: 'fetch',
|
|
13
|
+
description: 'Get UniProt protein details by accession',
|
|
14
|
+
database: 'uniprot',
|
|
15
|
+
strategy: Strategy.PUBLIC,
|
|
16
|
+
args: [
|
|
17
|
+
{ name: 'accession', positional: true, required: true, help: 'UniProt accession (e.g. P04637)' },
|
|
18
|
+
],
|
|
19
|
+
columns: ['accession', 'gene', 'protein', 'organism', 'function', 'subcellularLocation', 'goTerms', 'domains'],
|
|
20
|
+
func: async (ctx, args) => {
|
|
21
|
+
const accession = String(args.accession).trim().toUpperCase();
|
|
22
|
+
const entry = await ctx.fetchJson(buildUniprotUrl(`/uniprotkb/${accession}`, { format: 'json' }));
|
|
23
|
+
if (!entry || !entry.primaryAccession) {
|
|
24
|
+
throw new CliError('NOT_FOUND', `UniProt entry ${accession} not found`, 'Check the accession is correct');
|
|
25
|
+
}
|
|
26
|
+
// Extract gene name
|
|
27
|
+
const genes = entry.genes;
|
|
28
|
+
const primaryGene = genes?.[0];
|
|
29
|
+
const geneName = primaryGene?.geneName;
|
|
30
|
+
// Extract protein name
|
|
31
|
+
const proteinDesc = entry.proteinDescription;
|
|
32
|
+
const recName = proteinDesc?.recommendedName;
|
|
33
|
+
const fullName = recName?.fullName;
|
|
34
|
+
// Extract organism
|
|
35
|
+
const org = entry.organism;
|
|
36
|
+
// Extract function from comments
|
|
37
|
+
const comments = (entry.comments ?? []);
|
|
38
|
+
const functionComment = comments.find(c => c.commentType === 'FUNCTION');
|
|
39
|
+
const functionTexts = (functionComment?.texts ?? []);
|
|
40
|
+
const functionText = functionTexts.map(t => String(t.value ?? '')).join(' ');
|
|
41
|
+
// Extract subcellular location
|
|
42
|
+
const locComment = comments.find(c => c.commentType === 'SUBCELLULAR LOCATION');
|
|
43
|
+
const locNote = locComment?.subcellularLocations;
|
|
44
|
+
const locations = (locNote ?? []).map(l => {
|
|
45
|
+
const loc = l.location;
|
|
46
|
+
return String(loc?.value ?? '');
|
|
47
|
+
}).filter(Boolean);
|
|
48
|
+
// Extract GO terms from cross-references
|
|
49
|
+
const xrefs = (entry.uniProtKBCrossReferences ?? []);
|
|
50
|
+
const goTerms = xrefs
|
|
51
|
+
.filter(x => x.database === 'GO')
|
|
52
|
+
.map(x => {
|
|
53
|
+
const props = (x.properties ?? []);
|
|
54
|
+
const termProp = props.find(p => p.key === 'GoTerm');
|
|
55
|
+
const term = String(termProp?.value ?? '');
|
|
56
|
+
// GO terms are formatted as "C:nucleus" or "F:DNA binding" or "P:apoptosis"
|
|
57
|
+
const [aspect, name] = term.includes(':') ? [term[0], term.slice(2)] : ['', term];
|
|
58
|
+
const aspectMap = { C: 'CC', F: 'MF', P: 'BP' };
|
|
59
|
+
return `${aspectMap[aspect] ?? aspect}:${name}`;
|
|
60
|
+
});
|
|
61
|
+
// Extract domains from features
|
|
62
|
+
const features = (entry.features ?? []);
|
|
63
|
+
const domains = features
|
|
64
|
+
.filter(f => f.type === 'Domain')
|
|
65
|
+
.map(f => {
|
|
66
|
+
const loc = f.location;
|
|
67
|
+
const start = loc?.start?.value ?? '';
|
|
68
|
+
const end = loc?.end?.value ?? '';
|
|
69
|
+
return `${f.description ?? ''}(${start}-${end})`;
|
|
70
|
+
});
|
|
71
|
+
return [{
|
|
72
|
+
accession: String(entry.primaryAccession),
|
|
73
|
+
gene: String(geneName?.value ?? ''),
|
|
74
|
+
protein: String(fullName?.value ?? ''),
|
|
75
|
+
organism: String(org?.scientificName ?? ''),
|
|
76
|
+
function: functionText,
|
|
77
|
+
subcellularLocation: locations.join(', '),
|
|
78
|
+
goTerms: goTerms.join('; '),
|
|
79
|
+
domains: domains.join('; '),
|
|
80
|
+
}];
|
|
81
|
+
},
|
|
82
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* uniprot/search — Search UniProt KB by gene name, protein name, or keyword.
|
|
3
|
+
*
|
|
4
|
+
* Uses the UniProt REST API search endpoint with JSON output.
|
|
5
|
+
*/
|
|
6
|
+
import { cli, Strategy } from '../../registry.js';
|
|
7
|
+
import { CliError } from '../../errors.js';
|
|
8
|
+
import { buildUniprotUrl } from '../../databases/uniprot.js';
|
|
9
|
+
import { withMeta } from '../../types.js';
|
|
10
|
+
cli({
|
|
11
|
+
site: 'uniprot',
|
|
12
|
+
name: 'search',
|
|
13
|
+
description: 'Search UniProt proteins',
|
|
14
|
+
database: 'uniprot',
|
|
15
|
+
strategy: Strategy.PUBLIC,
|
|
16
|
+
args: [
|
|
17
|
+
{ name: 'query', positional: true, required: true, help: 'Search query (e.g. "TP53", "kinase human")' },
|
|
18
|
+
{ name: 'limit', type: 'int', default: 10, help: 'Max results (1-100)' },
|
|
19
|
+
{ name: 'organism', default: '9606', help: 'Organism taxonomy ID (default: 9606 for human)' },
|
|
20
|
+
{ name: 'reviewed', default: 'true', help: 'Only Swiss-Prot reviewed entries (true/false)' },
|
|
21
|
+
],
|
|
22
|
+
columns: ['accession', 'gene', 'protein', 'organism', 'length', 'reviewed'],
|
|
23
|
+
func: async (ctx, args) => {
|
|
24
|
+
const limit = Math.max(1, Math.min(Number(args.limit), 100));
|
|
25
|
+
const query = String(args.query);
|
|
26
|
+
const organism = String(args.organism);
|
|
27
|
+
const reviewed = String(args.reviewed) !== 'false';
|
|
28
|
+
// Build UniProt search query
|
|
29
|
+
let searchQuery = query;
|
|
30
|
+
if (organism && organism !== 'all') {
|
|
31
|
+
searchQuery += ` AND organism_id:${organism}`;
|
|
32
|
+
}
|
|
33
|
+
if (reviewed) {
|
|
34
|
+
searchQuery += ' AND reviewed:true';
|
|
35
|
+
}
|
|
36
|
+
const data = await ctx.fetchJson(buildUniprotUrl('/uniprotkb/search', {
|
|
37
|
+
query: searchQuery,
|
|
38
|
+
format: 'json',
|
|
39
|
+
size: String(limit),
|
|
40
|
+
fields: 'accession,gene_names,protein_name,organism_name,length,reviewed',
|
|
41
|
+
}));
|
|
42
|
+
const results = (data?.results ?? []);
|
|
43
|
+
if (!results.length) {
|
|
44
|
+
throw new CliError('NOT_FOUND', `No UniProt entries found for "${query}"`, 'Try a different search term or set --organism all');
|
|
45
|
+
}
|
|
46
|
+
const rows = results.map(entry => {
|
|
47
|
+
const genes = entry.genes;
|
|
48
|
+
const primaryGene = genes?.[0];
|
|
49
|
+
const geneName = primaryGene?.geneName;
|
|
50
|
+
const proteinDesc = entry.proteinDescription;
|
|
51
|
+
const recName = proteinDesc?.recommendedName;
|
|
52
|
+
const fullName = recName?.fullName;
|
|
53
|
+
const org = entry.organism;
|
|
54
|
+
return {
|
|
55
|
+
accession: String(entry.primaryAccession ?? ''),
|
|
56
|
+
gene: String(geneName?.value ?? ''),
|
|
57
|
+
protein: String(fullName?.value ?? ''),
|
|
58
|
+
organism: String(org?.scientificName ?? ''),
|
|
59
|
+
length: Number(entry.sequence && entry.sequence.length || 0),
|
|
60
|
+
reviewed: entry.entryType === 'UniProtKB reviewed (Swiss-Prot)' ? 'yes' : 'no',
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
return withMeta(rows, { totalCount: Number(data?.totalCount ?? rows.length), query });
|
|
64
|
+
},
|
|
65
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* uniprot/sequence — Download protein sequence in FASTA format.
|
|
3
|
+
*
|
|
4
|
+
* Uses UniProt REST API with format=fasta to retrieve the canonical
|
|
5
|
+
* protein sequence for a given accession.
|
|
6
|
+
*/
|
|
7
|
+
import { cli, Strategy } from '../../registry.js';
|
|
8
|
+
import { CliError } from '../../errors.js';
|
|
9
|
+
import { buildUniprotUrl } from '../../databases/uniprot.js';
|
|
10
|
+
import { writeFileSync } from 'node:fs';
|
|
11
|
+
cli({
|
|
12
|
+
site: 'uniprot',
|
|
13
|
+
name: 'sequence',
|
|
14
|
+
description: 'Download protein sequence in FASTA format',
|
|
15
|
+
database: 'uniprot',
|
|
16
|
+
strategy: Strategy.PUBLIC,
|
|
17
|
+
args: [
|
|
18
|
+
{ name: 'accession', positional: true, required: true, help: 'UniProt accession (e.g. P04637) or multiple comma-separated' },
|
|
19
|
+
{ name: 'output', help: 'Output file path (default: stdout)' },
|
|
20
|
+
],
|
|
21
|
+
columns: ['content'],
|
|
22
|
+
defaultFormat: 'plain',
|
|
23
|
+
func: async (ctx, args) => {
|
|
24
|
+
const input = String(args.accession).trim();
|
|
25
|
+
const outputFile = args.output ? String(args.output) : undefined;
|
|
26
|
+
const accessions = input.split(',').map(s => s.trim().toUpperCase()).filter(Boolean);
|
|
27
|
+
if (!accessions.length) {
|
|
28
|
+
throw new CliError('ARGUMENT', 'At least one UniProt accession is required');
|
|
29
|
+
}
|
|
30
|
+
const fastaChunks = [];
|
|
31
|
+
for (const acc of accessions) {
|
|
32
|
+
const fasta = await ctx.fetchText(buildUniprotUrl(`/uniprotkb/${acc}.fasta`));
|
|
33
|
+
if (!fasta || !fasta.startsWith('>')) {
|
|
34
|
+
throw new CliError('NOT_FOUND', `UniProt entry ${acc} not found or has no sequence`, 'Check the accession is correct (e.g. P04637 for TP53_HUMAN)');
|
|
35
|
+
}
|
|
36
|
+
fastaChunks.push(fasta.trim());
|
|
37
|
+
}
|
|
38
|
+
const allFasta = fastaChunks.join('\n');
|
|
39
|
+
if (outputFile) {
|
|
40
|
+
writeFileSync(outputFile, allFasta + '\n', 'utf-8');
|
|
41
|
+
const seqCount = fastaChunks.length;
|
|
42
|
+
const totalLength = allFasta.split('\n')
|
|
43
|
+
.filter(l => !l.startsWith('>'))
|
|
44
|
+
.join('')
|
|
45
|
+
.replace(/\s/g, '')
|
|
46
|
+
.length;
|
|
47
|
+
return [{ content: `Saved ${seqCount} sequence(s) to ${outputFile} (${totalLength} aa total)` }];
|
|
48
|
+
}
|
|
49
|
+
return [{ content: allFasta }];
|
|
50
|
+
},
|
|
51
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Commander adapter: bridges Registry commands to Commander subcommands.
|
|
3
|
+
*
|
|
4
|
+
* This is a THIN adapter — it only handles:
|
|
5
|
+
* 1. Commander arg/option registration
|
|
6
|
+
* 2. Collecting kwargs from Commander's action args
|
|
7
|
+
* 3. Calling executeCommand (which handles HttpContext, validation, etc.)
|
|
8
|
+
* 4. Rendering output and errors
|
|
9
|
+
*
|
|
10
|
+
* All execution logic lives in execution.ts.
|
|
11
|
+
*/
|
|
12
|
+
import { Command } from 'commander';
|
|
13
|
+
import { type CliCommand } from './registry.js';
|
|
14
|
+
export declare function normalizeArgValue(argType: string | undefined, value: unknown, name: string): unknown;
|
|
15
|
+
/**
|
|
16
|
+
* Register a single CliCommand as a Commander subcommand.
|
|
17
|
+
*/
|
|
18
|
+
export declare function registerCommandToProgram(siteCmd: Command, cmd: CliCommand): void;
|
|
19
|
+
/**
|
|
20
|
+
* Iterate the registry, group commands by site, and register each
|
|
21
|
+
* as a Commander subcommand under a site parent command.
|
|
22
|
+
*/
|
|
23
|
+
export declare function registerAllCommands(program: Command): void;
|
|
24
|
+
/**
|
|
25
|
+
* Map any thrown value to a Unix process exit code.
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolveExitCode(err: unknown): number;
|