m3triq 0.2.14 → 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
@@ -95,12 +95,47 @@ 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
98
99
 
99
100
  # OpenFold3 — AlphaFold3-class complex (protein + DNA/RNA + ligand); real MSAs
100
101
  m3t predict openfold3 --protein EVQL... --protein DIQM... # complex, auto-paired
101
102
  m3t predict openfold3 --protein MKFL... --ligand ATP --depth exhaustive
102
103
  ```
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
122
+ ```
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
+
104
139
  ## MSA Generation
105
140
 
106
141
  Generate a multiple-sequence alignment (`.a3m`) — the evolutionary input folders use.
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.14')
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: 'standard' | 'exhaustive' | 'custom' | 'fast' | 'deep';
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
+ }
@@ -12,8 +12,7 @@ import { jobUrl, maybeOpenBrowser } from '../url.js';
12
12
  // default; a monomer is never paired). --no-pair to force unpaired.
13
13
  // --templates also searches the PDB and saves per-chain structural templates.
14
14
  // Produces a downloadable .a3m artifact per chain (+ templates_<chain>.json).
15
- // (Legacy --depth fast/deep still accepted: fast = standard+unpaired, deep = standard.)
16
- const MSA_DEPTHS = ['standard', 'exhaustive', 'custom', 'fast', 'deep'];
15
+ const MSA_DEPTHS = ['standard', 'exhaustive', 'custom'];
17
16
  export function registerMsaCommands(program) {
18
17
  program.command('msa')
19
18
  .description('Generate a multiple-sequence alignment (a3m) — per-chain and (for complexes) species-paired')
@@ -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.14",
3
+ "version": "0.2.15",
4
4
  "description": "M3TRIQ \u2014 protein-ligand analysis from the terminal",
5
5
  "type": "module",
6
6
  "bin": {