m3triq 0.2.10 → 0.2.11

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.11')
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,6 +81,21 @@ 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';
91
+ pair: boolean;
92
+ title?: string;
93
+ }): Promise<{
94
+ job_id: string;
95
+ task_id?: string;
96
+ depth?: string;
97
+ n_chains?: number;
98
+ }>;
84
99
  createBoltz2Job(params: Boltz2JobParams): Promise<{
85
100
  job_id: string;
86
101
  }>;
package/dist/client.js CHANGED
@@ -126,6 +126,28 @@ 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
+ }),
144
+ });
145
+ if (!res.ok) {
146
+ const text = await res.text();
147
+ throw new Error(`API error ${res.status}: ${text.substring(0, 200)}`);
148
+ }
149
+ return res.json();
150
+ }
129
151
  async createBoltz2Job(params) {
130
152
  // Boltz-2 prediction is run client-side via the agents MCP, then saved here as a completed job.
131
153
  // Mirrors the RFantibody internal flow but with status='completed' and result_data already populated.
@@ -0,0 +1,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerMsaCommands(program: Command): void;
@@ -0,0 +1,91 @@
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
+ // fast = AlphaFold2-NIM reduced DBs; deep = self-hosted ColabFold (UniRef30 +
8
+ // species-paired) on an A100. Produces a downloadable .a3m artifact per chain.
9
+ export function registerMsaCommands(program) {
10
+ program.command('msa')
11
+ .description('Generate a multiple-sequence alignment (a3m) — per-chain and (for complexes) species-paired')
12
+ .option('--chain <id:seq>', 'Chain as "id:sequence" or "id:@path". Repeatable for multi-chain (paired) MSAs.', collectArg, [])
13
+ .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')
15
+ .option('--pair', 'Compute species-paired MSA across chains (complexes). Default on for ≥2 distinct chains.')
16
+ .option('--no-pair', 'Skip paired MSA (per-chain only).')
17
+ .option('--name <name>', 'Custom job title')
18
+ .action(async (opts) => {
19
+ await runMsa(opts);
20
+ });
21
+ }
22
+ async function runMsa(opts) {
23
+ 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`);
26
+ process.exit(1);
27
+ }
28
+ const chains = [];
29
+ for (const raw of opts.chain) {
30
+ const colon = raw.indexOf(':');
31
+ if (colon <= 0) {
32
+ process.stderr.write(`Error: --chain must be "id:sequence" or "id:@path", got "${raw}".\n`);
33
+ process.exit(1);
34
+ }
35
+ const id = raw.slice(0, colon).trim();
36
+ const seq = resolveSequence(raw.slice(colon + 1));
37
+ if (!id) {
38
+ process.stderr.write('Error: --chain id cannot be empty.\n');
39
+ process.exit(1);
40
+ }
41
+ chains.push({ id, sequence: seq });
42
+ }
43
+ if (chains.length === 0 && opts.sequence) {
44
+ chains.push({ id: 'A', sequence: resolveSequence(opts.sequence) });
45
+ }
46
+ if (chains.length === 0) {
47
+ process.stderr.write('Error: provide at least one --chain "id:seq" or --sequence "<seq>".\n');
48
+ process.exit(1);
49
+ }
50
+ // pair only matters for ≥2 distinct chains; let the server make the final call too.
51
+ const distinct = new Set(chains.map(c => c.sequence));
52
+ const pair = opts.pair && chains.length >= 2 && distinct.size >= 2;
53
+ const project = requireProject();
54
+ const client = createClient();
55
+ const consoleUrl = getConsoleUrl();
56
+ const result = await client.createMsaJob({
57
+ project_id: project.id,
58
+ chains,
59
+ depth: depth,
60
+ pair,
61
+ title: opts.name,
62
+ });
63
+ const url = jobUrl(consoleUrl, project.id, result.job_id);
64
+ 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)';
68
+ const lines = [
69
+ `MSA job queued (${chains.length} chain${chains.length > 1 ? 's' : ''}, depth=${depth}${pair ? ', paired' : ''})`,
70
+ `Job ID: ${result.job_id.substring(0, 8)}`,
71
+ `Estimated: ${timeEst}`,
72
+ `Output: one .a3m artifact per chain (Files tab)`,
73
+ `View: ${url}`,
74
+ ];
75
+ output({ job_id: result.job_id, n_chains: chains.length, depth, pair, url }, lines.join('\n'));
76
+ }
77
+ function collectArg(value, prev) {
78
+ return [...prev, value];
79
+ }
80
+ function resolveSequence(input) {
81
+ // @path → read file (FASTA-aware: strip header/whitespace)
82
+ if (input.startsWith('@')) {
83
+ const path = input.slice(1);
84
+ if (!fs.existsSync(path)) {
85
+ throw new Error(`Sequence file not found: ${path}`);
86
+ }
87
+ const raw = fs.readFileSync(path, 'utf-8');
88
+ return raw.split('\n').filter(line => !line.startsWith('>')).join('').replace(/\s+/g, '').toUpperCase();
89
+ }
90
+ return input.replace(/\s+/g, '').toUpperCase();
91
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "m3triq",
3
- "version": "0.2.10",
3
+ "version": "0.2.11",
4
4
  "description": "M3TRIQ \u2014 protein-ligand analysis from the terminal",
5
5
  "type": "module",
6
6
  "bin": {