m3triq 0.1.0 → 0.2.0
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 +24 -1
- package/dist/client.d.ts +4 -1
- package/dist/client.js +11 -0
- package/dist/commands/admet.d.ts +2 -0
- package/dist/commands/admet.js +134 -0
- 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/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 +34 -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,6 +13,10 @@ 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';
|
|
15
20
|
const program = new Command();
|
|
16
21
|
program
|
|
17
22
|
.name('m3t')
|
|
@@ -25,6 +30,7 @@ program
|
|
|
25
30
|
});
|
|
26
31
|
registerConfigCommands(program);
|
|
27
32
|
registerProjectCommands(program);
|
|
33
|
+
registerSessionCommands(program);
|
|
28
34
|
registerJobCommands(program);
|
|
29
35
|
registerDockingCommands(program);
|
|
30
36
|
registerPredictCommands(program);
|
|
@@ -32,7 +38,24 @@ registerChemblCommands(program);
|
|
|
32
38
|
registerSandboxCommands(program);
|
|
33
39
|
registerDesignCommands(program);
|
|
34
40
|
registerDataCommands(program);
|
|
35
|
-
program
|
|
41
|
+
registerFoodbCommands(program);
|
|
42
|
+
registerAdmetCommands(program);
|
|
43
|
+
registerMdCommands(program);
|
|
44
|
+
registerZincCommands(program);
|
|
45
|
+
// Catch async errors from all command actions
|
|
46
|
+
program.parseAsync().catch((err) => {
|
|
47
|
+
const message = err.message || String(err);
|
|
48
|
+
if (message.includes('API error') || message.includes('MCP')) {
|
|
49
|
+
process.stderr.write(`Error: ${message}\n`);
|
|
50
|
+
}
|
|
51
|
+
else if (message.includes('fetch failed') || message.includes('ECONNREFUSED')) {
|
|
52
|
+
process.stderr.write(`Error: Could not reach M3TRIQ API. Check your connection and config.\n`);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
process.stderr.write(`Error: ${message}\n`);
|
|
56
|
+
}
|
|
57
|
+
process.exit(1);
|
|
58
|
+
});
|
|
36
59
|
/** Create an API client from current config. Call lazily in command handlers. */
|
|
37
60
|
export function createClient() {
|
|
38
61
|
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 } 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<{
|
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
|
}
|
|
@@ -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,151 @@
|
|
|
1
|
+
import { output, formatTable } from '../output.js';
|
|
2
|
+
import { createAgentsClient } from '../cli.js';
|
|
3
|
+
import { requireApiKey, requireProject } from '../config.js';
|
|
4
|
+
export function registerFoodbCommands(program) {
|
|
5
|
+
const foodb = program
|
|
6
|
+
.command('foodb')
|
|
7
|
+
.description('Query the M3TRIQ FooDB database (self-hosted, food compounds)')
|
|
8
|
+
.hook('preAction', () => { requireApiKey(); });
|
|
9
|
+
// ── m3t foodb search <food> ───────────────────────────────────
|
|
10
|
+
foodb
|
|
11
|
+
.command('search')
|
|
12
|
+
.argument('<food>', 'Food name (e.g., "tomato", "coffee", "soybean")')
|
|
13
|
+
.option('--category <cat>', 'Filter by category (e.g., flavonoid, alkaloid)')
|
|
14
|
+
.option('--limit <n>', 'Max results', '20')
|
|
15
|
+
.option('--save <name>', 'Save results as project dataset')
|
|
16
|
+
.description('Search compounds in a specific food')
|
|
17
|
+
.action(async (food, opts) => {
|
|
18
|
+
const agents = createAgentsClient();
|
|
19
|
+
const params = {
|
|
20
|
+
food_name: food,
|
|
21
|
+
limit: parseInt(opts.limit),
|
|
22
|
+
auto_save: false,
|
|
23
|
+
};
|
|
24
|
+
if (opts.category)
|
|
25
|
+
params.category = opts.category;
|
|
26
|
+
if (opts.save) {
|
|
27
|
+
const project = requireProject();
|
|
28
|
+
params.auto_save = true;
|
|
29
|
+
params.project_id = project.id;
|
|
30
|
+
}
|
|
31
|
+
const data = await agents.callMcpTool('foodb', 'search_food_compounds', params);
|
|
32
|
+
const compounds = extractArray(data, ['compounds', 'results', 'data']);
|
|
33
|
+
if (compounds.length === 0) {
|
|
34
|
+
// Might be text response
|
|
35
|
+
if (typeof data.result === 'string') {
|
|
36
|
+
output(data, data.result);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
output([], 'No compounds found.');
|
|
40
|
+
}
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const rows = compounds.map(c => [
|
|
44
|
+
str(c.name, 30),
|
|
45
|
+
str(c.smiles || c.canonical_smiles, 40),
|
|
46
|
+
str(c.category, 15),
|
|
47
|
+
str(c.concentration || c.content),
|
|
48
|
+
]);
|
|
49
|
+
output(compounds, formatTable(['Compound', 'SMILES', 'Category', 'Content'], rows));
|
|
50
|
+
if (opts.save)
|
|
51
|
+
process.stderr.write(`Saved as dataset: ${opts.save}\n`);
|
|
52
|
+
});
|
|
53
|
+
// ── m3t foodb info <compound> ─────────────────────────────────
|
|
54
|
+
foodb
|
|
55
|
+
.command('info')
|
|
56
|
+
.argument('<compound>', 'Compound name (e.g., "caffeine", "quercetin")')
|
|
57
|
+
.description('Get detailed compound information')
|
|
58
|
+
.action(async (compound) => {
|
|
59
|
+
const agents = createAgentsClient();
|
|
60
|
+
const data = await agents.callMcpTool('foodb', 'get_compound_info', {
|
|
61
|
+
compound_name: compound,
|
|
62
|
+
});
|
|
63
|
+
if (typeof data.result === 'string') {
|
|
64
|
+
output(data, data.result);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
output(data, JSON.stringify(data, null, 2));
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
// ── m3t foodb export <food> ───────────────────────────────────
|
|
71
|
+
foodb
|
|
72
|
+
.command('export')
|
|
73
|
+
.argument('<food>', 'Food name to export compounds from')
|
|
74
|
+
.option('--limit <n>', 'Max compounds to export', '50')
|
|
75
|
+
.option('--name <name>', 'Dataset name')
|
|
76
|
+
.description('Export food compounds to project as dataset')
|
|
77
|
+
.action(async (food, opts) => {
|
|
78
|
+
const project = requireProject();
|
|
79
|
+
const agents = createAgentsClient();
|
|
80
|
+
const params = {
|
|
81
|
+
food_name: food,
|
|
82
|
+
limit: parseInt(opts.limit),
|
|
83
|
+
project_id: project.id,
|
|
84
|
+
};
|
|
85
|
+
if (opts.name)
|
|
86
|
+
params.dataset_name = opts.name;
|
|
87
|
+
process.stderr.write(`Exporting ${food} compounds to project...\n`);
|
|
88
|
+
const data = await agents.callMcpTool('foodb', 'export_compounds_to_project', params);
|
|
89
|
+
if (typeof data.result === 'string') {
|
|
90
|
+
output(data, data.result);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
output(data, JSON.stringify(data, null, 2));
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
// ── m3t foodb random ──────────────────────────────────────────
|
|
97
|
+
foodb
|
|
98
|
+
.command('random')
|
|
99
|
+
.option('--count <n>', 'Number of compounds', '20')
|
|
100
|
+
.option('--group <g>', 'Food group filter (e.g., Vegetables, Fruits, "Herbs and Spices")')
|
|
101
|
+
.description('Get random food compounds with SMILES')
|
|
102
|
+
.action(async (opts) => {
|
|
103
|
+
const agents = createAgentsClient();
|
|
104
|
+
const params = {
|
|
105
|
+
count: parseInt(opts.count),
|
|
106
|
+
with_smiles_only: true,
|
|
107
|
+
};
|
|
108
|
+
if (opts.group)
|
|
109
|
+
params.food_group = opts.group;
|
|
110
|
+
const data = await agents.callMcpTool('foodb', 'get_random_compounds', params);
|
|
111
|
+
const compounds = extractArray(data, ['compounds', 'results', 'data']);
|
|
112
|
+
if (compounds.length === 0) {
|
|
113
|
+
if (typeof data.result === 'string') {
|
|
114
|
+
output(data, data.result);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
output([], 'No compounds found.');
|
|
118
|
+
}
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const rows = compounds.map(c => [
|
|
122
|
+
str(c.name, 30),
|
|
123
|
+
str(c.smiles || c.canonical_smiles, 45),
|
|
124
|
+
str(c.food_name || c.food, 20),
|
|
125
|
+
]);
|
|
126
|
+
output(compounds, formatTable(['Compound', 'SMILES', 'Source Food'], rows));
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
// ── Helpers ───────────────────────────────────────────────────────
|
|
130
|
+
function str(val, maxLen) {
|
|
131
|
+
const s = val != null ? String(val) : '-';
|
|
132
|
+
return maxLen ? s.substring(0, maxLen) : s;
|
|
133
|
+
}
|
|
134
|
+
function extractArray(data, keys) {
|
|
135
|
+
if (Array.isArray(data))
|
|
136
|
+
return data;
|
|
137
|
+
for (const key of keys) {
|
|
138
|
+
if (Array.isArray(data[key]))
|
|
139
|
+
return data[key];
|
|
140
|
+
}
|
|
141
|
+
if (typeof data.result === 'string') {
|
|
142
|
+
try {
|
|
143
|
+
return extractArray(JSON.parse(data.result), keys);
|
|
144
|
+
}
|
|
145
|
+
catch { /* not JSON */ }
|
|
146
|
+
}
|
|
147
|
+
if (typeof data.result === 'object' && data.result !== null) {
|
|
148
|
+
return extractArray(data.result, keys);
|
|
149
|
+
}
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { output } from '../output.js';
|
|
3
|
+
import { createAgentsClient } from '../cli.js';
|
|
4
|
+
import { requireApiKey, requireProject } from '../config.js';
|
|
5
|
+
export function registerMdCommands(program) {
|
|
6
|
+
const md = program
|
|
7
|
+
.command('md')
|
|
8
|
+
.description('Run molecular dynamics simulations (GPU-accelerated, OpenMM)')
|
|
9
|
+
.hook('preAction', () => { requireApiKey(); });
|
|
10
|
+
// ── m3t md run ────────────────────────────────────────────────
|
|
11
|
+
md
|
|
12
|
+
.command('run')
|
|
13
|
+
.option('--protein <pdb>', 'Protein PDB ID or path to .pdb file')
|
|
14
|
+
.option('--ligand-sdf <sdf>', 'Ligand SDF content or path to .sdf file')
|
|
15
|
+
.option('--ligand-smiles <smiles>', 'Ligand SMILES (fallback if no SDF)')
|
|
16
|
+
.option('--diffdock-job <id>', 'DiffDock job ID (auto-fetches protein + ligand)')
|
|
17
|
+
.option('--mode <mode>', 'Simulation mode: quick (10ns ~1hr) or standard (50ns ~5hrs)', 'quick')
|
|
18
|
+
.option('--ns <n>', 'Override simulation duration in nanoseconds (1-100)')
|
|
19
|
+
.option('--temperature <K>', 'Temperature in Kelvin', '300')
|
|
20
|
+
.description('Submit a molecular dynamics simulation job')
|
|
21
|
+
.action(async (opts) => {
|
|
22
|
+
const project = requireProject();
|
|
23
|
+
const agents = createAgentsClient();
|
|
24
|
+
const params = {
|
|
25
|
+
mode: opts.mode,
|
|
26
|
+
temperature_k: parseFloat(opts.temperature),
|
|
27
|
+
project_id: project.id,
|
|
28
|
+
};
|
|
29
|
+
// Input source: DiffDock job OR protein + ligand
|
|
30
|
+
if (opts.diffdockJob) {
|
|
31
|
+
params.diffdock_job_id = opts.diffdockJob;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
if (!opts.protein) {
|
|
35
|
+
process.stderr.write('Error: Provide --protein <pdb> or --diffdock-job <id>\n');
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
// Resolve protein: file path or PDB ID
|
|
39
|
+
if (fs.existsSync(opts.protein)) {
|
|
40
|
+
params.protein_pdb = fs.readFileSync(opts.protein, 'utf-8');
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
params.protein_pdb = opts.protein;
|
|
44
|
+
}
|
|
45
|
+
// Resolve ligand
|
|
46
|
+
if (opts.ligandSdf) {
|
|
47
|
+
if (fs.existsSync(opts.ligandSdf)) {
|
|
48
|
+
params.ligand_sdf = fs.readFileSync(opts.ligandSdf, 'utf-8');
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
params.ligand_sdf = opts.ligandSdf;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
else if (opts.ligandSmiles) {
|
|
55
|
+
params.ligand_smiles = opts.ligandSmiles;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
process.stderr.write('Error: Provide --ligand-sdf or --ligand-smiles\n');
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (opts.ns)
|
|
63
|
+
params.simulation_ns = parseFloat(opts.ns);
|
|
64
|
+
process.stderr.write(`Submitting MD simulation (${opts.mode} mode)...\n`);
|
|
65
|
+
const data = await agents.callMcpTool('md', 'run_md_simulation', params);
|
|
66
|
+
// Extract job ID from response
|
|
67
|
+
const jobId = extractJobId(data);
|
|
68
|
+
if (jobId) {
|
|
69
|
+
output({ job_id: jobId, mode: opts.mode }, `MD simulation submitted\nJob ID: ${jobId.substring(0, 8)}\nMode: ${opts.mode}\nCheck status: m3t job ${jobId.substring(0, 8)}`);
|
|
70
|
+
}
|
|
71
|
+
else if (typeof data.result === 'string') {
|
|
72
|
+
output(data, data.result);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
output(data, JSON.stringify(data, null, 2));
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
// ── m3t md results <job_id> ───────────────────────────────────
|
|
79
|
+
md
|
|
80
|
+
.command('results')
|
|
81
|
+
.argument('<job_id>', 'MD simulation job ID')
|
|
82
|
+
.description('Get results from a completed MD simulation')
|
|
83
|
+
.action(async (jobId) => {
|
|
84
|
+
const agents = createAgentsClient();
|
|
85
|
+
const data = await agents.callMcpTool('md', 'get_md_results', { job_id: jobId });
|
|
86
|
+
if (typeof data.result === 'string') {
|
|
87
|
+
output(data, data.result);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
output(data, JSON.stringify(data, null, 2));
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
function extractJobId(data) {
|
|
95
|
+
if (typeof data.job_id === 'string')
|
|
96
|
+
return data.job_id;
|
|
97
|
+
if (typeof data.id === 'string')
|
|
98
|
+
return data.id;
|
|
99
|
+
if (typeof data.result === 'object' && data.result !== null) {
|
|
100
|
+
const r = data.result;
|
|
101
|
+
if (typeof r.job_id === 'string')
|
|
102
|
+
return r.job_id;
|
|
103
|
+
if (typeof r.id === 'string')
|
|
104
|
+
return r.id;
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
@@ -20,6 +20,53 @@ export function registerProjectCommands(program) {
|
|
|
20
20
|
]);
|
|
21
21
|
output(data, formatTable(['ID', 'Name', ''], rows));
|
|
22
22
|
});
|
|
23
|
+
program
|
|
24
|
+
.command('project')
|
|
25
|
+
.argument('<id>', 'Project ID (full or short 8-char)')
|
|
26
|
+
.description('Show details for a specific project')
|
|
27
|
+
.action(async (id) => {
|
|
28
|
+
const client = createClient();
|
|
29
|
+
// Resolve short ID via list
|
|
30
|
+
let fullId = id;
|
|
31
|
+
if (!id.includes('-')) {
|
|
32
|
+
const projects = await client.listProjects();
|
|
33
|
+
const match = projects.find(p => p.id.startsWith(id));
|
|
34
|
+
if (!match) {
|
|
35
|
+
process.stderr.write(`Error: No project found matching "${id}"\n`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
fullId = match.id;
|
|
39
|
+
}
|
|
40
|
+
const project = await client.getProject(fullId);
|
|
41
|
+
const consoleUrl = getConsoleUrl();
|
|
42
|
+
const url = `${consoleUrl}/?project=${project.id.substring(0, 8)}`;
|
|
43
|
+
const lines = [
|
|
44
|
+
`Project: ${project.name}`,
|
|
45
|
+
`ID: ${project.id}`,
|
|
46
|
+
];
|
|
47
|
+
if (project.description)
|
|
48
|
+
lines.push(`Summary: ${project.description}`);
|
|
49
|
+
if (project.owner_name || project.owner_email)
|
|
50
|
+
lines.push(`Owner: ${project.owner_name || ''} <${project.owner_email || ''}>`);
|
|
51
|
+
if (project.user_role)
|
|
52
|
+
lines.push(`Your role: ${project.user_role}${project.is_shared ? ' (shared)' : ''}`);
|
|
53
|
+
if (project.session_count !== undefined)
|
|
54
|
+
lines.push(`Sessions: ${project.session_count}`);
|
|
55
|
+
if (project.member_count !== undefined)
|
|
56
|
+
lines.push(`Members: ${project.member_count}`);
|
|
57
|
+
if (project.created_at)
|
|
58
|
+
lines.push(`Created: ${project.created_at}`);
|
|
59
|
+
if (project.last_activity_at)
|
|
60
|
+
lines.push(`Last used: ${project.last_activity_at}`);
|
|
61
|
+
lines.push(`View: ${url}`);
|
|
62
|
+
if (project.context) {
|
|
63
|
+
lines.push('');
|
|
64
|
+
lines.push('Project context (knowledge base):');
|
|
65
|
+
const ctx = project.context.length > 800 ? project.context.substring(0, 800) + '\n…(truncated)' : project.context;
|
|
66
|
+
lines.push(ctx);
|
|
67
|
+
}
|
|
68
|
+
output(project, lines.join('\n'));
|
|
69
|
+
});
|
|
23
70
|
program
|
|
24
71
|
.command('use')
|
|
25
72
|
.argument('<id>', 'Project ID (full or short 8-char)')
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { createClient, getConsoleUrl } from '../cli.js';
|
|
2
|
+
import { requireProject } from '../config.js';
|
|
3
|
+
import { output, formatTable } from '../output.js';
|
|
4
|
+
async function resolveSessionId(client, projectId, input) {
|
|
5
|
+
if (input.includes('-'))
|
|
6
|
+
return input;
|
|
7
|
+
const sessions = await client.listSessions(projectId, 200);
|
|
8
|
+
const match = sessions.find(s => s.id.startsWith(input));
|
|
9
|
+
if (!match) {
|
|
10
|
+
process.stderr.write(`Error: No session found matching "${input}" in the 200 most recent sessions.\nTry using the full UUID instead.\n`);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
return match.id;
|
|
14
|
+
}
|
|
15
|
+
export function registerSessionCommands(program) {
|
|
16
|
+
program
|
|
17
|
+
.command('sessions')
|
|
18
|
+
.description('List past chat sessions in the active project')
|
|
19
|
+
.option('--limit <n>', 'Max sessions to return', '20')
|
|
20
|
+
.option('--search <query>', 'Case-insensitive substring filter on title')
|
|
21
|
+
.option('--with-notes', 'Only sessions that have context notes')
|
|
22
|
+
.action(async (opts) => {
|
|
23
|
+
const project = requireProject();
|
|
24
|
+
const client = createClient();
|
|
25
|
+
const limit = parseInt(opts.limit);
|
|
26
|
+
let sessions = await client.listSessions(project.id, limit);
|
|
27
|
+
if (opts.search) {
|
|
28
|
+
const needle = String(opts.search).toLowerCase();
|
|
29
|
+
sessions = sessions.filter(s => (s.title || '').toLowerCase().includes(needle));
|
|
30
|
+
}
|
|
31
|
+
if (opts.withNotes) {
|
|
32
|
+
sessions = sessions.filter(s => s.has_notes);
|
|
33
|
+
}
|
|
34
|
+
const data = sessions.map(s => ({
|
|
35
|
+
id: s.id,
|
|
36
|
+
title: s.title,
|
|
37
|
+
message_count: s.message_count,
|
|
38
|
+
has_notes: s.has_notes,
|
|
39
|
+
updated_at: s.updated_at,
|
|
40
|
+
}));
|
|
41
|
+
const rows = sessions.map(s => [
|
|
42
|
+
s.id.substring(0, 8),
|
|
43
|
+
(s.title || 'Untitled').substring(0, 50),
|
|
44
|
+
String(s.message_count ?? ''),
|
|
45
|
+
s.has_notes ? '📝' : '',
|
|
46
|
+
(s.updated_at || s.created_at).substring(0, 19).replace('T', ' '),
|
|
47
|
+
]);
|
|
48
|
+
output(data, formatTable(['ID', 'Title', 'Msgs', 'Notes', 'Updated'], rows));
|
|
49
|
+
});
|
|
50
|
+
program
|
|
51
|
+
.command('session')
|
|
52
|
+
.argument('<id>', 'Session ID (full or short 8-char)')
|
|
53
|
+
.option('--full', 'Include full message contents (long output)')
|
|
54
|
+
.description('Show details for a specific chat session')
|
|
55
|
+
.action(async (id, opts) => {
|
|
56
|
+
const project = requireProject();
|
|
57
|
+
const client = createClient();
|
|
58
|
+
const fullId = await resolveSessionId(client, project.id, id);
|
|
59
|
+
const session = await client.getSession(fullId);
|
|
60
|
+
const consoleUrl = getConsoleUrl();
|
|
61
|
+
const url = `${consoleUrl}/?project=${project.id.substring(0, 8)}&session=${session.id.substring(0, 8)}`;
|
|
62
|
+
const lines = [
|
|
63
|
+
`Session: ${session.title || 'Untitled'}`,
|
|
64
|
+
`ID: ${session.id}`,
|
|
65
|
+
`Project: ${session.project_name || project.name}`,
|
|
66
|
+
];
|
|
67
|
+
if (session.user_name)
|
|
68
|
+
lines.push(`User: ${session.user_name}`);
|
|
69
|
+
if (session.model)
|
|
70
|
+
lines.push(`Model: ${session.model}`);
|
|
71
|
+
if (session.created_at)
|
|
72
|
+
lines.push(`Created: ${session.created_at}`);
|
|
73
|
+
if (session.updated_at)
|
|
74
|
+
lines.push(`Updated: ${session.updated_at}`);
|
|
75
|
+
const msgCount = session.messages?.length ?? session.message_count ?? 0;
|
|
76
|
+
lines.push(`Messages: ${msgCount}`);
|
|
77
|
+
if (session.context_notes) {
|
|
78
|
+
lines.push('');
|
|
79
|
+
lines.push('Context notes:');
|
|
80
|
+
const notes = session.context_notes.length > 600
|
|
81
|
+
? session.context_notes.substring(0, 600) + '\n…(truncated)'
|
|
82
|
+
: session.context_notes;
|
|
83
|
+
lines.push(notes);
|
|
84
|
+
}
|
|
85
|
+
lines.push(`View: ${url}`);
|
|
86
|
+
if (opts.full && session.messages?.length) {
|
|
87
|
+
lines.push('');
|
|
88
|
+
lines.push('— Conversation —');
|
|
89
|
+
for (const m of session.messages) {
|
|
90
|
+
const ts = (m.created_at || '').substring(11, 19);
|
|
91
|
+
lines.push('');
|
|
92
|
+
lines.push(`[${m.order}] ${m.role.toUpperCase()} ${ts}`);
|
|
93
|
+
lines.push(m.content);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
output(session, lines.join('\n'));
|
|
97
|
+
});
|
|
98
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { output, formatTable } from '../output.js';
|
|
2
|
+
import { createAgentsClient } from '../cli.js';
|
|
3
|
+
import { requireApiKey } from '../config.js';
|
|
4
|
+
export function registerZincCommands(program) {
|
|
5
|
+
const zinc = program
|
|
6
|
+
.command('zinc')
|
|
7
|
+
.description('Query ZINC purchasable compounds (self-hosted, 14M drug-like)')
|
|
8
|
+
.hook('preAction', () => { requireApiKey(); });
|
|
9
|
+
// ── m3t zinc search ───────────────���───────────────────────────
|
|
10
|
+
zinc
|
|
11
|
+
.command('search')
|
|
12
|
+
.option('--mw-min <n>', 'Min molecular weight')
|
|
13
|
+
.option('--mw-max <n>', 'Max molecular weight')
|
|
14
|
+
.option('--logp-min <n>', 'Min LogP')
|
|
15
|
+
.option('--logp-max <n>', 'Max LogP')
|
|
16
|
+
.option('--subset <s>', 'Subset: drug-like, lead-like, fragment-like, fda, in-stock')
|
|
17
|
+
.option('--limit <n>', 'Max results', '20')
|
|
18
|
+
.description('Search purchasable compounds by properties')
|
|
19
|
+
.action(async (opts) => {
|
|
20
|
+
const agents = createAgentsClient();
|
|
21
|
+
const params = {
|
|
22
|
+
limit: parseInt(opts.limit),
|
|
23
|
+
};
|
|
24
|
+
if (opts.mwMin)
|
|
25
|
+
params.mw_min = parseFloat(opts.mwMin);
|
|
26
|
+
if (opts.mwMax)
|
|
27
|
+
params.mw_max = parseFloat(opts.mwMax);
|
|
28
|
+
if (opts.logpMin)
|
|
29
|
+
params.logp_min = parseFloat(opts.logpMin);
|
|
30
|
+
if (opts.logpMax)
|
|
31
|
+
params.logp_max = parseFloat(opts.logpMax);
|
|
32
|
+
if (opts.subset)
|
|
33
|
+
params.subset = opts.subset;
|
|
34
|
+
const data = await agents.callMcpTool('zinc', 'search_purchasable', params);
|
|
35
|
+
if (typeof data.result === 'string') {
|
|
36
|
+
output(data, data.result);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const compounds = extractArray(data, ['compounds', 'results', 'data']);
|
|
40
|
+
if (compounds.length === 0) {
|
|
41
|
+
output([], 'No compounds found.');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const rows = compounds.map(c => [
|
|
45
|
+
str(c.zinc_id),
|
|
46
|
+
str(c.smiles || c.canonical_smiles, 45),
|
|
47
|
+
str(c.mw || c.molecular_weight),
|
|
48
|
+
str(c.logp),
|
|
49
|
+
str(c.subset || c.sub_set),
|
|
50
|
+
]);
|
|
51
|
+
output(compounds, formatTable(['ZINC ID', 'SMILES', 'MW', 'LogP', 'Subset'], rows));
|
|
52
|
+
});
|
|
53
|
+
// ── m3t zinc info <zinc_id> ───────────────────────────────────
|
|
54
|
+
zinc
|
|
55
|
+
.command('info')
|
|
56
|
+
.argument('<id>', 'ZINC compound ID (e.g., ZINC000000000001)')
|
|
57
|
+
.description('Get details for a ZINC compound')
|
|
58
|
+
.action(async (id) => {
|
|
59
|
+
const agents = createAgentsClient();
|
|
60
|
+
const data = await agents.callMcpTool('zinc', 'get_compound', { zinc_id: id });
|
|
61
|
+
if (typeof data.result === 'string') {
|
|
62
|
+
output(data, data.result);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
output(data, JSON.stringify(data, null, 2));
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
// ── m3t zinc random ───────��──────────────────────────────���────
|
|
69
|
+
zinc
|
|
70
|
+
.command('random')
|
|
71
|
+
.option('--count <n>', 'Number of compounds', '20')
|
|
72
|
+
.option('--subset <s>', 'Subset: drug-like, lead-like, fragment-like')
|
|
73
|
+
.description('Get random purchasable compounds')
|
|
74
|
+
.action(async (opts) => {
|
|
75
|
+
const agents = createAgentsClient();
|
|
76
|
+
const params = {
|
|
77
|
+
count: parseInt(opts.count),
|
|
78
|
+
};
|
|
79
|
+
if (opts.subset)
|
|
80
|
+
params.subset = opts.subset;
|
|
81
|
+
const data = await agents.callMcpTool('zinc', 'get_random_compounds', params);
|
|
82
|
+
if (typeof data.result === 'string') {
|
|
83
|
+
output(data, data.result);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const compounds = extractArray(data, ['compounds', 'results', 'data']);
|
|
87
|
+
if (compounds.length === 0) {
|
|
88
|
+
output([], 'No compounds found.');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const rows = compounds.map(c => [
|
|
92
|
+
str(c.zinc_id),
|
|
93
|
+
str(c.smiles || c.canonical_smiles, 45),
|
|
94
|
+
str(c.mw || c.molecular_weight),
|
|
95
|
+
str(c.logp),
|
|
96
|
+
]);
|
|
97
|
+
output(compounds, formatTable(['ZINC ID', 'SMILES', 'MW', 'LogP'], rows));
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
// ── Helpers ────���──────────────────────��───────────────────────────
|
|
101
|
+
function str(val, maxLen) {
|
|
102
|
+
const s = val != null ? String(val) : '-';
|
|
103
|
+
return maxLen ? s.substring(0, maxLen) : s;
|
|
104
|
+
}
|
|
105
|
+
function extractArray(data, keys) {
|
|
106
|
+
if (Array.isArray(data))
|
|
107
|
+
return data;
|
|
108
|
+
for (const key of keys) {
|
|
109
|
+
if (Array.isArray(data[key]))
|
|
110
|
+
return data[key];
|
|
111
|
+
}
|
|
112
|
+
if (typeof data.result === 'string') {
|
|
113
|
+
try {
|
|
114
|
+
return extractArray(JSON.parse(data.result), keys);
|
|
115
|
+
}
|
|
116
|
+
catch { /* not JSON */ }
|
|
117
|
+
}
|
|
118
|
+
if (typeof data.result === 'object' && data.result !== null) {
|
|
119
|
+
return extractArray(data.result, keys);
|
|
120
|
+
}
|
|
121
|
+
return [];
|
|
122
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -3,6 +3,40 @@ export interface Project {
|
|
|
3
3
|
name: string;
|
|
4
4
|
description: string;
|
|
5
5
|
created_at: string;
|
|
6
|
+
updated_at?: string;
|
|
7
|
+
last_activity_at?: string;
|
|
8
|
+
context?: string;
|
|
9
|
+
session_count?: number;
|
|
10
|
+
user_role?: string;
|
|
11
|
+
owner_email?: string;
|
|
12
|
+
owner_name?: string;
|
|
13
|
+
member_count?: number;
|
|
14
|
+
is_shared?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface ChatSession {
|
|
17
|
+
id: string;
|
|
18
|
+
title: string;
|
|
19
|
+
created_at: string;
|
|
20
|
+
updated_at: string;
|
|
21
|
+
message_count?: number;
|
|
22
|
+
has_notes?: boolean;
|
|
23
|
+
context_notes?: string;
|
|
24
|
+
project?: string;
|
|
25
|
+
project_name?: string;
|
|
26
|
+
user_name?: string;
|
|
27
|
+
selected_tab?: number;
|
|
28
|
+
model?: string;
|
|
29
|
+
messages?: ChatMessage[];
|
|
30
|
+
}
|
|
31
|
+
export interface ChatMessage {
|
|
32
|
+
id: number;
|
|
33
|
+
role: 'user' | 'assistant';
|
|
34
|
+
content: string;
|
|
35
|
+
order: number;
|
|
36
|
+
created_at: string;
|
|
37
|
+
elapsed_time?: number | null;
|
|
38
|
+
total_tokens?: number | null;
|
|
39
|
+
thinking_steps?: unknown[];
|
|
6
40
|
}
|
|
7
41
|
export interface Job {
|
|
8
42
|
id: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "m3triq",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "M3TRIQ — protein-ligand analysis from the terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,8 +10,11 @@
|
|
|
10
10
|
"keywords": ["bioinformatics", "chembl", "docking", "protein", "ligand", "drug-discovery", "cli"],
|
|
11
11
|
"repository": {
|
|
12
12
|
"type": "git",
|
|
13
|
-
"url": "https://github.com/
|
|
14
|
-
|
|
13
|
+
"url": "https://github.com/M3TRIQ/m3t.git"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/M3TRIQ/m3t#readme",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/M3TRIQ/m3t/issues"
|
|
15
18
|
},
|
|
16
19
|
"scripts": {
|
|
17
20
|
"build": "tsc",
|