m3triq 0.2.13 → 0.2.15

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/README.md CHANGED
@@ -85,7 +85,7 @@ m3t md results <job-id>
85
85
 
86
86
  ```bash
87
87
  m3t predict esmfold MKFLILLFNILCL... # Fast (~10s, max 1024aa)
88
- m3t predict alphafold2 MKFLILLFNILCL... # Accurate (~15-20min, max 2048aa)
88
+ m3t predict alphafold2 MKFLILLFNILCL... # AF2-ptm monomer via deep MSA + ColabFold (~5-15min)
89
89
 
90
90
  # ESMFold2-Fast — monomer or multi-chain complex (self-hosted A100, max 2048aa total).
91
91
  # Beats AlphaFold3 on antibody-antigen DockQ from single sequence; returns pLDDT/pTM/ipTM.
@@ -95,8 +95,66 @@ m3t predict esmfold2-batch inputs.json # fold N complexes
95
95
 
96
96
  # Boltz-2 — biomolecular complex (protein + DNA/RNA + ligand) with binding affinity
97
97
  m3t predict boltz2 --protein MKFL... --ligand "CC(=O)O" --rna GGUC...
98
+ m3t predict boltz2 --protein MKFL... --ligand "CC(=O)O" --depth exhaustive # + real per-chain MSAs
99
+
100
+ # OpenFold3 — AlphaFold3-class complex (protein + DNA/RNA + ligand); real MSAs
101
+ m3t predict openfold3 --protein EVQL... --protein DIQM... # complex, auto-paired
102
+ m3t predict openfold3 --protein MKFL... --ligand ATP --depth exhaustive
103
+ ```
104
+
105
+ ## Carbohydrate / Glycan Co-folding
106
+
107
+ Protein–carbohydrate interactions (e.g. lectins, phage receptor-binding proteins,
108
+ glycan-recognizing binders) are poorly served by classical docking — Vina/GNINA
109
+ score sugars badly. The reliable route is to **define** the glycan, then **co-fold**
110
+ it with the protein in an AlphaFold3-class model.
111
+
112
+ ```bash
113
+ # Define a glycan: IUPAC-condensed → SMILES + 3D SDF + per-residue CCD/bond decomposition
114
+ m3t glycan build "Gal(b1-4)GlcNAc"
115
+ m3t glycan build "Neu5Ac(a2-3)Gal(b1-4)Glc" --out sialyllactose.sdf --attach N-linked
116
+
117
+ # Co-fold a protein with a glycan
118
+ m3t predict openfold3 --protein MKFL... \
119
+ --glycan "Man(a1-3)[Man(a1-6)]Man(b1-4)GlcNAc(b1-4)GlcNAc" --depth exhaustive # CCD + bonds (default)
120
+ m3t predict openfold3 --protein MKFL... --glycan "Gal(b1-4)GlcNAc" --glycan-smiles # force one SMILES blob
121
+ m3t predict boltz2 --protein MKFL... --glycan "Gal(b1-4)GlcNAc" --depth exhaustive
98
122
  ```
99
123
 
124
+ - **IUPAC-condensed**: reducing end on the **right**, linkages in `()`, branches in `[]`
125
+ (e.g. `Neu5Ac(a2-3)Gal(b1-4)Glc`). Common monosaccharides map to PDB CCD codes.
126
+ - `--glycan` on **OpenFold3** defaults to the **per-residue CCD + glycosidic-bond**
127
+ (`bondedAtomPairs`) representation — the AF3-glycan literature finds a single SMILES blob
128
+ "frequently fails to reproduce correct conformations even for simple oligosaccharides".
129
+ Falls back to SMILES if the glycan can't be decomposed; `--glycan-smiles` forces SMILES.
130
+ Best-effort: if the hosted model rejects explicit bonds the fold retries as separate
131
+ ligand entities.
132
+ - `--glycan` on **Boltz-2** rides as a single SMILES ligand (Boltz-2 has no inter-entity
133
+ bond field).
134
+ - **pLDDT is unreliable for carbohydrates** — high confidence can accompany wrong
135
+ stereochemistry. Validate ring pucker / glycosidic torsions on the output.
136
+ - `--depth exhaustive` (metagenomic envDB) is recommended for orphan / phage / bacterial
137
+ families where standard databases come back sparse.
138
+
139
+ ## MSA Generation
140
+
141
+ Generate a multiple-sequence alignment (`.a3m`) — the evolutionary input folders use.
142
+ Two orthogonal knobs: **`--depth`** = coverage (databases), **`--pair`** = species
143
+ pairing (only matters for multi-chain complexes; on by default, a monomer is never paired).
144
+
145
+ ```bash
146
+ m3t msa --sequence MKFL... # standard, monomer
147
+ m3t msa --chain A:EVQL... --chain B:DIQM... # standard complex, auto-paired
148
+ m3t msa --chain A:EVQL... --chain B:DIQM... --no-pair # complex, force unpaired
149
+ m3t msa --sequence MKFL... --depth exhaustive --templates # + metagenomic envDB + PDB templates
150
+ m3t msa --sequence MKFL... --depth custom \
151
+ --databases uniref30,envdb --max-sequences 300 # fine control (max_sequences caps depth)
152
+ ```
153
+
154
+ - `--depth`: `standard` (UniRef30, default) | `exhaustive` (+ metagenomic envDB) | `custom`
155
+ - `--templates`: also save per-chain PDB structural templates (`templates_<chain>.json`)
156
+ - Output: one `.a3m` per chain in the project's Files tab; repeat sequences are cached (instant)
157
+
100
158
  ## Protein Embeddings
101
159
 
102
160
  ```bash
package/dist/cli.js CHANGED
@@ -11,6 +11,7 @@ import { registerJobCommands } from './commands/jobs.js';
11
11
  import { registerDockingCommands } from './commands/docking.js';
12
12
  import { registerPredictCommands } from './commands/predict.js';
13
13
  import { registerMsaCommands } from './commands/msa.js';
14
+ import { registerGlycanCommands } from './commands/glycan.js';
14
15
  import { registerChemblCommands } from './commands/chembl.js';
15
16
  import { registerSandboxCommands } from './commands/sandbox.js';
16
17
  import { registerDesignCommands } from './commands/design.js';
@@ -25,7 +26,7 @@ const program = new Command();
25
26
  program
26
27
  .name('m3t')
27
28
  .description('M3TRIQ — protein-ligand analysis from the terminal')
28
- .version('0.2.13')
29
+ .version('0.2.15')
29
30
  .option('--json', 'Output as JSON (machine-readable)')
30
31
  .hook('preAction', (thisCommand) => {
31
32
  const opts = thisCommand.optsWithGlobals();
@@ -39,6 +40,7 @@ registerJobCommands(program);
39
40
  registerDockingCommands(program);
40
41
  registerPredictCommands(program);
41
42
  registerMsaCommands(program);
43
+ registerGlycanCommands(program);
42
44
  registerChemblCommands(program);
43
45
  registerSandboxCommands(program);
44
46
  registerDesignCommands(program);
package/dist/client.d.ts CHANGED
@@ -87,7 +87,7 @@ export declare class M3triqClient {
87
87
  id: string;
88
88
  sequence: string;
89
89
  }[];
90
- depth: 'fast' | 'deep' | 'exhaustive' | 'custom';
90
+ depth: 'standard' | 'exhaustive' | 'custom';
91
91
  pair: boolean;
92
92
  templates?: boolean;
93
93
  custom?: {
package/dist/client.js CHANGED
@@ -166,6 +166,7 @@ export class M3triqClient {
166
166
  title: params.title,
167
167
  description: params.description ?? '',
168
168
  result_data: params.result_data,
169
+ ...(params.prediction_inputs ? { prediction_inputs: params.prediction_inputs } : {}),
169
170
  }),
170
171
  });
171
172
  if (!res.ok) {
@@ -187,6 +188,7 @@ export class M3triqClient {
187
188
  title: params.title,
188
189
  description: params.description ?? '',
189
190
  result_data: params.result_data,
191
+ ...(params.prediction_inputs ? { prediction_inputs: params.prediction_inputs } : {}),
190
192
  }),
191
193
  });
192
194
  if (!res.ok) {
@@ -0,0 +1,4 @@
1
+ import type { Command } from 'commander';
2
+ import type { GlycanBuildResult } from '../types.js';
3
+ export declare function registerGlycanCommands(program: Command): void;
4
+ export declare function buildGlycanViaMcp(iupac: string, embed3d?: boolean, attachment?: string): Promise<GlycanBuildResult>;
@@ -0,0 +1,73 @@
1
+ import fs from 'node:fs';
2
+ import { createAgentsClient } from '../cli.js';
3
+ import { output } from '../output.js';
4
+ export function registerGlycanCommands(program) {
5
+ const glycan = program
6
+ .command('glycan')
7
+ .description('Build / define carbohydrate (glycan) ligands for co-folding and docking');
8
+ // m3t glycan build "<iupac>"
9
+ glycan
10
+ .command('build')
11
+ .argument('<iupac>', 'Glycan in IUPAC-condensed notation (reducing end on the right). e.g. "Gal(b1-4)GlcNAc"')
12
+ .description('Build a glycan from IUPAC-condensed notation → SMILES + 3D SDF + CCD/bond decomposition')
13
+ .option('--out <file.sdf>', 'Write the 3D structure to an SDF file')
14
+ .option('--no-3d', 'Skip 3D conformer generation (SMILES + decomposition only)')
15
+ .option('--attach <type>', 'Also emit a protein-glycosylation attachment template: N-linked | O-linked | O-linked-thr')
16
+ .action(async (iupac, opts) => {
17
+ await runGlycanBuild(iupac, opts);
18
+ });
19
+ }
20
+ export async function buildGlycanViaMcp(iupac, embed3d = true, attachment) {
21
+ const agents = createAgentsClient();
22
+ const data = (await agents.callMcpTool('rdkit', 'build_glycan', {
23
+ iupac,
24
+ embed_3d: embed3d,
25
+ ...(attachment ? { attachment } : {}),
26
+ }));
27
+ if (data.success === false || data.error) {
28
+ throw new Error(data.error ?? `could not build glycan "${iupac}"`);
29
+ }
30
+ return data;
31
+ }
32
+ async function runGlycanBuild(iupac, opts) {
33
+ const embed3d = opts['3d'] !== false || !!opts.out; // need 3D if writing SDF
34
+ let result;
35
+ try {
36
+ result = await buildGlycanViaMcp(iupac, embed3d, opts.attach);
37
+ }
38
+ catch (e) {
39
+ process.stderr.write(`Error: ${e.message}\n`);
40
+ process.exit(1);
41
+ return;
42
+ }
43
+ if (opts.out) {
44
+ if (!result.sdf) {
45
+ process.stderr.write('Warning: no 3D structure was generated; nothing written.\n');
46
+ }
47
+ else {
48
+ fs.writeFileSync(opts.out, result.sdf);
49
+ process.stderr.write(`Wrote 3D structure → ${opts.out}\n`);
50
+ }
51
+ }
52
+ const lines = [
53
+ `Glycan: ${iupac}`,
54
+ `SMILES: ${result.smiles}`,
55
+ `Formula: ${result.formula ?? '?'} MW: ${result.molecular_weight ?? '?'} rotatable bonds: ${result.n_rotatable_bonds ?? '?'}`,
56
+ ];
57
+ if (result.ccd_supported && result.residues) {
58
+ lines.push(`Residues (${result.n_residues}, reducing-end first): ${result.residues.map(r => `${r.name}[${r.ccd}]`).join(' · ')}`);
59
+ if (result.bonds && result.bonds.length > 0) {
60
+ lines.push(`Glycosidic bonds: ${result.bonds.map(b => `${b.child_idx}.${b.child_atom}→${b.parent_idx}.${b.parent_atom} (${b.linkage})`).join(', ')}`);
61
+ }
62
+ lines.push('Use with: m3t predict openfold3 --protein <seq> --glycan "' + iupac + '" --depth exhaustive (co-folds as CCD + bonds)');
63
+ }
64
+ else if (result.ccd_note) {
65
+ lines.push(`Note: ${result.ccd_note}`);
66
+ lines.push('Use with: m3t predict openfold3 --protein <seq> --glycan "' + iupac + '" --depth exhaustive (falls back to SMILES ligand)');
67
+ }
68
+ if (result.attachment) {
69
+ const a = result.attachment;
70
+ lines.push(`Attachment: glycan ${a.glycan_atom} → protein ${a.protein_residue} ${a.protein_atom} (${a.type})`);
71
+ }
72
+ output(result, lines.join('\n'));
73
+ }
@@ -4,32 +4,33 @@ import { requireProject } from '../config.js';
4
4
  import { output } from '../output.js';
5
5
  import { jobUrl, maybeOpenBrowser } from '../url.js';
6
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)
7
+ // all on the self-hosted ColabFold MSA-search engine (GPU-MMseqs2). Two independent
8
+ // knobs:
9
+ // --depth = COVERAGE (which databases): standard (UniRef30) | exhaustive (+envDB,
10
+ // for remote-homology/orphan families) | custom (pick databases)
11
+ // --pair = species PAIRING, only meaningful for a multi-chain complex (on by
12
+ // default; a monomer is never paired). --no-pair to force unpaired.
12
13
  // --templates also searches the PDB and saves per-chain structural templates.
13
14
  // Produces a downloadable .a3m artifact per chain (+ templates_<chain>.json).
14
- const MSA_DEPTHS = ['fast', 'deep', 'exhaustive', 'custom'];
15
+ const MSA_DEPTHS = ['standard', 'exhaustive', 'custom'];
15
16
  export function registerMsaCommands(program) {
16
17
  program.command('msa')
17
18
  .description('Generate a multiple-sequence alignment (a3m) — per-chain and (for complexes) species-paired')
18
19
  .option('--chain <id:seq>', 'Chain as "id:sequence" or "id:@path". Repeatable for multi-chain (paired) MSAs.', collectArg, [])
19
20
  .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('--depth <coverage>', 'Coverage / databases: standard (UniRef30) | exhaustive (+ envDB) | custom', 'standard')
21
22
  .option('--templates', 'Also search the PDB for structural templates; saves a templates_<chain>.json per chain.')
22
23
  .option('--databases <csv>', 'For --depth custom: comma-separated database ids (e.g. "uniref30,envdb"). See get_msa_capabilities.')
23
24
  .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).')
25
+ .option('--pair', 'Species-paired MSA only matters for a multi-chain complex (on by default; a monomer is never paired).')
26
+ .option('--no-pair', 'Force unpaired (per-chain only), even for a complex.')
26
27
  .option('--name <name>', 'Custom job title')
27
28
  .action(async (opts) => {
28
29
  await runMsa(opts);
29
30
  });
30
31
  }
31
32
  async function runMsa(opts) {
32
- const depth = (opts.depth || 'fast').toLowerCase();
33
+ const depth = (opts.depth || 'standard').toLowerCase();
33
34
  if (!MSA_DEPTHS.includes(depth)) {
34
35
  process.stderr.write(`Error: --depth must be one of ${MSA_DEPTHS.join(' | ')}, got "${opts.depth}".\n`);
35
36
  process.exit(1);
@@ -85,11 +86,9 @@ async function runMsa(opts) {
85
86
  const url = jobUrl(consoleUrl, project.id, result.job_id);
86
87
  maybeOpenBrowser(url);
87
88
  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(', ');
89
+ ? `~4-6 min (UniRef30 + envDB on 2 T4s${pair ? ' + paired' : ''}; +cold start if scaled to zero)`
90
+ : `~3-5 min (UniRef30${pair ? ' + paired' : ''}; +cold start if scaled to zero)`;
91
+ const extras = [pair ? 'paired' : 'unpaired', templates ? '+templates' : ''].filter(Boolean).join(', ');
93
92
  const lines = [
94
93
  `MSA job queued (${chains.length} chain${chains.length > 1 ? 's' : ''}, depth=${depth}${extras ? `, ${extras}` : ''})`,
95
94
  `Job ID: ${result.job_id.substring(0, 8)}`,
@@ -3,6 +3,7 @@ import { createAgentsClient, 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
+ import { buildGlycanViaMcp } from './glycan.js';
6
7
  export function registerPredictCommands(program) {
7
8
  const predict = program.command('predict').description('Predict 3D structure from sequence (single protein or biomolecular complex)');
8
9
  // m3t predict esmfold
@@ -62,6 +63,8 @@ export function registerPredictCommands(program) {
62
63
  .option('--dna <seq>', 'DNA sequence (or @path to file). Repeatable.', collectArg, [])
63
64
  .option('--rna <seq>', 'RNA sequence (or @path to file). Repeatable.', collectArg, [])
64
65
  .option('--ligand <smiles_or_ccd>', 'Ligand as SMILES or CCD code (e.g. ATP). Repeatable.', collectArg, [])
66
+ .option('--glycan <iupac>', 'Carbohydrate in IUPAC-condensed notation (e.g. "Gal(b1-4)GlcNAc"); built to a SMILES ligand. Repeatable.', collectArg, [])
67
+ .option('--depth <tier>', 'Attach real per-chain MSAs: none (default, NIM-internal) | standard | exhaustive | custom', 'none')
65
68
  .option('--samples <n>', 'Number of structure samples (1-25, default: 1)', (v) => parseInt(v, 10), 1)
66
69
  .option('--recycling <n>', 'Recycling steps (1-10, default: 3)', (v) => parseInt(v, 10), 3)
67
70
  .option('--sampling <n>', 'Diffusion sampling steps (10-1000, default: 50)', (v) => parseInt(v, 10), 50)
@@ -76,7 +79,9 @@ export function registerPredictCommands(program) {
76
79
  .option('--dna <seq>', 'DNA sequence (or @path to file). Repeatable.', collectArg, [])
77
80
  .option('--rna <seq>', 'RNA sequence (or @path to file). Repeatable.', collectArg, [])
78
81
  .option('--ligand <smiles_or_ccd>', 'Ligand as SMILES or CCD code (e.g. ATP). Repeatable.', collectArg, [])
79
- .option('--depth <tier>', 'MSA depth for protein chains: fast | deep | exhaustive | none (default fast)', 'fast')
82
+ .option('--glycan <iupac>', 'Carbohydrate in IUPAC-condensed notation (e.g. "Gal(b1-4)GlcNAc"). Co-folded as a proper multi-residue sugar chain (per-residue CCD codes + glycosidic bonds, the AF3-recommended representation). Repeatable.', collectArg, [])
83
+ .option('--glycan-smiles', 'Attach each glycan as a single SMILES blob instead of the default per-residue CCD + bonds. Less accurate for conformation; use only if the CCD path misbehaves.')
84
+ .option('--depth <tier>', 'MSA depth for protein chains: standard (UniRef30, default) | exhaustive (+envDB, best for orphan/phage) | custom | none. (fast→standard, deep→exhaustive accepted)', 'standard')
80
85
  .option('--templates', 'Seed the fold with PDB structural templates per protein chain (OpenFold3 self-aligns).')
81
86
  .option('--samples <n>', 'Number of diffusion samples (1-25, default: 1)', (v) => parseInt(v, 10), 1)
82
87
  .option('--name <name>', 'Custom job title')
@@ -101,6 +106,11 @@ async function runPredict(sequence, method, name) {
101
106
  output(data, `${method} prediction created\nJob ID: ${result.job_id.substring(0, 8)}\nLength: ${sequence.length} residues\nEstimated: ${timeEst}`);
102
107
  }
103
108
  async function runBoltz2(opts) {
109
+ const depth = normalizeMsaDepth(opts.depth || 'none');
110
+ if (!['none', 'standard', 'exhaustive', 'custom'].includes(depth)) {
111
+ process.stderr.write(`Error: --depth must be none | standard | exhaustive | custom, got "${opts.depth}".\n`);
112
+ process.exit(1);
113
+ }
104
114
  const polymers = [];
105
115
  for (const seq of opts.protein)
106
116
  polymers.push({ molecule_type: 'protein', sequence: resolveSequence(seq) });
@@ -113,12 +123,23 @@ async function runBoltz2(opts) {
113
123
  process.exit(1);
114
124
  }
115
125
  const ligands = opts.ligand.map(parseLigand);
126
+ // Glycans → SMILES ligands (Boltz-2 has no inter-entity bond field, so a glycan
127
+ // rides in as one SMILES molecule).
128
+ for (const g of opts.glycan) {
129
+ const built = await buildGlycanViaMcp(g, false);
130
+ if (!built.smiles) {
131
+ process.stderr.write(`Error: could not build glycan "${g}".\n`);
132
+ process.exit(1);
133
+ }
134
+ process.stderr.write(`Glycan "${g}" → SMILES ligand (${built.formula ?? '?'})\n`);
135
+ ligands.push({ smiles: built.smiles });
136
+ }
116
137
  const project = requireProject();
117
138
  const client = createClient();
118
139
  const agents = createAgentsClient();
119
140
  const consoleUrl = getConsoleUrl();
120
141
  const summary = describeComplex(polymers, ligands);
121
- process.stderr.write(`Boltz-2: ${summary}\n`);
142
+ process.stderr.write(`Boltz-2: ${summary}${depth !== 'none' ? ` (MSA depth=${depth})` : ''}\n`);
122
143
  process.stderr.write('Running prediction (NVIDIA NIM, ~1-5 min)...');
123
144
  const startTime = Date.now();
124
145
  const mcpData = await agents.callMcpTool('bionemo', 'predict_structure_boltz2', {
@@ -127,6 +148,7 @@ async function runBoltz2(opts) {
127
148
  diffusion_samples: opts.samples,
128
149
  recycling_steps: opts.recycling,
129
150
  sampling_steps: opts.sampling,
151
+ msa_depth: depth,
130
152
  });
131
153
  const result = mcpData;
132
154
  if (result.error || !result.success) {
@@ -152,8 +174,18 @@ async function runBoltz2(opts) {
152
174
  quality: result.quality,
153
175
  num_structures_returned: result.num_structures_returned,
154
176
  diffusion_samples: result.diffusion_samples,
177
+ msa_depth: depth,
155
178
  model: 'boltz2',
156
179
  },
180
+ prediction_inputs: {
181
+ model: 'boltz2',
182
+ msa_depth: depth,
183
+ n_protein: opts.protein.length,
184
+ n_dna: opts.dna.length,
185
+ n_rna: opts.rna.length,
186
+ ligands: ligands.length,
187
+ glycans: opts.glycan,
188
+ },
157
189
  });
158
190
  const url = jobUrl(consoleUrl, project.id, job.job_id);
159
191
  maybeOpenBrowser(url);
@@ -181,10 +213,10 @@ async function runBoltz2(opts) {
181
213
  }, lines.join('\n'));
182
214
  }
183
215
  async function runOpenfold3(opts) {
184
- const depth = (opts.depth || 'fast').toLowerCase();
185
- const OF3_DEPTHS = ['fast', 'deep', 'exhaustive', 'custom', 'none'];
216
+ const depth = normalizeMsaDepth(opts.depth || 'standard');
217
+ const OF3_DEPTHS = ['standard', 'exhaustive', 'custom', 'none'];
186
218
  if (!OF3_DEPTHS.includes(depth)) {
187
- process.stderr.write(`Error: --depth must be one of ${OF3_DEPTHS.join(' | ')}, got "${opts.depth}".\n`);
219
+ process.stderr.write(`Error: --depth must be one of ${OF3_DEPTHS.join(' | ')} (fast/deep accepted as aliases), got "${opts.depth}".\n`);
188
220
  process.exit(1);
189
221
  }
190
222
  // Build the OpenFold3 molecules array (proteins get chain ids A, B, ...).
@@ -201,8 +233,46 @@ async function runOpenfold3(opts) {
201
233
  const parsed = parseLigand(lig);
202
234
  molecules.push({ type: 'ligand', id: chainIds[ci++], ...('ccd_code' in parsed ? { ccd_codes: [parsed.ccd_code] } : { smiles: parsed.smiles }) });
203
235
  }
236
+ // Glycans: by default a proper multi-residue chain (one single-CCD ligand per sugar
237
+ // + glycosidic bonds); --glycan-smiles forces a single SMILES ligand instead.
238
+ const bonds = [];
239
+ for (const g of opts.glycan) {
240
+ const built = await buildGlycanViaMcp(g, false);
241
+ // DEFAULT to the per-residue CCD + glycosidic-bond representation: the AF3 glycan
242
+ // literature finds a single SMILES blob "frequently fails to reproduce correct
243
+ // conformations even for simple linear oligosaccharides", and recommends
244
+ // bondedAtomPairs + monosaccharide CCD codes. Fall back to SMILES only when the
245
+ // glycan can't be decomposed, or when the user forces it with --glycan-smiles.
246
+ const useCcd = !opts.glycanSmiles && built.ccd_supported && !!built.residues && built.residues.length > 0;
247
+ if (useCcd) {
248
+ const idxToChain = {};
249
+ for (const res of built.residues) {
250
+ const id = chainIds[ci++];
251
+ idxToChain[res.index] = id;
252
+ molecules.push({ type: 'ligand', id, ccd_codes: [res.ccd] });
253
+ }
254
+ for (const b of built.bonds ?? []) {
255
+ bonds.push({
256
+ atom1: [idxToChain[b.child_idx], 1, b.child_atom],
257
+ atom2: [idxToChain[b.parent_idx], 1, b.parent_atom],
258
+ });
259
+ }
260
+ process.stderr.write(`Glycan "${g}" → ${built.residues.length} sugar residues + ${built.bonds?.length ?? 0} glycosidic bonds (CCD + bondedAtomPairs)\n`);
261
+ }
262
+ else {
263
+ if (!opts.glycanSmiles && !built.ccd_supported) {
264
+ process.stderr.write(`Glycan "${g}": CCD decomposition unavailable (${built.ccd_note ?? 'unsupported monosaccharide/topology'}); falling back to a single SMILES ligand.\n`);
265
+ }
266
+ if (!built.smiles) {
267
+ process.stderr.write(`Error: could not build glycan "${g}".\n`);
268
+ process.exit(1);
269
+ }
270
+ molecules.push({ type: 'ligand', id: chainIds[ci++], smiles: built.smiles });
271
+ process.stderr.write(`Glycan "${g}" → SMILES ligand (${built.formula ?? '?'})\n`);
272
+ }
273
+ }
204
274
  if (molecules.length === 0) {
205
- process.stderr.write('Error: at least one --protein, --dna, --rna, or --ligand is required.\n');
275
+ process.stderr.write('Error: at least one --protein, --dna, --rna, --ligand, or --glycan is required.\n');
206
276
  process.exit(1);
207
277
  }
208
278
  const project = requireProject();
@@ -210,7 +280,7 @@ async function runOpenfold3(opts) {
210
280
  const agents = createAgentsClient();
211
281
  const consoleUrl = getConsoleUrl();
212
282
  const summary = molecules.map(m => `${m.type}(${m.id})`).join(' + ');
213
- process.stderr.write(`OpenFold3: ${summary} (depth=${depth}${opts.templates ? ', +templates' : ''})\n`);
283
+ process.stderr.write(`OpenFold3: ${summary} (depth=${depth}${opts.templates ? ', +templates' : ''}${bonds.length ? `, ${bonds.length} bonds` : ''})\n`);
214
284
  process.stderr.write('Running prediction (NVIDIA NIM; real MSA may cold-start the engine, up to a few min)...');
215
285
  const startTime = Date.now();
216
286
  const mcpData = await agents.callMcpTool('bionemo', 'predict_complex_structure_openfold3', {
@@ -219,6 +289,7 @@ async function runOpenfold3(opts) {
219
289
  templates: !!opts.templates,
220
290
  diffusion_samples: opts.samples,
221
291
  output_format: 'pdb',
292
+ ...(bonds.length > 0 ? { bonds } : {}),
222
293
  });
223
294
  const result = mcpData;
224
295
  if (result.error || !result.success) {
@@ -248,6 +319,18 @@ async function runOpenfold3(opts) {
248
319
  templates_applied: !!result.templates_applied,
249
320
  model: 'openfold3',
250
321
  },
322
+ prediction_inputs: {
323
+ model: 'openfold3',
324
+ msa_depth: depth,
325
+ templates: !!opts.templates,
326
+ n_protein: opts.protein.length,
327
+ n_dna: opts.dna.length,
328
+ n_rna: opts.rna.length,
329
+ ligands: opts.ligand.length,
330
+ glycans: opts.glycan,
331
+ glycan_repr: opts.glycanSmiles ? 'smiles' : 'ccd+bonds',
332
+ n_bonds: bonds.length,
333
+ },
251
334
  });
252
335
  const url = jobUrl(consoleUrl, project.id, job.job_id);
253
336
  maybeOpenBrowser(url);
@@ -265,12 +348,24 @@ async function runOpenfold3(opts) {
265
348
  lines.push(`pLDDT: ${conf.plddt}`);
266
349
  if (opts.templates)
267
350
  lines.push(`Templates: ${result.templates_applied ? 'applied' : 'requested but not applied (NIM dropped them)'}`);
351
+ if (opts.glycan.length > 0)
352
+ lines.push('Note: validate the glycan geometry (ring pucker / glycosidic torsions) — pLDDT is unreliable for carbohydrates.');
268
353
  lines.push(`View: ${url}`);
269
354
  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'));
270
355
  }
271
356
  function collectArg(value, prev) {
272
357
  return [...prev, value];
273
358
  }
359
+ // MSA depth vocabulary aligns with the MSA service (standard|exhaustive|custom|none).
360
+ // fast/deep are accepted as legacy aliases (fast→standard, deep→exhaustive).
361
+ function normalizeMsaDepth(d) {
362
+ const v = (d || '').toLowerCase();
363
+ if (v === 'fast')
364
+ return 'standard';
365
+ if (v === 'deep')
366
+ return 'exhaustive';
367
+ return v;
368
+ }
274
369
  function resolveSequence(input) {
275
370
  // @path → read file (FASTA-aware: strip header/whitespace)
276
371
  if (input.startsWith('@')) {
package/dist/types.d.ts CHANGED
@@ -128,6 +128,36 @@ export interface Boltz2JobParams {
128
128
  title: string;
129
129
  result_data: Record<string, unknown>;
130
130
  description?: string;
131
+ prediction_inputs?: Record<string, unknown>;
132
+ }
133
+ export interface GlycanResidue {
134
+ index: number;
135
+ name: string;
136
+ ccd: string;
137
+ }
138
+ export interface GlycanBond {
139
+ child_idx: number;
140
+ parent_idx: number;
141
+ child_atom: string;
142
+ parent_atom: string;
143
+ linkage: string;
144
+ }
145
+ export interface GlycanBuildResult {
146
+ success?: boolean;
147
+ input?: string;
148
+ smiles?: string;
149
+ sdf?: string;
150
+ formula?: string;
151
+ molecular_weight?: number;
152
+ n_rotatable_bonds?: number;
153
+ n_residues?: number;
154
+ ccd_supported?: boolean;
155
+ residues?: GlycanResidue[];
156
+ bonds?: GlycanBond[];
157
+ reducing_end?: GlycanResidue;
158
+ attachment?: Record<string, unknown>;
159
+ ccd_note?: string;
160
+ error?: string;
131
161
  }
132
162
  export interface Openfold3Molecule {
133
163
  type: 'protein' | 'dna' | 'rna' | 'ligand';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "m3triq",
3
- "version": "0.2.13",
3
+ "version": "0.2.15",
4
4
  "description": "M3TRIQ \u2014 protein-ligand analysis from the terminal",
5
5
  "type": "module",
6
6
  "bin": {