m3triq 0.2.10 → 0.2.12

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/dist/cli.js CHANGED
@@ -10,6 +10,7 @@ import { registerSessionCommands } from './commands/sessions.js';
10
10
  import { registerJobCommands } from './commands/jobs.js';
11
11
  import { registerDockingCommands } from './commands/docking.js';
12
12
  import { registerPredictCommands } from './commands/predict.js';
13
+ import { registerMsaCommands } from './commands/msa.js';
13
14
  import { registerChemblCommands } from './commands/chembl.js';
14
15
  import { registerSandboxCommands } from './commands/sandbox.js';
15
16
  import { registerDesignCommands } from './commands/design.js';
@@ -24,7 +25,7 @@ const program = new Command();
24
25
  program
25
26
  .name('m3t')
26
27
  .description('M3TRIQ — protein-ligand analysis from the terminal')
27
- .version('0.2.10')
28
+ .version('0.2.12')
28
29
  .option('--json', 'Output as JSON (machine-readable)')
29
30
  .hook('preAction', (thisCommand) => {
30
31
  const opts = thisCommand.optsWithGlobals();
@@ -37,6 +38,7 @@ registerSessionCommands(program);
37
38
  registerJobCommands(program);
38
39
  registerDockingCommands(program);
39
40
  registerPredictCommands(program);
41
+ registerMsaCommands(program);
40
42
  registerChemblCommands(program);
41
43
  registerSandboxCommands(program);
42
44
  registerDesignCommands(program);
package/dist/client.d.ts CHANGED
@@ -81,9 +81,33 @@ export declare class M3triqClient {
81
81
  }): Promise<{
82
82
  job_id: string;
83
83
  }>;
84
+ createMsaJob(params: {
85
+ project_id: string;
86
+ chains: {
87
+ id: string;
88
+ sequence: string;
89
+ }[];
90
+ depth: 'fast' | 'deep' | 'exhaustive' | 'custom';
91
+ pair: boolean;
92
+ templates?: boolean;
93
+ custom?: {
94
+ databases: string[];
95
+ max_sequences?: number;
96
+ pair?: boolean;
97
+ };
98
+ title?: string;
99
+ }): Promise<{
100
+ job_id: string;
101
+ task_id?: string;
102
+ depth?: string;
103
+ n_chains?: number;
104
+ }>;
84
105
  createBoltz2Job(params: Boltz2JobParams): Promise<{
85
106
  job_id: string;
86
107
  }>;
108
+ createOpenfold3Job(params: Boltz2JobParams): Promise<{
109
+ job_id: string;
110
+ }>;
87
111
  createSandboxJob(params: SandboxParams): Promise<{
88
112
  job_id: string;
89
113
  }>;
package/dist/client.js CHANGED
@@ -126,6 +126,30 @@ export class M3triqClient {
126
126
  async createEsmfold2BatchJob(params) {
127
127
  return this.request('POST', '/api/jobs/create_esmfold2_batch/', params);
128
128
  }
129
+ async createMsaJob(params) {
130
+ // MSA generation is an async job created via the internal prediction endpoint
131
+ // (same path Boltz-2 uses). The server's is_msa branch keys off job_type.
132
+ const url = `${this.baseUrl}/api/jobs/create_prediction_internal/`;
133
+ const res = await fetch(url, {
134
+ method: 'POST',
135
+ headers: { ...this.headers, 'X-Internal-Service': 'true' },
136
+ body: JSON.stringify({
137
+ project_id: params.project_id,
138
+ job_type: 'msa_generation',
139
+ title: params.title,
140
+ chains: params.chains,
141
+ depth: params.depth,
142
+ pair: params.pair,
143
+ templates: params.templates ?? false,
144
+ ...(params.custom ? { custom: params.custom } : {}),
145
+ }),
146
+ });
147
+ if (!res.ok) {
148
+ const text = await res.text();
149
+ throw new Error(`API error ${res.status}: ${text.substring(0, 200)}`);
150
+ }
151
+ return res.json();
152
+ }
129
153
  async createBoltz2Job(params) {
130
154
  // Boltz-2 prediction is run client-side via the agents MCP, then saved here as a completed job.
131
155
  // Mirrors the RFantibody internal flow but with status='completed' and result_data already populated.
@@ -150,6 +174,27 @@ export class M3triqClient {
150
174
  }
151
175
  return res.json();
152
176
  }
177
+ async createOpenfold3Job(params) {
178
+ // OpenFold3 prediction runs client-side via the agents MCP, then is saved here
179
+ // as a completed job (same internal path Boltz-2 uses; server is_openfold3 branch).
180
+ const url = `${this.baseUrl}/api/jobs/create_prediction_internal/`;
181
+ const res = await fetch(url, {
182
+ method: 'POST',
183
+ headers: { ...this.headers, 'X-Internal-Service': 'true' },
184
+ body: JSON.stringify({
185
+ project_id: params.project_id,
186
+ job_type: 'openfold3_prediction',
187
+ title: params.title,
188
+ description: params.description ?? '',
189
+ result_data: params.result_data,
190
+ }),
191
+ });
192
+ if (!res.ok) {
193
+ const text = await res.text();
194
+ throw new Error(`API error ${res.status}: ${text.substring(0, 200)}`);
195
+ }
196
+ return res.json();
197
+ }
153
198
  async createSandboxJob(params) {
154
199
  return this.request('POST', '/api/jobs/create_sandbox_job/', params);
155
200
  }
@@ -0,0 +1,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerMsaCommands(program: Command): void;
@@ -0,0 +1,116 @@
1
+ import fs from 'node:fs';
2
+ import { createClient, getConsoleUrl } from '../cli.js';
3
+ import { requireProject } from '../config.js';
4
+ import { output } from '../output.js';
5
+ import { jobUrl, maybeOpenBrowser } from '../url.js';
6
+ // m3t msa — generate a multiple-sequence alignment (a3m) for one or more chains,
7
+ // all on the self-hosted ColabFold MSA-search engine (GPU-MMseqs2):
8
+ // fast = UniRef30, shallow, no pairing (quick triage)
9
+ // deep = UniRef30, full depth + species pairing (complexes/hard families)
10
+ // exhaustive = deep + the metagenomic envDB (remote-homology / orphan families)
11
+ // custom = you pick the databases (+ max_sequences / pairing)
12
+ // --templates also searches the PDB and saves per-chain structural templates.
13
+ // Produces a downloadable .a3m artifact per chain (+ templates_<chain>.json).
14
+ const MSA_DEPTHS = ['fast', 'deep', 'exhaustive', 'custom'];
15
+ export function registerMsaCommands(program) {
16
+ program.command('msa')
17
+ .description('Generate a multiple-sequence alignment (a3m) — per-chain and (for complexes) species-paired')
18
+ .option('--chain <id:seq>', 'Chain as "id:sequence" or "id:@path". Repeatable for multi-chain (paired) MSAs.', collectArg, [])
19
+ .option('--sequence <seq>', 'Single-chain shortcut (chain id "A"). Use --chain for multi-chain.')
20
+ .option('--depth <tier>', 'MSA depth: fast | deep | exhaustive | custom (see `m3t msa --help`)', 'fast')
21
+ .option('--templates', 'Also search the PDB for structural templates; saves a templates_<chain>.json per chain.')
22
+ .option('--databases <csv>', 'For --depth custom: comma-separated database ids (e.g. "uniref30,envdb"). See get_msa_capabilities.')
23
+ .option('--max-sequences <n>', 'For --depth custom: alignment depth/breadth cap.', (v) => parseInt(v, 10))
24
+ .option('--pair', 'Compute species-paired MSA across chains (complexes). Default on for ≥2 distinct chains.')
25
+ .option('--no-pair', 'Skip paired MSA (per-chain only).')
26
+ .option('--name <name>', 'Custom job title')
27
+ .action(async (opts) => {
28
+ await runMsa(opts);
29
+ });
30
+ }
31
+ async function runMsa(opts) {
32
+ const depth = (opts.depth || 'fast').toLowerCase();
33
+ if (!MSA_DEPTHS.includes(depth)) {
34
+ process.stderr.write(`Error: --depth must be one of ${MSA_DEPTHS.join(' | ')}, got "${opts.depth}".\n`);
35
+ process.exit(1);
36
+ }
37
+ // Build the custom config when depth=custom.
38
+ let custom;
39
+ if (depth === 'custom') {
40
+ const databases = (opts.databases || '').split(',').map(s => s.trim()).filter(Boolean);
41
+ if (databases.length === 0) {
42
+ process.stderr.write('Error: --depth custom requires --databases (e.g. --databases "uniref30,envdb").\n');
43
+ process.exit(1);
44
+ }
45
+ custom = { databases, max_sequences: opts.maxSequences, pair: opts.pair };
46
+ }
47
+ const chains = [];
48
+ for (const raw of opts.chain) {
49
+ const colon = raw.indexOf(':');
50
+ if (colon <= 0) {
51
+ process.stderr.write(`Error: --chain must be "id:sequence" or "id:@path", got "${raw}".\n`);
52
+ process.exit(1);
53
+ }
54
+ const id = raw.slice(0, colon).trim();
55
+ const seq = resolveSequence(raw.slice(colon + 1));
56
+ if (!id) {
57
+ process.stderr.write('Error: --chain id cannot be empty.\n');
58
+ process.exit(1);
59
+ }
60
+ chains.push({ id, sequence: seq });
61
+ }
62
+ if (chains.length === 0 && opts.sequence) {
63
+ chains.push({ id: 'A', sequence: resolveSequence(opts.sequence) });
64
+ }
65
+ if (chains.length === 0) {
66
+ process.stderr.write('Error: provide at least one --chain "id:seq" or --sequence "<seq>".\n');
67
+ process.exit(1);
68
+ }
69
+ // pair only matters for ≥2 distinct chains; let the server make the final call too.
70
+ const distinct = new Set(chains.map(c => c.sequence));
71
+ const pair = opts.pair && chains.length >= 2 && distinct.size >= 2;
72
+ const templates = !!opts.templates;
73
+ const project = requireProject();
74
+ const client = createClient();
75
+ const consoleUrl = getConsoleUrl();
76
+ const result = await client.createMsaJob({
77
+ project_id: project.id,
78
+ chains,
79
+ depth: depth,
80
+ pair,
81
+ templates,
82
+ custom,
83
+ title: opts.name,
84
+ });
85
+ const url = jobUrl(consoleUrl, project.id, result.job_id);
86
+ maybeOpenBrowser(url);
87
+ const timeEst = depth === 'exhaustive'
88
+ ? '~4-6 min (ColabFold UniRef30 + envDB on 2 T4s; +cold start if scaled to zero)'
89
+ : depth === 'fast'
90
+ ? '~2-4 min (ColabFold UniRef30; +cold start if scaled to zero)'
91
+ : '~3-5 min (ColabFold UniRef30 + paired; +cold start if scaled to zero)';
92
+ const extras = [pair ? 'paired' : '', templates ? '+templates' : ''].filter(Boolean).join(', ');
93
+ const lines = [
94
+ `MSA job queued (${chains.length} chain${chains.length > 1 ? 's' : ''}, depth=${depth}${extras ? `, ${extras}` : ''})`,
95
+ `Job ID: ${result.job_id.substring(0, 8)}`,
96
+ `Estimated: ${timeEst}`,
97
+ `Output: one .a3m artifact per chain${templates ? ' (+ templates_<chain>.json)' : ''} (Files tab)`,
98
+ `View: ${url}`,
99
+ ];
100
+ output({ job_id: result.job_id, n_chains: chains.length, depth, pair, templates, url }, lines.join('\n'));
101
+ }
102
+ function collectArg(value, prev) {
103
+ return [...prev, value];
104
+ }
105
+ function resolveSequence(input) {
106
+ // @path → read file (FASTA-aware: strip header/whitespace)
107
+ if (input.startsWith('@')) {
108
+ const path = input.slice(1);
109
+ if (!fs.existsSync(path)) {
110
+ throw new Error(`Sequence file not found: ${path}`);
111
+ }
112
+ const raw = fs.readFileSync(path, 'utf-8');
113
+ return raw.split('\n').filter(line => !line.startsWith('>')).join('').replace(/\s+/g, '').toUpperCase();
114
+ }
115
+ return input.replace(/\s+/g, '').toUpperCase();
116
+ }
@@ -13,13 +13,19 @@ export function registerPredictCommands(program) {
13
13
  .action(async (sequence, opts) => {
14
14
  await runPredict(sequence, 'esmfold', opts.name);
15
15
  });
16
- // m3t predict alphafold2
16
+ // m3t predict alphafold2 — RETIRED (the alphafold2-nim VM was decommissioned in
17
+ // favour of ESMFold2). Kept as a signpost so scripts get a clear redirect instead
18
+ // of a job that silently fails on the deleted backend.
17
19
  predict.command('alphafold2')
18
- .argument('<sequence>', 'Amino acid sequence (single letter codes, max 2048aa)')
20
+ .argument('[sequence]', 'Amino acid sequence (single letter codes)')
19
21
  .option('--name <name>', 'Name for the prediction')
20
- .description('High-accuracy prediction with AlphaFold2 (~15-20 min, varies with length; scale-to-zero VM)')
21
- .action(async (sequence, opts) => {
22
- await runPredict(sequence, 'alphafold2', opts.name);
22
+ .description('[RETIRED use `predict esmfold2`] AlphaFold2 single-sequence prediction')
23
+ .action(async () => {
24
+ process.stderr.write('AlphaFold2 has been retired (the self-hosted backend was decommissioned).\n' +
25
+ 'Use ESMFold2 instead — higher accuracy and it folds complexes too:\n' +
26
+ ' m3t predict esmfold2 --sequence <seq> # monomer\n' +
27
+ ' m3t predict esmfold2 --chain A:<seq> --chain B:<seq> # complex\n');
28
+ process.exit(2);
23
29
  });
24
30
  // m3t predict esmfold2 — monomer OR multi-chain (antibody-antigen, PPI)
25
31
  predict.command('esmfold2')
@@ -68,6 +74,20 @@ export function registerPredictCommands(program) {
68
74
  .action(async (opts) => {
69
75
  await runBoltz2(opts);
70
76
  });
77
+ // m3t predict openfold3
78
+ predict.command('openfold3')
79
+ .description('Predict biomolecular complex (protein + DNA/RNA + ligand) with OpenFold3 / AlphaFold3-class (NVIDIA NIM). Real MSAs + optional PDB templates.')
80
+ .option('--protein <seq>', 'Protein sequence (or @path to file). Repeatable for multi-chain complexes.', collectArg, [])
81
+ .option('--dna <seq>', 'DNA sequence (or @path to file). Repeatable.', collectArg, [])
82
+ .option('--rna <seq>', 'RNA sequence (or @path to file). Repeatable.', collectArg, [])
83
+ .option('--ligand <smiles_or_ccd>', 'Ligand as SMILES or CCD code (e.g. ATP). Repeatable.', collectArg, [])
84
+ .option('--depth <tier>', 'MSA depth for protein chains: fast | deep | exhaustive | none (default fast)', 'fast')
85
+ .option('--templates', 'Seed the fold with PDB structural templates per protein chain (OpenFold3 self-aligns).')
86
+ .option('--samples <n>', 'Number of diffusion samples (1-25, default: 1)', (v) => parseInt(v, 10), 1)
87
+ .option('--name <name>', 'Custom job title')
88
+ .action(async (opts) => {
89
+ await runOpenfold3(opts);
90
+ });
71
91
  }
72
92
  async function runPredict(sequence, method, name) {
73
93
  const project = requireProject();
@@ -165,6 +185,94 @@ async function runBoltz2(opts) {
165
185
  url,
166
186
  }, lines.join('\n'));
167
187
  }
188
+ async function runOpenfold3(opts) {
189
+ const depth = (opts.depth || 'fast').toLowerCase();
190
+ const OF3_DEPTHS = ['fast', 'deep', 'exhaustive', 'custom', 'none'];
191
+ if (!OF3_DEPTHS.includes(depth)) {
192
+ process.stderr.write(`Error: --depth must be one of ${OF3_DEPTHS.join(' | ')}, got "${opts.depth}".\n`);
193
+ process.exit(1);
194
+ }
195
+ // Build the OpenFold3 molecules array (proteins get chain ids A, B, ...).
196
+ const molecules = [];
197
+ const chainIds = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
198
+ let ci = 0;
199
+ for (const seq of opts.protein)
200
+ molecules.push({ type: 'protein', sequence: resolveSequence(seq), id: chainIds[ci++] });
201
+ for (const seq of opts.dna)
202
+ molecules.push({ type: 'dna', sequence: resolveSequence(seq), id: chainIds[ci++] });
203
+ for (const seq of opts.rna)
204
+ molecules.push({ type: 'rna', sequence: resolveSequence(seq), id: chainIds[ci++] });
205
+ for (const lig of opts.ligand) {
206
+ const parsed = parseLigand(lig);
207
+ molecules.push({ type: 'ligand', id: chainIds[ci++], ...('ccd_code' in parsed ? { ccd_codes: [parsed.ccd_code] } : { smiles: parsed.smiles }) });
208
+ }
209
+ if (molecules.length === 0) {
210
+ process.stderr.write('Error: at least one --protein, --dna, --rna, or --ligand is required.\n');
211
+ process.exit(1);
212
+ }
213
+ const project = requireProject();
214
+ const client = createClient();
215
+ const agents = createAgentsClient();
216
+ const consoleUrl = getConsoleUrl();
217
+ const summary = molecules.map(m => `${m.type}(${m.id})`).join(' + ');
218
+ process.stderr.write(`OpenFold3: ${summary} (depth=${depth}${opts.templates ? ', +templates' : ''})\n`);
219
+ process.stderr.write('Running prediction (NVIDIA NIM; real MSA may cold-start the engine, up to a few min)...');
220
+ const startTime = Date.now();
221
+ const mcpData = await agents.callMcpTool('bionemo', 'predict_complex_structure_openfold3', {
222
+ molecules,
223
+ msa_depth: depth,
224
+ templates: !!opts.templates,
225
+ diffusion_samples: opts.samples,
226
+ output_format: 'pdb',
227
+ });
228
+ const result = mcpData;
229
+ if (result.error || !result.success) {
230
+ const msg = result.error ?? result.message ?? 'Unknown error';
231
+ process.stderr.write(`\nFailed: ${msg}\n`);
232
+ process.exit(1);
233
+ }
234
+ const elapsed = Math.round((Date.now() - startTime) / 1000);
235
+ process.stderr.write(` done (${elapsed}s).\n`);
236
+ const conf = result.confidence ?? {};
237
+ const title = opts.name ?? `OpenFold3: ${result.complex || summary}`;
238
+ const job = await client.createOpenfold3Job({
239
+ project_id: project.id,
240
+ title,
241
+ description: `OpenFold3 complex prediction. ${result.quality ?? ''}`.trim(),
242
+ result_data: {
243
+ complex: result.complex,
244
+ structure_data: result.structure_data,
245
+ output_format: result.output_format,
246
+ confidence: result.confidence,
247
+ quality: result.quality,
248
+ num_structures_returned: result.num_structures_returned,
249
+ diffusion_samples: result.diffusion_samples,
250
+ inference_time_seconds: result.inference_time_seconds,
251
+ msa_depth: depth,
252
+ templates_requested: !!opts.templates,
253
+ templates_applied: !!result.templates_applied,
254
+ model: 'openfold3',
255
+ },
256
+ });
257
+ const url = jobUrl(consoleUrl, project.id, job.job_id);
258
+ maybeOpenBrowser(url);
259
+ const lines = [
260
+ `OpenFold3 prediction complete in ${elapsed}s`,
261
+ `Job ID: ${job.job_id.substring(0, 8)}`,
262
+ `Complex: ${result.complex}`,
263
+ `Quality: ${result.quality ?? 'unknown'}`,
264
+ ];
265
+ if (conf.iptm !== null && conf.iptm !== undefined)
266
+ lines.push(`ipTM: ${conf.iptm.toFixed(3)}`);
267
+ if (conf.ptm !== null && conf.ptm !== undefined)
268
+ lines.push(`pTM: ${conf.ptm.toFixed(3)}`);
269
+ if (conf.plddt !== null && conf.plddt !== undefined)
270
+ lines.push(`pLDDT: ${conf.plddt}`);
271
+ if (opts.templates)
272
+ lines.push(`Templates: ${result.templates_applied ? 'applied' : 'requested but not applied (NIM dropped them)'}`);
273
+ lines.push(`View: ${url}`);
274
+ output({ job_id: job.job_id, elapsed_seconds: elapsed, complex: result.complex, quality: result.quality, confidence: result.confidence, msa_depth: depth, templates_requested: !!opts.templates, templates_applied: !!result.templates_applied, url }, lines.join('\n'));
275
+ }
168
276
  function collectArg(value, prev) {
169
277
  return [...prev, value];
170
278
  }
package/dist/types.d.ts CHANGED
@@ -129,6 +129,34 @@ export interface Boltz2JobParams {
129
129
  result_data: Record<string, unknown>;
130
130
  description?: string;
131
131
  }
132
+ export interface Openfold3Molecule {
133
+ type: 'protein' | 'dna' | 'rna' | 'ligand';
134
+ sequence?: string;
135
+ id?: string;
136
+ smiles?: string;
137
+ ccd_codes?: string[];
138
+ }
139
+ export interface Openfold3McpResult {
140
+ success?: boolean;
141
+ model?: string;
142
+ complex?: string;
143
+ output_format?: string;
144
+ diffusion_samples?: number;
145
+ num_structures_returned?: number;
146
+ structure_data?: string;
147
+ confidence?: {
148
+ plddt?: number | null;
149
+ ptm?: number | null;
150
+ iptm?: number | null;
151
+ confidence_score?: number | null;
152
+ pde?: number | null;
153
+ } | null;
154
+ quality?: string;
155
+ templates_applied?: boolean;
156
+ inference_time_seconds?: number | null;
157
+ message?: string;
158
+ error?: string;
159
+ }
132
160
  export interface SandboxParams {
133
161
  project_id: string;
134
162
  script: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "m3triq",
3
- "version": "0.2.10",
3
+ "version": "0.2.12",
4
4
  "description": "M3TRIQ \u2014 protein-ligand analysis from the terminal",
5
5
  "type": "module",
6
6
  "bin": {