m3triq 0.1.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/dist/cli.d.ts +7 -0
- package/dist/cli.js +50 -0
- package/dist/client.d.ts +51 -0
- package/dist/client.js +158 -0
- package/dist/commands/chembl.d.ts +2 -0
- package/dist/commands/chembl.js +305 -0
- package/dist/commands/config-cmd.d.ts +2 -0
- package/dist/commands/config-cmd.js +52 -0
- package/dist/commands/data.d.ts +2 -0
- package/dist/commands/data.js +96 -0
- package/dist/commands/design.d.ts +2 -0
- package/dist/commands/design.js +219 -0
- package/dist/commands/docking.d.ts +2 -0
- package/dist/commands/docking.js +157 -0
- package/dist/commands/jobs.d.ts +2 -0
- package/dist/commands/jobs.js +89 -0
- package/dist/commands/predict.d.ts +2 -0
- package/dist/commands/predict.js +39 -0
- package/dist/commands/projects.d.ts +2 -0
- package/dist/commands/projects.js +53 -0
- package/dist/commands/sandbox.d.ts +2 -0
- package/dist/commands/sandbox.js +37 -0
- package/dist/config.d.ts +34 -0
- package/dist/config.js +101 -0
- package/dist/output.d.ts +8 -0
- package/dist/output.js +27 -0
- package/dist/types.d.ts +71 -0
- package/dist/types.js +1 -0
- package/dist/url.d.ts +5 -0
- package/dist/url.js +21 -0
- package/package.json +30 -0
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { M3triqClient, AgentsClient } from './client.js';
|
|
3
|
+
/** Create an API client from current config. Call lazily in command handlers. */
|
|
4
|
+
export declare function createClient(): M3triqClient;
|
|
5
|
+
export declare function getConsoleUrl(): string;
|
|
6
|
+
/** Create an agents client for MCP tool calls. */
|
|
7
|
+
export declare function createAgentsClient(): AgentsClient;
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { setJsonMode } from './output.js';
|
|
4
|
+
import { getEffectiveConfig, requireApiKey } from './config.js';
|
|
5
|
+
import { M3triqClient, AgentsClient } from './client.js';
|
|
6
|
+
import { registerConfigCommands } from './commands/config-cmd.js';
|
|
7
|
+
import { registerProjectCommands } from './commands/projects.js';
|
|
8
|
+
import { registerJobCommands } from './commands/jobs.js';
|
|
9
|
+
import { registerDockingCommands } from './commands/docking.js';
|
|
10
|
+
import { registerPredictCommands } from './commands/predict.js';
|
|
11
|
+
import { registerChemblCommands } from './commands/chembl.js';
|
|
12
|
+
import { registerSandboxCommands } from './commands/sandbox.js';
|
|
13
|
+
import { registerDesignCommands } from './commands/design.js';
|
|
14
|
+
import { registerDataCommands } from './commands/data.js';
|
|
15
|
+
const program = new Command();
|
|
16
|
+
program
|
|
17
|
+
.name('m3t')
|
|
18
|
+
.description('M3TRIQ — protein-ligand analysis from the terminal')
|
|
19
|
+
.version('0.1.0')
|
|
20
|
+
.option('--json', 'Output as JSON (machine-readable)')
|
|
21
|
+
.hook('preAction', (thisCommand) => {
|
|
22
|
+
const opts = thisCommand.optsWithGlobals();
|
|
23
|
+
if (opts.json)
|
|
24
|
+
setJsonMode(true);
|
|
25
|
+
});
|
|
26
|
+
registerConfigCommands(program);
|
|
27
|
+
registerProjectCommands(program);
|
|
28
|
+
registerJobCommands(program);
|
|
29
|
+
registerDockingCommands(program);
|
|
30
|
+
registerPredictCommands(program);
|
|
31
|
+
registerChemblCommands(program);
|
|
32
|
+
registerSandboxCommands(program);
|
|
33
|
+
registerDesignCommands(program);
|
|
34
|
+
registerDataCommands(program);
|
|
35
|
+
program.parse();
|
|
36
|
+
/** Create an API client from current config. Call lazily in command handlers. */
|
|
37
|
+
export function createClient() {
|
|
38
|
+
const config = getEffectiveConfig();
|
|
39
|
+
const apiKey = requireApiKey();
|
|
40
|
+
return new M3triqClient(apiKey, config.api_url);
|
|
41
|
+
}
|
|
42
|
+
export function getConsoleUrl() {
|
|
43
|
+
const config = getEffectiveConfig();
|
|
44
|
+
return config.console_url;
|
|
45
|
+
}
|
|
46
|
+
/** Create an agents client for MCP tool calls. */
|
|
47
|
+
export function createAgentsClient() {
|
|
48
|
+
const config = getEffectiveConfig();
|
|
49
|
+
return new AgentsClient(config.agents_url);
|
|
50
|
+
}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Job, Project, DockingParams, BatchDockingParams, DiffDockParams, StructurePredictionParams, SandboxParams, McpCallResult } from './types.js';
|
|
2
|
+
export declare class M3triqClient {
|
|
3
|
+
private baseUrl;
|
|
4
|
+
private apiKey;
|
|
5
|
+
constructor(apiKey: string, baseUrl: string);
|
|
6
|
+
private get headers();
|
|
7
|
+
private request;
|
|
8
|
+
listProjects(): Promise<Project[]>;
|
|
9
|
+
getJob(jobId: string): Promise<Job>;
|
|
10
|
+
listJobs(projectId: string, limit?: number): Promise<Job[]>;
|
|
11
|
+
createDockingJob(params: DockingParams): Promise<{
|
|
12
|
+
job_id: string;
|
|
13
|
+
}>;
|
|
14
|
+
createBatchDockingJob(params: BatchDockingParams): Promise<{
|
|
15
|
+
job_id: string;
|
|
16
|
+
}>;
|
|
17
|
+
listProjectData(projectId: string): Promise<Array<Record<string, unknown>>>;
|
|
18
|
+
getProjectData(projectId: string, dataId: string): Promise<Record<string, unknown>>;
|
|
19
|
+
createDiffDockJob(params: DiffDockParams): Promise<{
|
|
20
|
+
job_id: string;
|
|
21
|
+
}>;
|
|
22
|
+
createRFAntibodyJob(params: {
|
|
23
|
+
project_id: string;
|
|
24
|
+
title: string;
|
|
25
|
+
job_type: string;
|
|
26
|
+
antibody_type: string;
|
|
27
|
+
}): Promise<{
|
|
28
|
+
job_id: string;
|
|
29
|
+
}>;
|
|
30
|
+
callbackJob(jobId: string, data: {
|
|
31
|
+
status: string;
|
|
32
|
+
percentage?: number;
|
|
33
|
+
message?: string;
|
|
34
|
+
result?: unknown;
|
|
35
|
+
}): Promise<void>;
|
|
36
|
+
createStructurePrediction(params: StructurePredictionParams): Promise<{
|
|
37
|
+
job_id: string;
|
|
38
|
+
}>;
|
|
39
|
+
createSandboxJob(params: SandboxParams): Promise<{
|
|
40
|
+
job_id: string;
|
|
41
|
+
}>;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Client for the agents service (MCP tool calls).
|
|
45
|
+
* Separate from M3triqClient because it talks to a different host.
|
|
46
|
+
*/
|
|
47
|
+
export declare class AgentsClient {
|
|
48
|
+
private baseUrl;
|
|
49
|
+
constructor(baseUrl: string);
|
|
50
|
+
callMcpTool(server: string, tool: string, params?: Record<string, unknown>): Promise<McpCallResult>;
|
|
51
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
export class M3triqClient {
|
|
2
|
+
baseUrl;
|
|
3
|
+
apiKey;
|
|
4
|
+
constructor(apiKey, baseUrl) {
|
|
5
|
+
this.apiKey = apiKey;
|
|
6
|
+
this.baseUrl = baseUrl.replace(/\/$/, '');
|
|
7
|
+
}
|
|
8
|
+
get headers() {
|
|
9
|
+
return {
|
|
10
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
11
|
+
'Content-Type': 'application/json',
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
async request(method, path, body) {
|
|
15
|
+
const url = `${this.baseUrl}${path}`;
|
|
16
|
+
const res = await fetch(url, {
|
|
17
|
+
method,
|
|
18
|
+
headers: this.headers,
|
|
19
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
20
|
+
});
|
|
21
|
+
if (!res.ok) {
|
|
22
|
+
const text = await res.text();
|
|
23
|
+
throw new Error(`API error ${res.status}: ${text.substring(0, 200)}`);
|
|
24
|
+
}
|
|
25
|
+
return res.json();
|
|
26
|
+
}
|
|
27
|
+
async listProjects() {
|
|
28
|
+
const data = await this.request('GET', '/api/membership/projects/');
|
|
29
|
+
return Array.isArray(data) ? data : (data.results || []);
|
|
30
|
+
}
|
|
31
|
+
async getJob(jobId) {
|
|
32
|
+
return this.request('GET', `/api/jobs/${jobId}/`);
|
|
33
|
+
}
|
|
34
|
+
async listJobs(projectId, limit = 20) {
|
|
35
|
+
const data = await this.request('GET', `/api/jobs/?project_id=${projectId}&page_size=${limit}`);
|
|
36
|
+
return Array.isArray(data) ? data : (data.results || []);
|
|
37
|
+
}
|
|
38
|
+
async createDockingJob(params) {
|
|
39
|
+
return this.request('POST', '/api/jobs/create_molecular_docking/', params);
|
|
40
|
+
}
|
|
41
|
+
async createBatchDockingJob(params) {
|
|
42
|
+
return this.request('POST', '/api/jobs/create_batch_docking/', params);
|
|
43
|
+
}
|
|
44
|
+
// ── Project Data ──────────────────────────────────────────────
|
|
45
|
+
async listProjectData(projectId) {
|
|
46
|
+
const data = await this.request('GET', `/api/membership/projects/${projectId}/data/`);
|
|
47
|
+
return Array.isArray(data) ? data : (data.results || []);
|
|
48
|
+
}
|
|
49
|
+
async getProjectData(projectId, dataId) {
|
|
50
|
+
return this.request('GET', `/api/membership/projects/${projectId}/data/${dataId}/`);
|
|
51
|
+
}
|
|
52
|
+
// ── Docking (continued) ──────────────────────────────────────
|
|
53
|
+
async createDiffDockJob(params) {
|
|
54
|
+
return this.request('POST', '/api/jobs/create_diffdock_prediction/', params);
|
|
55
|
+
}
|
|
56
|
+
async createRFAntibodyJob(params) {
|
|
57
|
+
// Uses internal endpoint — needs X-Internal-Service header
|
|
58
|
+
const url = `${this.baseUrl}/api/jobs/create_prediction_internal/`;
|
|
59
|
+
const res = await fetch(url, {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: {
|
|
62
|
+
...this.headers,
|
|
63
|
+
'X-Internal-Service': 'true',
|
|
64
|
+
},
|
|
65
|
+
body: JSON.stringify({
|
|
66
|
+
project_id: params.project_id,
|
|
67
|
+
job_type: params.job_type,
|
|
68
|
+
title: params.title,
|
|
69
|
+
result_data: { antibody_type: params.antibody_type },
|
|
70
|
+
}),
|
|
71
|
+
});
|
|
72
|
+
if (!res.ok) {
|
|
73
|
+
const text = await res.text();
|
|
74
|
+
throw new Error(`API error ${res.status}: ${text.substring(0, 200)}`);
|
|
75
|
+
}
|
|
76
|
+
return res.json();
|
|
77
|
+
}
|
|
78
|
+
async callbackJob(jobId, data) {
|
|
79
|
+
const url = `${this.baseUrl}/api/jobs/${jobId}/cloud-callback/`;
|
|
80
|
+
const payload = { ...data, message: data.message?.substring(0, 190) };
|
|
81
|
+
const res = await fetch(url, {
|
|
82
|
+
method: 'POST',
|
|
83
|
+
headers: { 'Content-Type': 'application/json' },
|
|
84
|
+
body: JSON.stringify(payload),
|
|
85
|
+
});
|
|
86
|
+
if (!res.ok) {
|
|
87
|
+
const text = await res.text();
|
|
88
|
+
throw new Error(`Callback error ${res.status}: ${text.substring(0, 200)}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async createStructurePrediction(params) {
|
|
92
|
+
const endpoint = params.method === 'alphafold2'
|
|
93
|
+
? '/api/jobs/create_alphafold2_prediction/'
|
|
94
|
+
: '/api/jobs/create_esmfold_prediction/';
|
|
95
|
+
return this.request('POST', endpoint, params);
|
|
96
|
+
}
|
|
97
|
+
async createSandboxJob(params) {
|
|
98
|
+
return this.request('POST', '/api/jobs/create_sandbox_job/', params);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Client for the agents service (MCP tool calls).
|
|
103
|
+
* Separate from M3triqClient because it talks to a different host.
|
|
104
|
+
*/
|
|
105
|
+
export class AgentsClient {
|
|
106
|
+
baseUrl;
|
|
107
|
+
constructor(baseUrl) {
|
|
108
|
+
this.baseUrl = baseUrl.replace(/\/$/, '');
|
|
109
|
+
}
|
|
110
|
+
async callMcpTool(server, tool, params = {}) {
|
|
111
|
+
const url = `${this.baseUrl}/mcp/call`;
|
|
112
|
+
const res = await fetch(url, {
|
|
113
|
+
method: 'POST',
|
|
114
|
+
headers: { 'Content-Type': 'application/json' },
|
|
115
|
+
body: JSON.stringify({ server, tool, params }),
|
|
116
|
+
});
|
|
117
|
+
if (!res.ok) {
|
|
118
|
+
const text = await res.text();
|
|
119
|
+
throw new Error(`MCP call failed (${res.status}): ${text.substring(0, 200)}`);
|
|
120
|
+
}
|
|
121
|
+
const data = await res.json();
|
|
122
|
+
if (data.error) {
|
|
123
|
+
throw new Error(`MCP error: ${data.error}`);
|
|
124
|
+
}
|
|
125
|
+
// Unwrap nested MCP response: { result: { result: { content: [{ text: "JSON" }] } } }
|
|
126
|
+
return unwrapMcpResponse(data);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Unwrap the nested MCP response format.
|
|
131
|
+
* The agents /mcp/call endpoint returns: { result: { result: { content: [{ text: "JSON" }] } } }
|
|
132
|
+
* We want to extract the parsed JSON from the innermost text content.
|
|
133
|
+
*/
|
|
134
|
+
function unwrapMcpResponse(data) {
|
|
135
|
+
const content = findContent(data);
|
|
136
|
+
if (content) {
|
|
137
|
+
const textItem = content.find((c) => c.type === 'text' && typeof c.text === 'string');
|
|
138
|
+
if (textItem) {
|
|
139
|
+
try {
|
|
140
|
+
return JSON.parse(textItem.text);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return { result: textItem.text };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return data;
|
|
148
|
+
}
|
|
149
|
+
function findContent(obj) {
|
|
150
|
+
if (!obj || typeof obj !== 'object')
|
|
151
|
+
return null;
|
|
152
|
+
const o = obj;
|
|
153
|
+
if (Array.isArray(o.content))
|
|
154
|
+
return o.content;
|
|
155
|
+
if (o.result && typeof o.result === 'object')
|
|
156
|
+
return findContent(o.result);
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { output, formatTable } from '../output.js';
|
|
2
|
+
import { createAgentsClient } from '../cli.js';
|
|
3
|
+
import { requireApiKey, requireProject } from '../config.js';
|
|
4
|
+
export function registerChemblCommands(program) {
|
|
5
|
+
const chembl = program
|
|
6
|
+
.command('chembl')
|
|
7
|
+
.description('Query the M3TRIQ ChEMBL database (self-hosted, 2.4M compounds)')
|
|
8
|
+
.hook('preAction', () => { requireApiKey(); });
|
|
9
|
+
// ── m3t chembl search <query> ─────────────────────────────────
|
|
10
|
+
chembl
|
|
11
|
+
.command('search')
|
|
12
|
+
.argument('<query>', 'Compound name, SMILES, InChI, or ChEMBL ID')
|
|
13
|
+
.option('--type <type>', 'Search type: name, smiles, inchi, chembl_id, similarity', 'name')
|
|
14
|
+
.option('--limit <n>', 'Max results', '10')
|
|
15
|
+
.option('--save <name>', 'Save results as project dataset')
|
|
16
|
+
.description('Search ChEMBL compounds')
|
|
17
|
+
.action(async (query, opts) => {
|
|
18
|
+
const agents = createAgentsClient();
|
|
19
|
+
const params = {
|
|
20
|
+
query,
|
|
21
|
+
search_type: opts.type,
|
|
22
|
+
limit: parseInt(opts.limit),
|
|
23
|
+
return_format: 'json',
|
|
24
|
+
export_to_project: false,
|
|
25
|
+
};
|
|
26
|
+
if (opts.save) {
|
|
27
|
+
const project = requireProject();
|
|
28
|
+
params.export_to_project = true;
|
|
29
|
+
params.project_id = project.id;
|
|
30
|
+
params.dataset_name = opts.save;
|
|
31
|
+
}
|
|
32
|
+
const data = await agents.callMcpTool('chembl', 'search_compounds', params);
|
|
33
|
+
const compounds = extractCompounds(data);
|
|
34
|
+
if (compounds.length === 0) {
|
|
35
|
+
output([], 'No compounds found.');
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const rows = compounds.map(c => [
|
|
39
|
+
str(c.chembl_id),
|
|
40
|
+
str(c.name || c.pref_name, 30),
|
|
41
|
+
str(c.molecular_weight || c.mw),
|
|
42
|
+
str(c.alogp),
|
|
43
|
+
str(c.canonical_smiles, 50),
|
|
44
|
+
]);
|
|
45
|
+
output(compounds, formatTable(['ID', 'Name', 'MW', 'LogP', 'SMILES'], rows));
|
|
46
|
+
if (opts.save)
|
|
47
|
+
process.stderr.write(`Saved as dataset: ${opts.save}\n`);
|
|
48
|
+
});
|
|
49
|
+
// ── m3t chembl info <chembl_id> ───────────────────────────────
|
|
50
|
+
chembl
|
|
51
|
+
.command('info')
|
|
52
|
+
.argument('<id>', 'ChEMBL compound ID (e.g., CHEMBL25)')
|
|
53
|
+
.description('Get detailed compound information')
|
|
54
|
+
.action(async (id) => {
|
|
55
|
+
const agents = createAgentsClient();
|
|
56
|
+
const data = await agents.callMcpTool('chembl', 'get_compound_info', {
|
|
57
|
+
chembl_id: normalizeChemblId(id),
|
|
58
|
+
return_format: 'json',
|
|
59
|
+
});
|
|
60
|
+
output(data, formatCompoundInfo(data));
|
|
61
|
+
});
|
|
62
|
+
// ── m3t chembl targets <query> ────────────────────────────────
|
|
63
|
+
chembl
|
|
64
|
+
.command('targets')
|
|
65
|
+
.argument('<query>', 'Target name or gene symbol (e.g., EGFR, PDE3B)')
|
|
66
|
+
.option('--type <type>', 'Target type: PROTEIN, ORGANISM, TISSUE, CELL-LINE')
|
|
67
|
+
.option('--organism <org>', 'Filter by organism (e.g., "Homo sapiens")')
|
|
68
|
+
.option('--limit <n>', 'Max results', '10')
|
|
69
|
+
.description('Search protein targets')
|
|
70
|
+
.action(async (query, opts) => {
|
|
71
|
+
const agents = createAgentsClient();
|
|
72
|
+
const params = {
|
|
73
|
+
query,
|
|
74
|
+
limit: parseInt(opts.limit),
|
|
75
|
+
return_format: 'json',
|
|
76
|
+
};
|
|
77
|
+
if (opts.type)
|
|
78
|
+
params.target_type = opts.type;
|
|
79
|
+
if (opts.organism)
|
|
80
|
+
params.organism = opts.organism;
|
|
81
|
+
const data = await agents.callMcpTool('chembl', 'search_targets', params);
|
|
82
|
+
const targets = extractTargets(data);
|
|
83
|
+
if (targets.length === 0) {
|
|
84
|
+
output([], 'No targets found.');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const rows = targets.map(t => [
|
|
88
|
+
str(t.chembl_id || t.target_chembl_id),
|
|
89
|
+
str(t.name || t.pref_name, 40),
|
|
90
|
+
str(t.organism),
|
|
91
|
+
str(t.target_type),
|
|
92
|
+
]);
|
|
93
|
+
output(targets, formatTable(['ID', 'Name', 'Organism', 'Type'], rows));
|
|
94
|
+
});
|
|
95
|
+
// ── m3t chembl binders <target> ───────────────────────────────
|
|
96
|
+
chembl
|
|
97
|
+
.command('binders')
|
|
98
|
+
.argument('<target>', 'Target name or gene symbol (e.g., PDE3B, EGFR)')
|
|
99
|
+
.option('--activity <types>', 'Activity types, comma-separated (default: IC50,EC50,Ki,Kd)', 'IC50,EC50,Ki,Kd')
|
|
100
|
+
.option('--max-value <nM>', 'Max activity value in nM (default: 10000)', '10000')
|
|
101
|
+
.option('--limit <n>', 'Max results', '100')
|
|
102
|
+
.option('--save <name>', 'Save results as project dataset')
|
|
103
|
+
.description('Get all compounds binding to a target (with bioactivity data)')
|
|
104
|
+
.action(async (target, opts) => {
|
|
105
|
+
const agents = createAgentsClient();
|
|
106
|
+
const params = {
|
|
107
|
+
target_name: target,
|
|
108
|
+
activity_types: opts.activity.split(','),
|
|
109
|
+
max_value: parseFloat(opts.maxValue),
|
|
110
|
+
limit: parseInt(opts.limit),
|
|
111
|
+
return_format: 'json',
|
|
112
|
+
export_to_project: false,
|
|
113
|
+
};
|
|
114
|
+
if (opts.save) {
|
|
115
|
+
const project = requireProject();
|
|
116
|
+
params.export_to_project = true;
|
|
117
|
+
params.project_id = project.id;
|
|
118
|
+
params.dataset_name = opts.save;
|
|
119
|
+
}
|
|
120
|
+
process.stderr.write(`Querying binders for ${target}...\n`);
|
|
121
|
+
const data = await agents.callMcpTool('chembl', 'get_target_binders', params);
|
|
122
|
+
const compounds = extractCompounds(data);
|
|
123
|
+
if (compounds.length === 0) {
|
|
124
|
+
output([], 'No binders found.');
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const rows = compounds.map(c => [
|
|
128
|
+
str(c.chembl_id),
|
|
129
|
+
str(c.name || c.pref_name, 25),
|
|
130
|
+
str(c.activity_type),
|
|
131
|
+
str(c.activity_value),
|
|
132
|
+
str(c.canonical_smiles, 40),
|
|
133
|
+
]);
|
|
134
|
+
output(compounds, formatTable(['ID', 'Name', 'Type', 'Value (nM)', 'SMILES'], rows));
|
|
135
|
+
process.stderr.write(`${compounds.length} binders found.\n`);
|
|
136
|
+
if (opts.save)
|
|
137
|
+
process.stderr.write(`Saved as dataset: ${opts.save}\n`);
|
|
138
|
+
});
|
|
139
|
+
// ── m3t chembl activities <chembl_id> ─────────────────────────
|
|
140
|
+
chembl
|
|
141
|
+
.command('activities')
|
|
142
|
+
.argument('<id>', 'ChEMBL compound ID (e.g., CHEMBL25)')
|
|
143
|
+
.option('--type <type>', 'Activity type: IC50, EC50, Ki, Kd, ED50, LD50, MIC')
|
|
144
|
+
.option('--target <id>', 'Filter by target ChEMBL ID')
|
|
145
|
+
.option('--limit <n>', 'Max results', '50')
|
|
146
|
+
.description('Get bioactivity data for a compound')
|
|
147
|
+
.action(async (id, opts) => {
|
|
148
|
+
const agents = createAgentsClient();
|
|
149
|
+
const params = {
|
|
150
|
+
chembl_id: normalizeChemblId(id),
|
|
151
|
+
limit: parseInt(opts.limit),
|
|
152
|
+
};
|
|
153
|
+
if (opts.type)
|
|
154
|
+
params.activity_type = opts.type;
|
|
155
|
+
if (opts.target)
|
|
156
|
+
params.target_chembl_id = opts.target;
|
|
157
|
+
const data = await agents.callMcpTool('chembl', 'get_compound_activities', params);
|
|
158
|
+
// Activities endpoint returns text format — pass through
|
|
159
|
+
if (typeof data.result === 'string') {
|
|
160
|
+
output(data, data.result);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const activities = extractActivities(data);
|
|
164
|
+
if (activities.length === 0) {
|
|
165
|
+
output([], 'No activities found.');
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const rows = activities.map(a => [
|
|
169
|
+
str(a.activity_type || a.standard_type),
|
|
170
|
+
str(a.value || a.standard_value),
|
|
171
|
+
str(a.units || a.standard_units),
|
|
172
|
+
str(a.target_name, 35),
|
|
173
|
+
str(a.target_chembl_id),
|
|
174
|
+
]);
|
|
175
|
+
output(activities, formatTable(['Type', 'Value', 'Units', 'Target', 'Target ID'], rows));
|
|
176
|
+
});
|
|
177
|
+
// ── m3t chembl similar <smiles> ───────────────────────────────
|
|
178
|
+
chembl
|
|
179
|
+
.command('similar')
|
|
180
|
+
.argument('<smiles>', 'SMILES string of query compound')
|
|
181
|
+
.option('--threshold <t>', 'Min Tanimoto similarity (0-1)', '0.7')
|
|
182
|
+
.option('--limit <n>', 'Max results', '20')
|
|
183
|
+
.description('Find structurally similar compounds')
|
|
184
|
+
.action(async (smiles, opts) => {
|
|
185
|
+
const agents = createAgentsClient();
|
|
186
|
+
const data = await agents.callMcpTool('chembl', 'search_similar_compounds', {
|
|
187
|
+
smiles,
|
|
188
|
+
similarity_threshold: parseFloat(opts.threshold),
|
|
189
|
+
limit: parseInt(opts.limit),
|
|
190
|
+
});
|
|
191
|
+
const compounds = extractCompounds(data);
|
|
192
|
+
if (compounds.length === 0) {
|
|
193
|
+
output([], 'No similar compounds found.');
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const rows = compounds.map(c => [
|
|
197
|
+
str(c.chembl_id),
|
|
198
|
+
str(c.name || c.pref_name, 25),
|
|
199
|
+
str(c.similarity),
|
|
200
|
+
str(c.molecular_weight || c.mw),
|
|
201
|
+
str(c.canonical_smiles, 40),
|
|
202
|
+
]);
|
|
203
|
+
output(compounds, formatTable(['ID', 'Name', 'Similarity', 'MW', 'SMILES'], rows));
|
|
204
|
+
});
|
|
205
|
+
// ── m3t chembl sql <query> ────────────────────────────────────
|
|
206
|
+
chembl
|
|
207
|
+
.command('sql')
|
|
208
|
+
.argument('<query>', 'SQL SELECT query')
|
|
209
|
+
.option('--limit <n>', 'Max results', '100')
|
|
210
|
+
.description('Execute read-only SQL against ChEMBL database')
|
|
211
|
+
.action(async (query, opts) => {
|
|
212
|
+
const agents = createAgentsClient();
|
|
213
|
+
const data = await agents.callMcpTool('chembl', 'execute_custom_query', {
|
|
214
|
+
query,
|
|
215
|
+
limit: parseInt(opts.limit),
|
|
216
|
+
});
|
|
217
|
+
// Raw SQL results — output as-is
|
|
218
|
+
output(data, JSON.stringify(data, null, 2));
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
// ── Helpers ───────────────────────────────────────────────────────
|
|
222
|
+
function str(val, maxLen) {
|
|
223
|
+
const s = val != null ? String(val) : '-';
|
|
224
|
+
return maxLen ? s.substring(0, maxLen) : s;
|
|
225
|
+
}
|
|
226
|
+
/** Accept both "CHEMBL25" and "25" */
|
|
227
|
+
function normalizeChemblId(id) {
|
|
228
|
+
return id.startsWith('CHEMBL') ? id : `CHEMBL${id}`;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Extract compound array from MCP response.
|
|
232
|
+
* The response shape varies — could be { compounds: [...] }, { results: [...] },
|
|
233
|
+
* or the array directly. This handles all cases.
|
|
234
|
+
*/
|
|
235
|
+
function extractCompounds(data) {
|
|
236
|
+
if (Array.isArray(data))
|
|
237
|
+
return data;
|
|
238
|
+
if (Array.isArray(data.compounds))
|
|
239
|
+
return data.compounds;
|
|
240
|
+
if (Array.isArray(data.results))
|
|
241
|
+
return data.results;
|
|
242
|
+
if (Array.isArray(data.data))
|
|
243
|
+
return data.data;
|
|
244
|
+
// Might be text response — try parsing
|
|
245
|
+
if (typeof data.result === 'string') {
|
|
246
|
+
try {
|
|
247
|
+
const parsed = JSON.parse(data.result);
|
|
248
|
+
return extractCompounds(parsed);
|
|
249
|
+
}
|
|
250
|
+
catch { /* not JSON */ }
|
|
251
|
+
}
|
|
252
|
+
if (typeof data.result === 'object' && data.result !== null) {
|
|
253
|
+
return extractCompounds(data.result);
|
|
254
|
+
}
|
|
255
|
+
return [];
|
|
256
|
+
}
|
|
257
|
+
function extractTargets(data) {
|
|
258
|
+
if (Array.isArray(data))
|
|
259
|
+
return data;
|
|
260
|
+
if (Array.isArray(data.targets))
|
|
261
|
+
return data.targets;
|
|
262
|
+
if (Array.isArray(data.results))
|
|
263
|
+
return data.results;
|
|
264
|
+
if (Array.isArray(data.data))
|
|
265
|
+
return data.data;
|
|
266
|
+
if (typeof data.result === 'object' && data.result !== null) {
|
|
267
|
+
return extractTargets(data.result);
|
|
268
|
+
}
|
|
269
|
+
return [];
|
|
270
|
+
}
|
|
271
|
+
function extractActivities(data) {
|
|
272
|
+
if (Array.isArray(data))
|
|
273
|
+
return data;
|
|
274
|
+
if (Array.isArray(data.activities))
|
|
275
|
+
return data.activities;
|
|
276
|
+
if (Array.isArray(data.results))
|
|
277
|
+
return data.results;
|
|
278
|
+
if (Array.isArray(data.data))
|
|
279
|
+
return data.data;
|
|
280
|
+
if (typeof data.result === 'object' && data.result !== null) {
|
|
281
|
+
return extractActivities(data.result);
|
|
282
|
+
}
|
|
283
|
+
return [];
|
|
284
|
+
}
|
|
285
|
+
function formatCompoundInfo(data) {
|
|
286
|
+
const lines = [];
|
|
287
|
+
const d = (data.result ?? data);
|
|
288
|
+
const fields = [
|
|
289
|
+
['ChEMBL ID', str(d.chembl_id || d.molecule_chembl_id)],
|
|
290
|
+
['Name', str(d.name || d.pref_name)],
|
|
291
|
+
['SMILES', str(d.canonical_smiles || d.smiles)],
|
|
292
|
+
['MW', str(d.molecular_weight || d.mw || d.full_mwt)],
|
|
293
|
+
['LogP', str(d.alogp || d.logp)],
|
|
294
|
+
['TPSA', str(d.psa || d.tpsa)],
|
|
295
|
+
['HBA', str(d.hba)],
|
|
296
|
+
['HBD', str(d.hbd)],
|
|
297
|
+
['Max Phase', str(d.max_phase)],
|
|
298
|
+
['Molecule Type', str(d.molecule_type)],
|
|
299
|
+
];
|
|
300
|
+
for (const [label, value] of fields) {
|
|
301
|
+
if (value !== '-')
|
|
302
|
+
lines.push(`${label.padEnd(15)} ${value}`);
|
|
303
|
+
}
|
|
304
|
+
return lines.join('\n');
|
|
305
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { loadConfig, saveConfig, getEffectiveConfig, loadLocalProject } from '../config.js';
|
|
2
|
+
import { output } from '../output.js';
|
|
3
|
+
export function registerConfigCommands(program) {
|
|
4
|
+
program
|
|
5
|
+
.command('config')
|
|
6
|
+
.description('Show or update CLI configuration')
|
|
7
|
+
.option('--key <api-key>', 'Set API key')
|
|
8
|
+
.option('--url <api-url>', 'Set API URL')
|
|
9
|
+
.option('--console <console-url>', 'Set console URL')
|
|
10
|
+
.action((opts) => {
|
|
11
|
+
const config = loadConfig();
|
|
12
|
+
// Update fields if provided
|
|
13
|
+
let updated = false;
|
|
14
|
+
if (opts.key) {
|
|
15
|
+
config.api_key = opts.key;
|
|
16
|
+
updated = true;
|
|
17
|
+
}
|
|
18
|
+
if (opts.url) {
|
|
19
|
+
config.api_url = opts.url;
|
|
20
|
+
updated = true;
|
|
21
|
+
}
|
|
22
|
+
if (opts.console) {
|
|
23
|
+
config.console_url = opts.console;
|
|
24
|
+
updated = true;
|
|
25
|
+
}
|
|
26
|
+
if (updated) {
|
|
27
|
+
saveConfig(config);
|
|
28
|
+
process.stderr.write('Config saved.\n');
|
|
29
|
+
}
|
|
30
|
+
// Display current config
|
|
31
|
+
const effective = getEffectiveConfig();
|
|
32
|
+
const maskedKey = effective.api_key
|
|
33
|
+
? `***${effective.api_key.slice(-8)}`
|
|
34
|
+
: '(not set)';
|
|
35
|
+
const data = {
|
|
36
|
+
api_key: maskedKey,
|
|
37
|
+
api_url: effective.api_url,
|
|
38
|
+
console_url: effective.console_url,
|
|
39
|
+
active_project: effective.active_project || null,
|
|
40
|
+
active_project_name: effective.active_project_name || null,
|
|
41
|
+
};
|
|
42
|
+
const local = loadLocalProject();
|
|
43
|
+
const projectSource = local ? 'local .m3triq' : effective.active_project ? 'global' : '';
|
|
44
|
+
const human = [
|
|
45
|
+
`API Key: ${maskedKey}`,
|
|
46
|
+
`API URL: ${effective.api_url}`,
|
|
47
|
+
`Console URL: ${effective.console_url}`,
|
|
48
|
+
`Project: ${effective.active_project_name || '(none)'} ${effective.active_project ? `(${effective.active_project.substring(0, 8)})` : ''} ${projectSource ? `[${projectSource}]` : ''}`,
|
|
49
|
+
].join('\n');
|
|
50
|
+
output(data, human);
|
|
51
|
+
});
|
|
52
|
+
}
|