m3triq 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -3,6 +3,7 @@ import { Command } from 'commander';
3
3
  import { setJsonMode } from './output.js';
4
4
  import { getEffectiveConfig, requireApiKey } from './config.js';
5
5
  import { M3triqClient, AgentsClient } from './client.js';
6
+ import { reportCliError } from './errorReporter.js';
6
7
  import { registerConfigCommands } from './commands/config-cmd.js';
7
8
  import { registerProjectCommands } from './commands/projects.js';
8
9
  import { registerSessionCommands } from './commands/sessions.js';
@@ -23,7 +24,7 @@ const program = new Command();
23
24
  program
24
25
  .name('m3t')
25
26
  .description('M3TRIQ — protein-ligand analysis from the terminal')
26
- .version('0.2.4')
27
+ .version('0.2.6')
27
28
  .option('--json', 'Output as JSON (machine-readable)')
28
29
  .hook('preAction', (thisCommand) => {
29
30
  const opts = thisCommand.optsWithGlobals();
@@ -46,8 +47,37 @@ registerMdCommands(program);
46
47
  registerZincCommands(program);
47
48
  registerCreditsCommands(program);
48
49
  registerPricingCommands(program);
50
+ // Last-line-of-defense catches: anything that escapes a command handler.
51
+ // These fire BEFORE process.exit(), so the reporter has time to flush
52
+ // (it has its own 3-second timeout to bound the wait).
53
+ process.on('uncaughtException', async (err) => {
54
+ try {
55
+ await reportCliError(err, { kind: 'uncaughtException' });
56
+ }
57
+ catch { }
58
+ process.stderr.write(`Error: ${err?.message || String(err)}\n`);
59
+ process.exit(1);
60
+ });
61
+ process.on('unhandledRejection', async (reason) => {
62
+ try {
63
+ await reportCliError(reason, { kind: 'unhandledRejection' });
64
+ }
65
+ catch { }
66
+ const msg = reason instanceof Error ? reason.message : String(reason);
67
+ process.stderr.write(`Error: ${msg}\n`);
68
+ process.exit(1);
69
+ });
49
70
  // Catch async errors from all command actions
50
- program.parseAsync().catch((err) => {
71
+ program.parseAsync().catch(async (err) => {
72
+ // Don't beacon commander's own user-facing exits (--help, --version, bad args).
73
+ const code = err.code;
74
+ const isCommanderExit = typeof code === 'string' && code.startsWith('commander.');
75
+ if (!isCommanderExit) {
76
+ try {
77
+ await reportCliError(err, { kind: 'command' });
78
+ }
79
+ catch { }
80
+ }
51
81
  const message = err.message || String(err);
52
82
  if (message.includes('API error') || message.includes('MCP')) {
53
83
  process.stderr.write(`Error: ${message}\n`);
@@ -7,6 +7,7 @@ export function registerConfigCommands(program) {
7
7
  .option('--key <api-key>', 'Set API key')
8
8
  .option('--url <api-url>', 'Set API URL')
9
9
  .option('--console <console-url>', 'Set console URL')
10
+ .option('--error-reporting <on|off>', 'Toggle anonymous error reporting (default: on)')
10
11
  .action((opts) => {
11
12
  const config = loadConfig();
12
13
  // Update fields if provided
@@ -23,6 +24,21 @@ export function registerConfigCommands(program) {
23
24
  config.console_url = opts.console;
24
25
  updated = true;
25
26
  }
27
+ if (opts.errorReporting !== undefined) {
28
+ const v = String(opts.errorReporting).toLowerCase();
29
+ if (['off', 'false', '0', 'no'].includes(v)) {
30
+ config.error_reporting_disabled = true;
31
+ updated = true;
32
+ }
33
+ else if (['on', 'true', '1', 'yes'].includes(v)) {
34
+ config.error_reporting_disabled = false;
35
+ updated = true;
36
+ }
37
+ else {
38
+ process.stderr.write(`Error: --error-reporting expects on|off (got "${opts.errorReporting}")\n`);
39
+ process.exit(1);
40
+ }
41
+ }
26
42
  if (updated) {
27
43
  saveConfig(config);
28
44
  process.stderr.write('Config saved.\n');
@@ -38,14 +54,16 @@ export function registerConfigCommands(program) {
38
54
  console_url: effective.console_url,
39
55
  active_project: effective.active_project || null,
40
56
  active_project_name: effective.active_project_name || null,
57
+ error_reporting: effective.error_reporting_disabled ? 'off' : 'on',
41
58
  };
42
59
  const local = loadLocalProject();
43
60
  const projectSource = local ? 'local .m3triq' : effective.active_project ? 'global' : '';
44
61
  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}]` : ''}`,
62
+ `API Key: ${maskedKey}`,
63
+ `API URL: ${effective.api_url}`,
64
+ `Console URL: ${effective.console_url}`,
65
+ `Project: ${effective.active_project_name || '(none)'} ${effective.active_project ? `(${effective.active_project.substring(0, 8)})` : ''} ${projectSource ? `[${projectSource}]` : ''}`,
66
+ `Error reporting: ${effective.error_reporting_disabled ? 'off' : 'on'}`,
49
67
  ].join('\n');
50
68
  output(data, human);
51
69
  });
@@ -11,8 +11,8 @@ export function registerMdCommands(program) {
11
11
  md
12
12
  .command('run')
13
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)')
14
+ .option('--ligand-sdf <sdf>', 'Ligand SDF content or path to .sdf file (omit for apo simulation)')
15
+ .option('--ligand-smiles <smiles>', 'Ligand SMILES (omit for apo simulation)')
16
16
  .option('--diffdock-job <id>', 'DiffDock job ID (auto-fetches protein + ligand)')
17
17
  .option('--mode <mode>', 'Simulation mode: quick (10ns ~1hr) or standard (50ns ~5hrs)', 'quick')
18
18
  .option('--ns <n>', 'Override simulation duration in nanoseconds (1-100)')
@@ -42,7 +42,7 @@ export function registerMdCommands(program) {
42
42
  else {
43
43
  params.protein_pdb = opts.protein;
44
44
  }
45
- // Resolve ligand
45
+ // Resolve ligand (optional — omit for apo / protein-only MD)
46
46
  if (opts.ligandSdf) {
47
47
  if (fs.existsSync(opts.ligandSdf)) {
48
48
  params.ligand_sdf = fs.readFileSync(opts.ligandSdf, 'utf-8');
@@ -55,8 +55,7 @@ export function registerMdCommands(program) {
55
55
  params.ligand_smiles = opts.ligandSmiles;
56
56
  }
57
57
  else {
58
- process.stderr.write('Error: Provide --ligand-sdf or --ligand-smiles\n');
59
- process.exit(1);
58
+ process.stderr.write('Running protein-only (apo) MD — no ligand specified.\n');
60
59
  }
61
60
  }
62
61
  if (opts.ns)
package/dist/config.d.ts CHANGED
@@ -5,6 +5,7 @@ export interface M3triqConfig {
5
5
  console_url?: string;
6
6
  active_project?: string;
7
7
  active_project_name?: string;
8
+ error_reporting_disabled?: boolean;
8
9
  }
9
10
  interface LocalProjectConfig {
10
11
  project_id: string;
@@ -0,0 +1,5 @@
1
+ export interface ReportContext {
2
+ kind?: string;
3
+ argv?: string[];
4
+ }
5
+ export declare function reportCliError(err: unknown, context?: ReportContext): Promise<void>;
@@ -0,0 +1,77 @@
1
+ // Beacon CLI errors to Django POST /api/errors/.
2
+ // Best-effort: never throws, never blocks the exit beyond ~3 seconds.
3
+ import os from 'node:os';
4
+ import { getEffectiveConfig } from './config.js';
5
+ const REPORT_TIMEOUT_MS = 3000;
6
+ export async function reportCliError(err, context = {}) {
7
+ let config;
8
+ try {
9
+ config = getEffectiveConfig();
10
+ }
11
+ catch {
12
+ return;
13
+ }
14
+ // Opt-out gate. Either the config flag or env var disables reporting.
15
+ if (config.error_reporting_disabled === true)
16
+ return;
17
+ if (process.env.M3TRIQ_NO_ERROR_REPORTING === '1')
18
+ return;
19
+ const message = err instanceof Error ? err.message : String(err);
20
+ const stack = err instanceof Error ? (err.stack || '') : '';
21
+ const argv = (context.argv || process.argv).slice(1);
22
+ // Replace any token-like value (long hex/base64 strings) with a placeholder.
23
+ const sanitizedArgv = argv.map(a => /^[A-Za-z0-9_\-+/=]{32,}$/.test(a) ? '<redacted>' : a);
24
+ const url_or_cmd = sanitizedArgv.join(' ').slice(0, 500);
25
+ const cliVersion = await readCliVersion();
26
+ const payload = {
27
+ source: 'cli',
28
+ session_id: '',
29
+ url_or_cmd,
30
+ message: message.slice(0, 5000),
31
+ stack: stack.slice(0, 10000),
32
+ metadata: {
33
+ kind: context.kind || 'command',
34
+ cli_version: cliVersion,
35
+ node_version: process.version,
36
+ platform: process.platform,
37
+ arch: process.arch,
38
+ os_release: os.release(),
39
+ },
40
+ };
41
+ const url = `${config.api_url || 'https://server.m3triq.com'}/api/errors/`;
42
+ const ctrl = new AbortController();
43
+ const timer = setTimeout(() => ctrl.abort(), REPORT_TIMEOUT_MS);
44
+ try {
45
+ await fetch(url, {
46
+ method: 'POST',
47
+ headers: { 'Content-Type': 'application/json' },
48
+ body: JSON.stringify(payload),
49
+ signal: ctrl.signal,
50
+ });
51
+ }
52
+ catch {
53
+ // Best-effort: swallow network errors, timeouts, anything.
54
+ }
55
+ finally {
56
+ clearTimeout(timer);
57
+ }
58
+ }
59
+ let _cachedVersion = null;
60
+ async function readCliVersion() {
61
+ if (_cachedVersion)
62
+ return _cachedVersion;
63
+ try {
64
+ const fs = await import('node:fs');
65
+ const path = await import('node:path');
66
+ const url = await import('node:url');
67
+ const here = path.dirname(url.fileURLToPath(import.meta.url));
68
+ const pkgPath = path.join(here, '..', 'package.json');
69
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
70
+ _cachedVersion = pkg.version || 'unknown';
71
+ return _cachedVersion;
72
+ }
73
+ catch {
74
+ _cachedVersion = 'unknown';
75
+ return _cachedVersion;
76
+ }
77
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "m3triq",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "M3TRIQ — protein-ligand analysis from the terminal",
5
5
  "type": "module",
6
6
  "bin": {