m3triq 0.1.0 → 0.2.1
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 +129 -0
- package/dist/cli.js +29 -2
- package/dist/client.d.ts +16 -1
- package/dist/client.js +57 -0
- package/dist/commands/admet.d.ts +2 -0
- package/dist/commands/admet.js +134 -0
- package/dist/commands/credits.d.ts +2 -0
- package/dist/commands/credits.js +97 -0
- package/dist/commands/docking.js +10 -7
- package/dist/commands/foodb.d.ts +2 -0
- package/dist/commands/foodb.js +151 -0
- package/dist/commands/md.d.ts +2 -0
- package/dist/commands/md.js +107 -0
- package/dist/commands/predict.js +135 -4
- package/dist/commands/pricing.d.ts +2 -0
- package/dist/commands/pricing.js +36 -0
- package/dist/commands/projects.js +47 -0
- package/dist/commands/sessions.d.ts +2 -0
- package/dist/commands/sessions.js +98 -0
- package/dist/commands/zinc.d.ts +2 -0
- package/dist/commands/zinc.js +122 -0
- package/dist/types.d.ts +119 -0
- package/package.json +6 -3
package/README.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# m3t
|
|
2
|
+
|
|
3
|
+
The M3TRIQ CLI — protein-ligand analysis from the terminal.
|
|
4
|
+
|
|
5
|
+
Query self-hosted databases (ChEMBL, FooDB, ZINC), predict ADMET properties, run molecular docking, and submit molecular dynamics simulations — all from the command line.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g m3triq
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Setup
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
m3t config --key <your-api-key>
|
|
17
|
+
m3t projects # list your projects
|
|
18
|
+
m3t use <project-id> # set active project
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Databases
|
|
22
|
+
|
|
23
|
+
### ChEMBL (2.4M compounds, self-hosted)
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
m3t chembl search aspirin
|
|
27
|
+
m3t chembl info CHEMBL25
|
|
28
|
+
m3t chembl targets PDE3B
|
|
29
|
+
m3t chembl binders EGFR --max-value 1000 --save "EGFR Binders"
|
|
30
|
+
m3t chembl activities CHEMBL25 --type IC50
|
|
31
|
+
m3t chembl similar "CC(=O)Oc1ccccc1C(=O)O" --threshold 0.7
|
|
32
|
+
m3t chembl sql "SELECT chembl_id, pref_name FROM molecule_dictionary WHERE pref_name ILIKE '%caffeine%'"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### FooDB (food compounds, self-hosted)
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
m3t foodb search tomato --limit 20
|
|
39
|
+
m3t foodb info caffeine
|
|
40
|
+
m3t foodb export banana --name "Banana Compounds"
|
|
41
|
+
m3t foodb random --count 50 --group Fruits
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### ZINC (14M purchasable compounds, self-hosted)
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
m3t zinc search --subset drug-like --mw-min 200 --mw-max 500
|
|
48
|
+
m3t zinc info ZINC000000000001
|
|
49
|
+
m3t zinc random --count 20 --subset lead-like
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## ADMET Prediction (41 properties)
|
|
53
|
+
|
|
54
|
+
Self-hosted ADMET-AI model. Predicts absorption, distribution, metabolism, excretion, and toxicity.
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
m3t admet predict "CC(=O)Oc1ccccc1C(=O)O"
|
|
58
|
+
m3t admet batch "CCO" "CC(=O)Oc1ccccc1C(=O)O" "CN1C=NC2=C1C(=O)N(C(=O)N2C)C"
|
|
59
|
+
m3t admet compare "CCO" "CCCO" --name1 Ethanol --name2 Propanol
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Molecular Docking
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# Single compound
|
|
66
|
+
m3t dock gnina "CCO" 5NJ8 --cx 10 --cy 20 --cz 30
|
|
67
|
+
m3t dock vina "CCO" 5NJ8 --cx 10 --cy 20 --cz 30
|
|
68
|
+
m3t dock diffdock "CCO" 5NJ8
|
|
69
|
+
|
|
70
|
+
# Batch (CSV with smiles column)
|
|
71
|
+
m3t batch gnina compounds.csv 5NJ8 --cx 10 --cy 20 --cz 30
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Molecular Dynamics
|
|
75
|
+
|
|
76
|
+
GPU-accelerated simulations to validate docking poses.
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
m3t md run --protein 5NJ8 --ligand-smiles "CCO" --mode quick
|
|
80
|
+
m3t md run --diffdock-job <job-id> --mode standard
|
|
81
|
+
m3t md results <job-id>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Structure Prediction
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
m3t predict esmfold MKFLILLFNILCL... # Fast (~10s, max 1024aa)
|
|
88
|
+
m3t predict alphafold2 MKFLILLFNILCL... # Accurate (~5min, max 2048aa)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Protein Design
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
m3t design rfantibody 6VXX --hotspots A100,A105 --type nanobody --designs 3
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Other Commands
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
m3t jobs # list recent jobs
|
|
101
|
+
m3t job <id> # job status & results
|
|
102
|
+
m3t data # list project datasets
|
|
103
|
+
m3t dataset <id> --limit 20 # view dataset rows
|
|
104
|
+
m3t run script.py --packages pandas,httpx --save "Results"
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## JSON Mode
|
|
108
|
+
|
|
109
|
+
Pipe-friendly output for scripting:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
m3t --json chembl binders PDE3B | jq '.[].canonical_smiles'
|
|
113
|
+
m3t --json zinc search --subset drug-like | jq length
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Project-per-folder
|
|
117
|
+
|
|
118
|
+
Like `.git`, each directory can be tied to a different M3TRIQ project:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
cd ~/research/egfr && m3t use <project-id> # writes .m3triq here
|
|
122
|
+
cd ~/research/pde3b && m3t use <project-id> # different project
|
|
123
|
+
m3t use --global <id> # fallback
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Links
|
|
127
|
+
|
|
128
|
+
- [M3TRIQ Platform](https://console.m3triq.com)
|
|
129
|
+
- [Documentation](https://m3triq.com)
|
package/dist/cli.js
CHANGED
|
@@ -5,6 +5,7 @@ import { getEffectiveConfig, requireApiKey } from './config.js';
|
|
|
5
5
|
import { M3triqClient, AgentsClient } from './client.js';
|
|
6
6
|
import { registerConfigCommands } from './commands/config-cmd.js';
|
|
7
7
|
import { registerProjectCommands } from './commands/projects.js';
|
|
8
|
+
import { registerSessionCommands } from './commands/sessions.js';
|
|
8
9
|
import { registerJobCommands } from './commands/jobs.js';
|
|
9
10
|
import { registerDockingCommands } from './commands/docking.js';
|
|
10
11
|
import { registerPredictCommands } from './commands/predict.js';
|
|
@@ -12,11 +13,17 @@ import { registerChemblCommands } from './commands/chembl.js';
|
|
|
12
13
|
import { registerSandboxCommands } from './commands/sandbox.js';
|
|
13
14
|
import { registerDesignCommands } from './commands/design.js';
|
|
14
15
|
import { registerDataCommands } from './commands/data.js';
|
|
16
|
+
import { registerFoodbCommands } from './commands/foodb.js';
|
|
17
|
+
import { registerAdmetCommands } from './commands/admet.js';
|
|
18
|
+
import { registerMdCommands } from './commands/md.js';
|
|
19
|
+
import { registerZincCommands } from './commands/zinc.js';
|
|
20
|
+
import { registerCreditsCommands } from './commands/credits.js';
|
|
21
|
+
import { registerPricingCommands } from './commands/pricing.js';
|
|
15
22
|
const program = new Command();
|
|
16
23
|
program
|
|
17
24
|
.name('m3t')
|
|
18
25
|
.description('M3TRIQ — protein-ligand analysis from the terminal')
|
|
19
|
-
.version('0.1
|
|
26
|
+
.version('0.2.1')
|
|
20
27
|
.option('--json', 'Output as JSON (machine-readable)')
|
|
21
28
|
.hook('preAction', (thisCommand) => {
|
|
22
29
|
const opts = thisCommand.optsWithGlobals();
|
|
@@ -25,6 +32,7 @@ program
|
|
|
25
32
|
});
|
|
26
33
|
registerConfigCommands(program);
|
|
27
34
|
registerProjectCommands(program);
|
|
35
|
+
registerSessionCommands(program);
|
|
28
36
|
registerJobCommands(program);
|
|
29
37
|
registerDockingCommands(program);
|
|
30
38
|
registerPredictCommands(program);
|
|
@@ -32,7 +40,26 @@ registerChemblCommands(program);
|
|
|
32
40
|
registerSandboxCommands(program);
|
|
33
41
|
registerDesignCommands(program);
|
|
34
42
|
registerDataCommands(program);
|
|
35
|
-
program
|
|
43
|
+
registerFoodbCommands(program);
|
|
44
|
+
registerAdmetCommands(program);
|
|
45
|
+
registerMdCommands(program);
|
|
46
|
+
registerZincCommands(program);
|
|
47
|
+
registerCreditsCommands(program);
|
|
48
|
+
registerPricingCommands(program);
|
|
49
|
+
// Catch async errors from all command actions
|
|
50
|
+
program.parseAsync().catch((err) => {
|
|
51
|
+
const message = err.message || String(err);
|
|
52
|
+
if (message.includes('API error') || message.includes('MCP')) {
|
|
53
|
+
process.stderr.write(`Error: ${message}\n`);
|
|
54
|
+
}
|
|
55
|
+
else if (message.includes('fetch failed') || message.includes('ECONNREFUSED')) {
|
|
56
|
+
process.stderr.write(`Error: Could not reach M3TRIQ API. Check your connection and config.\n`);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
process.stderr.write(`Error: ${message}\n`);
|
|
60
|
+
}
|
|
61
|
+
process.exit(1);
|
|
62
|
+
});
|
|
36
63
|
/** Create an API client from current config. Call lazily in command handlers. */
|
|
37
64
|
export function createClient() {
|
|
38
65
|
const config = getEffectiveConfig();
|
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Job, Project, DockingParams, BatchDockingParams, DiffDockParams, StructurePredictionParams, SandboxParams, McpCallResult } from './types.js';
|
|
1
|
+
import type { Job, Project, ChatSession, DockingParams, BatchDockingParams, DiffDockParams, StructurePredictionParams, SandboxParams, McpCallResult, CreditQuota, CreditLogResponse, Boltz2JobParams, PricingCatalog } from './types.js';
|
|
2
2
|
export declare class M3triqClient {
|
|
3
3
|
private baseUrl;
|
|
4
4
|
private apiKey;
|
|
@@ -6,6 +6,9 @@ export declare class M3triqClient {
|
|
|
6
6
|
private get headers();
|
|
7
7
|
private request;
|
|
8
8
|
listProjects(): Promise<Project[]>;
|
|
9
|
+
getProject(projectId: string): Promise<Project>;
|
|
10
|
+
listSessions(projectId: string, limit?: number): Promise<ChatSession[]>;
|
|
11
|
+
getSession(sessionId: string): Promise<ChatSession>;
|
|
9
12
|
getJob(jobId: string): Promise<Job>;
|
|
10
13
|
listJobs(projectId: string, limit?: number): Promise<Job[]>;
|
|
11
14
|
createDockingJob(params: DockingParams): Promise<{
|
|
@@ -36,9 +39,21 @@ export declare class M3triqClient {
|
|
|
36
39
|
createStructurePrediction(params: StructurePredictionParams): Promise<{
|
|
37
40
|
job_id: string;
|
|
38
41
|
}>;
|
|
42
|
+
createBoltz2Job(params: Boltz2JobParams): Promise<{
|
|
43
|
+
job_id: string;
|
|
44
|
+
}>;
|
|
39
45
|
createSandboxJob(params: SandboxParams): Promise<{
|
|
40
46
|
job_id: string;
|
|
41
47
|
}>;
|
|
48
|
+
getCredits(): Promise<CreditQuota>;
|
|
49
|
+
getCreditLog(params?: {
|
|
50
|
+
page?: number;
|
|
51
|
+
pageSize?: number;
|
|
52
|
+
event?: string;
|
|
53
|
+
since?: string;
|
|
54
|
+
until?: string;
|
|
55
|
+
}): Promise<CreditLogResponse>;
|
|
56
|
+
getPricing(): Promise<PricingCatalog>;
|
|
42
57
|
}
|
|
43
58
|
/**
|
|
44
59
|
* Client for the agents service (MCP tool calls).
|
package/dist/client.js
CHANGED
|
@@ -28,6 +28,17 @@ export class M3triqClient {
|
|
|
28
28
|
const data = await this.request('GET', '/api/membership/projects/');
|
|
29
29
|
return Array.isArray(data) ? data : (data.results || []);
|
|
30
30
|
}
|
|
31
|
+
async getProject(projectId) {
|
|
32
|
+
return this.request('GET', `/api/membership/projects/${projectId}/`);
|
|
33
|
+
}
|
|
34
|
+
// ── Chat Sessions ─────────────────────────────────────────────
|
|
35
|
+
async listSessions(projectId, limit = 20) {
|
|
36
|
+
const data = await this.request('GET', `/api/membership/sessions/?project=${projectId}&page_size=${limit}`);
|
|
37
|
+
return Array.isArray(data) ? data : (data.results || []);
|
|
38
|
+
}
|
|
39
|
+
async getSession(sessionId) {
|
|
40
|
+
return this.request('GET', `/api/membership/sessions/${sessionId}/`);
|
|
41
|
+
}
|
|
31
42
|
async getJob(jobId) {
|
|
32
43
|
return this.request('GET', `/api/jobs/${jobId}/`);
|
|
33
44
|
}
|
|
@@ -94,9 +105,55 @@ export class M3triqClient {
|
|
|
94
105
|
: '/api/jobs/create_esmfold_prediction/';
|
|
95
106
|
return this.request('POST', endpoint, params);
|
|
96
107
|
}
|
|
108
|
+
async createBoltz2Job(params) {
|
|
109
|
+
// Boltz-2 prediction is run client-side via the agents MCP, then saved here as a completed job.
|
|
110
|
+
// Mirrors the RFantibody internal flow but with status='completed' and result_data already populated.
|
|
111
|
+
const url = `${this.baseUrl}/api/jobs/create_prediction_internal/`;
|
|
112
|
+
const res = await fetch(url, {
|
|
113
|
+
method: 'POST',
|
|
114
|
+
headers: {
|
|
115
|
+
...this.headers,
|
|
116
|
+
'X-Internal-Service': 'true',
|
|
117
|
+
},
|
|
118
|
+
body: JSON.stringify({
|
|
119
|
+
project_id: params.project_id,
|
|
120
|
+
job_type: 'boltz2_prediction',
|
|
121
|
+
title: params.title,
|
|
122
|
+
description: params.description ?? '',
|
|
123
|
+
result_data: params.result_data,
|
|
124
|
+
}),
|
|
125
|
+
});
|
|
126
|
+
if (!res.ok) {
|
|
127
|
+
const text = await res.text();
|
|
128
|
+
throw new Error(`API error ${res.status}: ${text.substring(0, 200)}`);
|
|
129
|
+
}
|
|
130
|
+
return res.json();
|
|
131
|
+
}
|
|
97
132
|
async createSandboxJob(params) {
|
|
98
133
|
return this.request('POST', '/api/jobs/create_sandbox_job/', params);
|
|
99
134
|
}
|
|
135
|
+
// ── Credits ──────────────────────────────────────────────────
|
|
136
|
+
async getCredits() {
|
|
137
|
+
return this.request('GET', '/api/membership/user/credits/');
|
|
138
|
+
}
|
|
139
|
+
async getCreditLog(params = {}) {
|
|
140
|
+
const q = new URLSearchParams();
|
|
141
|
+
if (params.page)
|
|
142
|
+
q.set('page', String(params.page));
|
|
143
|
+
if (params.pageSize)
|
|
144
|
+
q.set('page_size', String(params.pageSize));
|
|
145
|
+
if (params.event)
|
|
146
|
+
q.set('event', params.event);
|
|
147
|
+
if (params.since)
|
|
148
|
+
q.set('since', params.since);
|
|
149
|
+
if (params.until)
|
|
150
|
+
q.set('until', params.until);
|
|
151
|
+
const qs = q.toString();
|
|
152
|
+
return this.request('GET', `/api/membership/user/credits/log/${qs ? `?${qs}` : ''}`);
|
|
153
|
+
}
|
|
154
|
+
async getPricing() {
|
|
155
|
+
return this.request('GET', '/api/membership/billing/pricing/');
|
|
156
|
+
}
|
|
100
157
|
}
|
|
101
158
|
/**
|
|
102
159
|
* Client for the agents service (MCP tool calls).
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { output } from '../output.js';
|
|
2
|
+
import { createAgentsClient } from '../cli.js';
|
|
3
|
+
import { requireApiKey } from '../config.js';
|
|
4
|
+
export function registerAdmetCommands(program) {
|
|
5
|
+
const admet = program
|
|
6
|
+
.command('admet')
|
|
7
|
+
.description('Predict ADMET properties (self-hosted ADMET-AI, 41 properties)')
|
|
8
|
+
.hook('preAction', () => { requireApiKey(); });
|
|
9
|
+
// ── m3t admet predict <smiles> ────────────────────────────────
|
|
10
|
+
admet
|
|
11
|
+
.command('predict')
|
|
12
|
+
.argument('<smiles>', 'SMILES string of compound')
|
|
13
|
+
.description('Predict ADMET properties for a single compound')
|
|
14
|
+
.action(async (smiles) => {
|
|
15
|
+
const agents = createAgentsClient();
|
|
16
|
+
process.stderr.write('Predicting ADMET properties...\n');
|
|
17
|
+
const data = await agents.callMcpTool('admet', 'predict_admet', { smiles });
|
|
18
|
+
if (typeof data.result === 'string') {
|
|
19
|
+
output(data, data.result);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
// Try to format structured ADMET results
|
|
23
|
+
const props = extractProperties(data);
|
|
24
|
+
if (props) {
|
|
25
|
+
output(data, formatAdmetResult(smiles, props));
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
output(data, JSON.stringify(data, null, 2));
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
// ── m3t admet batch <smiles...> ───────────────────────────────
|
|
32
|
+
admet
|
|
33
|
+
.command('batch')
|
|
34
|
+
.argument('<smiles...>', 'SMILES strings (space-separated, max 100)')
|
|
35
|
+
.description('Predict ADMET for multiple compounds')
|
|
36
|
+
.action(async (smilesList) => {
|
|
37
|
+
if (smilesList.length > 100) {
|
|
38
|
+
process.stderr.write('Error: Maximum 100 compounds per batch.\n');
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
const agents = createAgentsClient();
|
|
42
|
+
process.stderr.write(`Predicting ADMET for ${smilesList.length} compounds...\n`);
|
|
43
|
+
const data = await agents.callMcpTool('admet', 'predict_admet_batch', {
|
|
44
|
+
smiles_list: smilesList,
|
|
45
|
+
});
|
|
46
|
+
if (typeof data.result === 'string') {
|
|
47
|
+
output(data, data.result);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
output(data, JSON.stringify(data, null, 2));
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
// ── m3t admet compare <smiles1> <smiles2> ─────────────────────
|
|
54
|
+
admet
|
|
55
|
+
.command('compare')
|
|
56
|
+
.argument('<smiles1>', 'SMILES of first compound')
|
|
57
|
+
.argument('<smiles2>', 'SMILES of second compound')
|
|
58
|
+
.option('--name1 <n>', 'Name for compound 1', 'Compound 1')
|
|
59
|
+
.option('--name2 <n>', 'Name for compound 2', 'Compound 2')
|
|
60
|
+
.description('Compare ADMET profiles of two compounds side-by-side')
|
|
61
|
+
.action(async (smiles1, smiles2, opts) => {
|
|
62
|
+
const agents = createAgentsClient();
|
|
63
|
+
process.stderr.write(`Comparing ${opts.name1} vs ${opts.name2}...\n`);
|
|
64
|
+
const data = await agents.callMcpTool('admet', 'compare_admet', {
|
|
65
|
+
smiles1,
|
|
66
|
+
smiles2,
|
|
67
|
+
name1: opts.name1,
|
|
68
|
+
name2: opts.name2,
|
|
69
|
+
});
|
|
70
|
+
if (typeof data.result === 'string') {
|
|
71
|
+
output(data, data.result);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
output(data, JSON.stringify(data, null, 2));
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
// ── Helpers ───────────────────────────────────────────────────────
|
|
79
|
+
function extractProperties(data) {
|
|
80
|
+
if (data.predictions)
|
|
81
|
+
return data.predictions;
|
|
82
|
+
if (data.properties)
|
|
83
|
+
return data.properties;
|
|
84
|
+
if (data.admet)
|
|
85
|
+
return data.admet;
|
|
86
|
+
if (typeof data.result === 'object' && data.result !== null) {
|
|
87
|
+
return extractProperties(data.result);
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
function formatAdmetResult(smiles, props) {
|
|
92
|
+
const lines = [`ADMET Prediction: ${smiles}`, ''];
|
|
93
|
+
// Group properties by category if they follow ADMET naming
|
|
94
|
+
const categories = {};
|
|
95
|
+
for (const [key, value] of Object.entries(props)) {
|
|
96
|
+
const category = categorize(key);
|
|
97
|
+
if (!categories[category])
|
|
98
|
+
categories[category] = [];
|
|
99
|
+
categories[category].push([key, formatValue(value)]);
|
|
100
|
+
}
|
|
101
|
+
for (const [category, entries] of Object.entries(categories)) {
|
|
102
|
+
lines.push(`── ${category} ──`);
|
|
103
|
+
for (const [key, val] of entries) {
|
|
104
|
+
lines.push(` ${key.padEnd(35)} ${val}`);
|
|
105
|
+
}
|
|
106
|
+
lines.push('');
|
|
107
|
+
}
|
|
108
|
+
return lines.join('\n');
|
|
109
|
+
}
|
|
110
|
+
function categorize(key) {
|
|
111
|
+
const k = key.toLowerCase();
|
|
112
|
+
if (k.includes('caco') || k.includes('pampa') || k.includes('hia') || k.includes('pgp') || k.includes('bioavail') || k.includes('solub') || k.includes('logd') || k.includes('hydra'))
|
|
113
|
+
return 'Absorption';
|
|
114
|
+
if (k.includes('bbb') || k.includes('ppb') || k.includes('vd') || k.includes('distribution'))
|
|
115
|
+
return 'Distribution';
|
|
116
|
+
if (k.includes('cyp') || k.includes('metabol'))
|
|
117
|
+
return 'Metabolism';
|
|
118
|
+
if (k.includes('half_life') || k.includes('clearance') || k.includes('excret'))
|
|
119
|
+
return 'Excretion';
|
|
120
|
+
if (k.includes('herg') || k.includes('ames') || k.includes('dili') || k.includes('ld50') || k.includes('toxic') || k.includes('carcin') || k.includes('skin') || k.includes('nr_'))
|
|
121
|
+
return 'Toxicity';
|
|
122
|
+
if (k.includes('mw') || k.includes('logp') || k.includes('hba') || k.includes('hbd') || k.includes('tpsa') || k.includes('lipinski') || k.includes('qed'))
|
|
123
|
+
return 'Physicochemical';
|
|
124
|
+
return 'Other';
|
|
125
|
+
}
|
|
126
|
+
function formatValue(val) {
|
|
127
|
+
if (val === null || val === undefined)
|
|
128
|
+
return '-';
|
|
129
|
+
if (typeof val === 'number')
|
|
130
|
+
return val.toFixed(4);
|
|
131
|
+
if (typeof val === 'boolean')
|
|
132
|
+
return val ? 'PASS' : 'FAIL';
|
|
133
|
+
return String(val);
|
|
134
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { createClient } from '../cli.js';
|
|
2
|
+
import { output, formatTable, error } from '../output.js';
|
|
3
|
+
function formatPercentage(pct) {
|
|
4
|
+
return `${pct.toFixed(1)}%`;
|
|
5
|
+
}
|
|
6
|
+
function progressBar(used, total, width = 30) {
|
|
7
|
+
if (total <= 0)
|
|
8
|
+
return '';
|
|
9
|
+
const ratio = Math.min(1, used / total);
|
|
10
|
+
const filled = Math.round(ratio * width);
|
|
11
|
+
const empty = width - filled;
|
|
12
|
+
return `[${'█'.repeat(filled)}${'░'.repeat(empty)}]`;
|
|
13
|
+
}
|
|
14
|
+
function formatCredits(n) {
|
|
15
|
+
return n.toLocaleString();
|
|
16
|
+
}
|
|
17
|
+
function shortJobId(jobId) {
|
|
18
|
+
if (!jobId)
|
|
19
|
+
return '';
|
|
20
|
+
return jobId.split('-')[0] || jobId.substring(0, 8);
|
|
21
|
+
}
|
|
22
|
+
export function registerCreditsCommands(program) {
|
|
23
|
+
const credits = program
|
|
24
|
+
.command('credits')
|
|
25
|
+
.description('Show your account credit balance')
|
|
26
|
+
.action(async () => {
|
|
27
|
+
const client = createClient();
|
|
28
|
+
let q;
|
|
29
|
+
try {
|
|
30
|
+
q = await client.getCredits();
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
error(e.message);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const remainingClass = q.is_quota_exceeded ? '!! EXHAUSTED' : '';
|
|
38
|
+
const periodEnd = q.period_end ? new Date(q.period_end).toLocaleDateString() : '—';
|
|
39
|
+
const lines = [
|
|
40
|
+
`Tier: ${q.tier}`,
|
|
41
|
+
`Used: ${formatCredits(q.credits_used)} / ${formatCredits(q.monthly_credits)} ${progressBar(q.credits_used, q.monthly_credits)} ${formatPercentage(q.usage_percentage || 0)}`,
|
|
42
|
+
`Top-up: ${formatCredits(q.topup_credits)}`,
|
|
43
|
+
`Remaining: ${formatCredits(q.credits_remaining)} ${remainingClass}`.trimEnd(),
|
|
44
|
+
`Resets: ${periodEnd}`,
|
|
45
|
+
];
|
|
46
|
+
output(q, lines.join('\n'));
|
|
47
|
+
});
|
|
48
|
+
credits
|
|
49
|
+
.command('log')
|
|
50
|
+
.description('Show your credit ledger (paginated)')
|
|
51
|
+
.option('--event <type>', 'Filter to event type: reserve|tokens|refund|reconcile|topup|reset')
|
|
52
|
+
.option('--since <date>', 'Events on or after this ISO date (yyyy-mm-dd)')
|
|
53
|
+
.option('--until <date>', 'Events on or before this ISO date (yyyy-mm-dd)')
|
|
54
|
+
.option('--page <n>', 'Page number (default 1)', '1')
|
|
55
|
+
.option('--limit <n>', 'Results per page (default 25, max 200)', '25')
|
|
56
|
+
.action(async (opts) => {
|
|
57
|
+
const client = createClient();
|
|
58
|
+
let response;
|
|
59
|
+
try {
|
|
60
|
+
response = await client.getCreditLog({
|
|
61
|
+
page: parseInt(opts.page, 10),
|
|
62
|
+
pageSize: parseInt(opts.limit, 10),
|
|
63
|
+
event: opts.event,
|
|
64
|
+
since: opts.since,
|
|
65
|
+
until: opts.until,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
error(e.message);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (response.results.length === 0) {
|
|
74
|
+
output(response, 'No credit events match these filters.');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const rows = response.results.map(evt => {
|
|
78
|
+
const detail = evt.note
|
|
79
|
+
|| (evt.metadata && (evt.metadata.job_type || evt.metadata.model))
|
|
80
|
+
|| '';
|
|
81
|
+
const sign = evt.credits > 0 ? '-' : '+';
|
|
82
|
+
const date = new Date(evt.created_at).toISOString().replace('T', ' ').substring(0, 16);
|
|
83
|
+
return [
|
|
84
|
+
date,
|
|
85
|
+
evt.event,
|
|
86
|
+
`${sign}${Math.abs(evt.credits).toLocaleString()}`,
|
|
87
|
+
detail.toString().substring(0, 50),
|
|
88
|
+
shortJobId(evt.job_id),
|
|
89
|
+
];
|
|
90
|
+
});
|
|
91
|
+
const page = parseInt(opts.page, 10);
|
|
92
|
+
const limit = parseInt(opts.limit, 10);
|
|
93
|
+
const totalPages = Math.max(1, Math.ceil(response.count / limit));
|
|
94
|
+
const footer = `\nPage ${page} of ${totalPages} • ${response.count.toLocaleString()} total events`;
|
|
95
|
+
output(response, formatTable(['Date (UTC)', 'Event', 'Credits', 'Detail', 'Job'], rows) + footer);
|
|
96
|
+
});
|
|
97
|
+
}
|
package/dist/commands/docking.js
CHANGED
|
@@ -52,7 +52,7 @@ export function registerDockingCommands(program) {
|
|
|
52
52
|
.argument('<smiles>', 'SMILES string of the ligand')
|
|
53
53
|
.argument('<pdb>', 'PDB ID (e.g., 5NJ8) or path to .pdb file')
|
|
54
54
|
.option('--exhaustiveness <n>', 'Search exhaustiveness', parseInt)
|
|
55
|
-
.description('Dock with GNINA CNN scoring (
|
|
55
|
+
.description('Dock with GNINA CNN scoring (GCP T4 GPU, ~3-4 min/job incl. GPU spin-up)')).action(async (smiles, pdb, opts) => {
|
|
56
56
|
await runSiteDock(smiles, pdb, 'gnina', opts);
|
|
57
57
|
});
|
|
58
58
|
// m3t dock vina
|
|
@@ -60,7 +60,7 @@ export function registerDockingCommands(program) {
|
|
|
60
60
|
.argument('<smiles>', 'SMILES string of the ligand')
|
|
61
61
|
.argument('<pdb>', 'PDB ID (e.g., 5NJ8) or path to .pdb file')
|
|
62
62
|
.option('--exhaustiveness <n>', 'Search exhaustiveness', parseInt)
|
|
63
|
-
.description('Dock with AutoDock Vina (Cloud Run CPU, ~
|
|
63
|
+
.description('Dock with AutoDock Vina (Cloud Run CPU, ~2-3 min/compound)')).action(async (smiles, pdb, opts) => {
|
|
64
64
|
await runSiteDock(smiles, pdb, 'vina', opts);
|
|
65
65
|
});
|
|
66
66
|
// m3t dock diffdock
|
|
@@ -91,14 +91,14 @@ export function registerDockingCommands(program) {
|
|
|
91
91
|
addSiteOptions(batch.command('gnina')
|
|
92
92
|
.argument('<file>', 'Compounds file (CSV/JSON, - for stdin)')
|
|
93
93
|
.argument('<pdb>', 'PDB ID (e.g., 5NJ8) or path to .pdb file')
|
|
94
|
-
.description('Batch dock with GNINA (~
|
|
94
|
+
.description('Batch dock with GNINA (GCP T4 GPU, ~3-4 min/job for small batches)')).action(async (file, pdb, opts) => {
|
|
95
95
|
await runBatchDock(file, pdb, 'gnina', opts);
|
|
96
96
|
});
|
|
97
97
|
// m3t batch vina
|
|
98
98
|
addSiteOptions(batch.command('vina')
|
|
99
99
|
.argument('<file>', 'Compounds file (CSV/JSON, - for stdin)')
|
|
100
100
|
.argument('<pdb>', 'PDB ID (e.g., 5NJ8) or path to .pdb file')
|
|
101
|
-
.description('Batch dock with Vina (~
|
|
101
|
+
.description('Batch dock with Vina (Cloud Run CPU, ~2-3 min/compound)')).action(async (file, pdb, opts) => {
|
|
102
102
|
await runBatchDock(file, pdb, 'vina', opts);
|
|
103
103
|
});
|
|
104
104
|
}
|
|
@@ -124,7 +124,7 @@ async function runSiteDock(smiles, pdb, method, opts) {
|
|
|
124
124
|
});
|
|
125
125
|
const url = jobUrl(consoleUrl, project.id, result.job_id);
|
|
126
126
|
maybeOpenBrowser(url);
|
|
127
|
-
const timeEst = method === 'gnina' ? '~
|
|
127
|
+
const timeEst = method === 'gnina' ? '~3-4 min (incl. GPU spin-up)' : '~2-3 min';
|
|
128
128
|
const data = { job_id: result.job_id, method, url };
|
|
129
129
|
output(data, `${method.toUpperCase()} docking job created\nJob ID: ${result.job_id.substring(0, 8)}\nEstimated: ${timeEst}`);
|
|
130
130
|
}
|
|
@@ -150,8 +150,11 @@ async function runBatchDock(file, pdb, method, opts) {
|
|
|
150
150
|
});
|
|
151
151
|
const url = jobUrl(consoleUrl, project.id, result.job_id);
|
|
152
152
|
maybeOpenBrowser(url);
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
// GNINA is provisioning-bound (~constant per job for small batches); Vina
|
|
154
|
+
// scales per compound on CPU.
|
|
155
|
+
const totalMin = method === 'gnina'
|
|
156
|
+
? Math.max(4, Math.ceil(compounds.length * 0.05))
|
|
157
|
+
: Math.ceil(compounds.length * 2.5);
|
|
155
158
|
const data = { job_id: result.job_id, compounds: compounds.length, method, url };
|
|
156
159
|
output(data, `${method.toUpperCase()} batch docking created\nJob ID: ${result.job_id.substring(0, 8)}\nCompounds: ${compounds.length}\nEstimated: ~${totalMin} minutes`);
|
|
157
160
|
}
|