spb/autoscholar
Public
1import { execFileSync } from "node:child_process";2import { readFileSync } from "node:fs";3import { createRequire } from "node:module";4import { arch, cpus, hostname, platform, release, totalmem, userInfo } from "node:os";5import type { NodeMetadata } from "@autoscholar/schemas";6import { WORKER_PROTOCOL_VERSION } from "@autoscholar/schemas";78const require = createRequire(import.meta.url);910function tryExec(cmd: string, args: string[]): string | undefined {11 try {12 return execFileSync(cmd, args, {13 encoding: "utf8",14 timeout: 8000,15 stdio: ["ignore", "pipe", "ignore"],16 }).trim();17 } catch {18 return undefined;19 }20}2122export function sdkVersion(): string {23 for (const p of [new URL("../node_modules/@anthropic-ai/claude-agent-sdk/package.json", import.meta.url), new URL("../../../node_modules/@anthropic-ai/claude-agent-sdk/package.json", import.meta.url)]) {24 try {25 return (JSON.parse(readFileSync(p, "utf8")) as { version: string }).version;26 } catch {27 /* try next */28 }29 }30 try {31 const entry = require.resolve("@anthropic-ai/claude-agent-sdk");32 const m = entry.match(/claude-agent-sdk@(\d+\.\d+\.\d+)/);33 if (m) return m[1]!;34 } catch {35 /* unknown */36 }37 return "unknown";38}3940export function workerVersion(): string {41 try {42 return (43 JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")) as { version: string }44 ).version;45 } catch {46 return "0.0.0";47 }48}4950export function machineId(): string {51 const ioreg = tryExec("/usr/sbin/ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"]);52 const m = ioreg?.match(/"IOPlatformUUID" = "([^"]+)"/);53 return m?.[1] ?? `${hostname()}-${arch()}`;54}5556export function collectNodeMetadata(opts: { simulated: boolean; profiles: string[] }): NodeMetadata {57 const tools: Record<string, string> = {};58 const probe = (name: string, cmd: string, args: string[]) => {59 const v = tryExec(cmd, args);60 if (v) tools[name] = v.split("\n")[0]!.slice(0, 80);61 };62 probe("node", "node", ["--version"]);63 probe("python3", "/opt/homebrew/bin/python3", ["--version"]);64 probe("uv", "/opt/homebrew/bin/uv", ["--version"]);65 probe("tectonic", process.env.TECTONIC_BIN ?? "/opt/homebrew/bin/tectonic", ["--version"]);66 probe("pdftoppm", "/opt/homebrew/bin/pdftoppm", ["-v"]);67 probe("git", "git", ["--version"]);68 probe("R", "/opt/homebrew/bin/Rscript", ["--version"]);69 probe("julia", "/opt/homebrew/bin/julia", ["--version"]);70 tools["claude-agent-sdk"] = sdkVersion();71 const sandboxAvailable =72 platform() === "darwin"73 ? !!tryExec("/usr/bin/sandbox-exec", ["-n", "no-network", "/usr/bin/true"]) ||74 tryExec("/bin/ls", ["/usr/bin/sandbox-exec"]) === "/usr/bin/sandbox-exec"75 : false;76 return {77 machineId: machineId(),78 hostname: hostname(),79 platform: platform(),80 arch: arch(),81 cpuCores: cpus().length,82 memoryBytes: totalmem(),83 osVersion: tryExec("/usr/bin/sw_vers", ["-productVersion"]) ?? release(),84 workerVersion: workerVersion(),85 protocolVersion: WORKER_PROTOCOL_VERSION,86 sdkVersion: sdkVersion(),87 tools,88 profiles: opts.profiles,89 user: userInfo().username,90 sandboxAvailable: opts.simulated ? false : sandboxAvailable,91 ...(opts.simulated ? { simulated: true } : {}),92 } as NodeMetadata;93}9495export function loadAverage(): { load1: number; memFreeBytes: number; diskFreeBytes: number } {96 const load1 = (() => {97 try {98 const { loadavg } = require("node:os") as typeof import("node:os");99 return loadavg()[0] ?? 0;100 } catch {101 return 0;102 }103 })();104 const { freemem } = require("node:os") as typeof import("node:os");105 let diskFreeBytes = 0;106 const df = tryExec("/bin/df", ["-k", process.env.WORKER_ROOT ?? "/"]);107 const line = df?.split("\n")[1];108 if (line) diskFreeBytes = Number(line.split(/\s+/)[3] ?? 0) * 1024;109 return { load1, memFreeBytes: freemem(), diskFreeBytes };110}111