sagemath.ts•2.58 kB
import { spawn } from 'node:child_process';
import { promises as fs } from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import { config } from '../config.js';
export interface SageRunResult {
  stdout: string;
  stderr: string;
  exitCode: number | null;
  durationMs: number;
  timedOut: boolean;
}
function getSageExecutable(): string {
  return (config.SagePath?.trim()) || (process.env.SAGE_PATH?.trim()) || 'sage';
}
async function runProcess(cmd: string, args: string[], timeoutMs: number): Promise<SageRunResult> {
  const start = Date.now();
  let stdout = '';
  let stderr = '';
  let timedOut = false;
  return new Promise<SageRunResult>((resolve) => {
    let child: ReturnType<typeof spawn> | undefined;
    try {
      child = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'] });
    } catch (err: any) {
      resolve({
        stdout: '',
        stderr: `Failed to spawn process: ${err?.message || String(err)}`,
        exitCode: null,
        durationMs: Date.now() - start,
        timedOut: false,
      });
      return;
    }
    const t = setTimeout(() => {
      timedOut = true;
      try { child?.kill('SIGKILL'); } catch {}
    }, Math.max(1, timeoutMs));
    // Handle asynchronous spawn errors (e.g., command not found)
    child.on('error', (err) => {
      clearTimeout(t);
      resolve({
        stdout: '',
        stderr: `Process error: ${err?.message || String(err)}`,
        exitCode: null,
        durationMs: Date.now() - start,
        timedOut,
      });
    });
    child.stdout?.on('data', (d) => { stdout += d.toString(); });
    child.stderr?.on('data', (d) => { stderr += d.toString(); });
    child.on('close', (code) => {
      clearTimeout(t);
      resolve({
        stdout,
        stderr,
        exitCode: code,
        durationMs: Date.now() - start,
        timedOut,
      });
    });
  });
}
export async function getSageVersion(timeoutMs = 5000): Promise<SageRunResult> {
  const sage = getSageExecutable();
  return runProcess(sage, ['--version'], timeoutMs);
}
export async function evaluateSage(code: string, timeoutMs = 10000): Promise<SageRunResult & { tmpDir?: string }> {
  const sage = getSageExecutable();
  const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-sage-'));
  const scriptPath = path.join(tmpRoot, 'script.sage');
  await fs.writeFile(scriptPath, code, 'utf8');
  const result = await runProcess(sage, [scriptPath], timeoutMs);
  // Attempt cleanup
  try { await fs.rm(tmpRoot, { recursive: true, force: true }); } catch {}
  return { ...result };
}