m3triq 0.2.11 → 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
@@ -25,7 +25,7 @@ const program = new Command();
25
25
  program
26
26
  .name('m3t')
27
27
  .description('M3TRIQ — protein-ligand analysis from the terminal')
28
- .version('0.2.11')
28
+ .version('0.2.12')
29
29
  .option('--json', 'Output as JSON (machine-readable)')
30
30
  .hook('preAction', (thisCommand) => {
31
31
  const opts = thisCommand.optsWithGlobals();
package/dist/client.d.ts CHANGED
@@ -87,8 +87,14 @@ export declare class M3triqClient {
87
87
  id: string;
88
88
  sequence: string;
89
89
  }[];
90
- depth: 'fast' | 'deep';
90
+ depth: 'fast' | 'deep' | 'exhaustive' | 'custom';
91
91
  pair: boolean;
92
+ templates?: boolean;
93
+ custom?: {
94
+ databases: string[];
95
+ max_sequences?: number;
96
+ pair?: boolean;
97
+ };
92
98
  title?: string;
93
99
  }): Promise<{
94
100
  job_id: string;
@@ -99,6 +105,9 @@ export declare class M3triqClient {
99
105
  createBoltz2Job(params: Boltz2JobParams): Promise<{
100
106
  job_id: string;
101
107
  }>;
108
+ createOpenfold3Job(params: Boltz2JobParams): Promise<{
109
+ job_id: string;
110
+ }>;
102
111
  createSandboxJob(params: SandboxParams): Promise<{
103
112
  job_id: string;
104
113
  }>;
package/dist/client.js CHANGED
@@ -140,6 +140,8 @@ export class M3triqClient {
140
140
  chains: params.chains,
141
141
  depth: params.depth,
142
142
  pair: params.pair,
143
+ templates: params.templates ?? false,
144
+ ...(params.custom ? { custom: params.custom } : {}),
143
145
  }),
144
146
  });
145
147
  if (!res.ok) {
@@ -172,6 +174,27 @@ export class M3triqClient {
172
174
  }
173
175
  return res.json();
174
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
+ }
175
198
  async createSandboxJob(params) {
176
199
  return this.request('POST', '/api/jobs/create_sandbox_job/', params);
177
200
  }
@@ -3,15 +3,24 @@ import { createClient, getConsoleUrl } from '../cli.js';
3
3
  import { requireProject } from '../config.js';
4
4
  import { output } from '../output.js';
5
5
  import { jobUrl, maybeOpenBrowser } from '../url.js';
6
- // m3t msa — generate a multiple-sequence alignment (a3m) for one or more chains.
7
- // fast = AlphaFold2-NIM reduced DBs; deep = self-hosted ColabFold (UniRef30 +
8
- // species-paired) on an A100. Produces a downloadable .a3m artifact per chain.
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'];
9
15
  export function registerMsaCommands(program) {
10
16
  program.command('msa')
11
17
  .description('Generate a multiple-sequence alignment (a3m) — per-chain and (for complexes) species-paired')
12
18
  .option('--chain <id:seq>', 'Chain as "id:sequence" or "id:@path". Repeatable for multi-chain (paired) MSAs.', collectArg, [])
13
19
  .option('--sequence <seq>', 'Single-chain shortcut (chain id "A"). Use --chain for multi-chain.')
14
- .option('--depth <tier>', 'MSA depth: "fast" (reduced DBs, ~quick) or "deep" (ColabFold UniRef30 + paired, A100)', 'fast')
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))
15
24
  .option('--pair', 'Compute species-paired MSA across chains (complexes). Default on for ≥2 distinct chains.')
16
25
  .option('--no-pair', 'Skip paired MSA (per-chain only).')
17
26
  .option('--name <name>', 'Custom job title')
@@ -21,10 +30,20 @@ export function registerMsaCommands(program) {
21
30
  }
22
31
  async function runMsa(opts) {
23
32
  const depth = (opts.depth || 'fast').toLowerCase();
24
- if (depth !== 'fast' && depth !== 'deep') {
25
- process.stderr.write(`Error: --depth must be "fast" or "deep", got "${opts.depth}".\n`);
33
+ if (!MSA_DEPTHS.includes(depth)) {
34
+ process.stderr.write(`Error: --depth must be one of ${MSA_DEPTHS.join(' | ')}, got "${opts.depth}".\n`);
26
35
  process.exit(1);
27
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
+ }
28
47
  const chains = [];
29
48
  for (const raw of opts.chain) {
30
49
  const colon = raw.indexOf(':');
@@ -50,6 +69,7 @@ async function runMsa(opts) {
50
69
  // pair only matters for ≥2 distinct chains; let the server make the final call too.
51
70
  const distinct = new Set(chains.map(c => c.sequence));
52
71
  const pair = opts.pair && chains.length >= 2 && distinct.size >= 2;
72
+ const templates = !!opts.templates;
53
73
  const project = requireProject();
54
74
  const client = createClient();
55
75
  const consoleUrl = getConsoleUrl();
@@ -58,21 +78,26 @@ async function runMsa(opts) {
58
78
  chains,
59
79
  depth: depth,
60
80
  pair,
81
+ templates,
82
+ custom,
61
83
  title: opts.name,
62
84
  });
63
85
  const url = jobUrl(consoleUrl, project.id, result.job_id);
64
86
  maybeOpenBrowser(url);
65
- const timeEst = depth === 'deep'
66
- ? '~3-10 min (ColabFold A100; +cold start if scaled to zero)'
67
- : '~2-5 min (reduced DBs; +cold start if scaled to zero)';
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(', ');
68
93
  const lines = [
69
- `MSA job queued (${chains.length} chain${chains.length > 1 ? 's' : ''}, depth=${depth}${pair ? ', paired' : ''})`,
94
+ `MSA job queued (${chains.length} chain${chains.length > 1 ? 's' : ''}, depth=${depth}${extras ? `, ${extras}` : ''})`,
70
95
  `Job ID: ${result.job_id.substring(0, 8)}`,
71
96
  `Estimated: ${timeEst}`,
72
- `Output: one .a3m artifact per chain (Files tab)`,
97
+ `Output: one .a3m artifact per chain${templates ? ' (+ templates_<chain>.json)' : ''} (Files tab)`,
73
98
  `View: ${url}`,
74
99
  ];
75
- output({ job_id: result.job_id, n_chains: chains.length, depth, pair, url }, lines.join('\n'));
100
+ output({ job_id: result.job_id, n_chains: chains.length, depth, pair, templates, url }, lines.join('\n'));
76
101
  }
77
102
  function collectArg(value, prev) {
78
103
  return [...prev, value];
@@ -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.11",
3
+ "version": "0.2.12",
4
4
  "description": "M3TRIQ \u2014 protein-ligand analysis from the terminal",
5
5
  "type": "module",
6
6
  "bin": {