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 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.parse();
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,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerAdmetCommands(program: Command): void;
@@ -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,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerFoodbCommands(program: Command): void;
@@ -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,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerMdCommands(program: Command): void;
@@ -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,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerSessionCommands(program: Command): void;
@@ -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,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerZincCommands(program: Command): void;
@@ -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.1.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/khai-m3triq/m3t_mono.git",
14
- "directory": "m3triq_cli"
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",