#!/usr/bin/env node /** * Survey host readiness for Ollama and OpenClaw memory setup. * * Emits a readable report by default, or JSON with --json. */ import { accessSync, constants, readFileSync, statSync } from "node:fs"; import os from "node:os"; import { dirname, join } from "node:path"; import { parseArgs } from "node:util"; type Report = { os: string; release: string; arch: string; shell_context: string; cpu_count: number; memory_gib: number | null; commands: Record; versions: Record; ollama_api: { reachable: boolean; models: string[] }; recommended_candidates: string[]; openclaw_install_note: string; }; function usageText(): string { return [ "Usage:", " node scripts/survey_host_readiness.ts [--ollama-host ] [--json]", "", "Options:", " --ollama-host Ollama host to probe (default http://127.0.0.1:11434)", " --json Emit JSON instead of a readable report", ].join("\n"); } function systemName(): string { if (process.platform === "win32") return "Windows"; if (process.platform === "darwin") return "Darwin"; if (process.platform === "linux") return "Linux"; return process.platform; } function shellContext(): string { if (process.platform === "win32") return "windows-native"; if (process.platform === "darwin") return "macos-native"; if (process.platform === "linux") { const lowerRelease = os.release().toLowerCase(); if (lowerRelease.includes("microsoft") || lowerRelease.includes("wsl")) { return "wsl"; } try { const version = readFileSync("/proc/version", "utf8").toLowerCase(); if (version.includes("microsoft")) { return "wsl"; } } catch { // ignore } return "linux-native"; } return "unknown"; } function isExecutableFile(path: string): boolean { try { const stats = statSync(path); if (!stats.isFile()) return false; if (process.platform === "win32") return true; accessSync(path, constants.X_OK); return true; } catch { return false; } } function commandCandidates(command: string): string[] { const normalized = command.trim(); if (!normalized) return []; const candidates: string[] = []; const unixBins = ["/usr/local/bin", "/opt/homebrew/bin", "/usr/bin", "/bin"]; if (normalized === "node") { candidates.push(process.execPath); } if (normalized === "npm") { const base = dirname(process.execPath); if (process.platform === "win32") { candidates.push(join(base, "npm.cmd"), join(base, "npm.exe")); } else { candidates.push(join(base, "npm")); } } if (process.platform === "win32") { if (normalized === "ollama") { candidates.push( "C:\\\\Program Files\\\\Ollama\\\\ollama.exe", "C:\\\\Program Files (x86)\\\\Ollama\\\\ollama.exe", ); } if (normalized === "node") { candidates.push("C:\\\\Program Files\\\\nodejs\\\\node.exe", "C:\\\\Program Files (x86)\\\\nodejs\\\\node.exe"); } if (normalized === "npm") { candidates.push("C:\\\\Program Files\\\\nodejs\\\\npm.cmd", "C:\\\\Program Files (x86)\\\\nodejs\\\\npm.cmd"); } if (normalized === "openclaw") { candidates.push("C:\\\\Program Files\\\\OpenClaw\\\\openclaw.exe", "C:\\\\Program Files (x86)\\\\OpenClaw\\\\openclaw.exe"); } return candidates; } for (const dir of unixBins) { candidates.push(join(dir, normalized)); } return candidates; } function resolveCommandPath(command: string): string | null { for (const candidate of commandCandidates(command)) { if (isExecutableFile(candidate)) { return candidate; } } return null; } function commandVersion(command: string, path: string | null): string | null { if (!path) return null; if (command === "node") { return process.version; } return null; } function recommendCandidates(memoryGib: number | null): string[] { if (memoryGib === null) { return ["embeddinggemma:300m-qat-q8_0", "nomic-embed-text:latest", "qwen3-embedding:0.6b"]; } if (memoryGib < 8) { return ["nomic-embed-text:latest", "embeddinggemma:300m-qat-q8_0"]; } if (memoryGib < 32) { return ["embeddinggemma:300m-qat-q8_0", "nomic-embed-text:latest", "qwen3-embedding:0.6b"]; } return [ "embeddinggemma:300m-qat-q8_0", "nomic-embed-text:latest", "qwen3-embedding:0.6b", "qwen3-embedding:latest", ]; } async function fetchWithTimeout(url: string, timeoutMs: number): Promise { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); try { return await fetch(url, { signal: controller.signal }); } finally { clearTimeout(timer); } } async function probeOllama(host: string): Promise<{ reachable: boolean; models: string[] }> { const url = host.replace(/\/+$/, "") + "/api/tags"; try { const response = await fetchWithTimeout(url, 5000); if (!response.ok) { return { reachable: false, models: [] }; } const text = await response.text(); const payload = JSON.parse(text) as { models?: unknown }; const models = Array.isArray(payload.models) ? payload.models : []; const names = models .map((item) => (item && typeof item === "object" ? (item as Record).name : undefined)) .filter((name): name is string => typeof name === "string"); return { reachable: true, models: names }; } catch { return { reachable: false, models: [] }; } } async function buildReport(ollamaHost: string): Promise { const memoryGib = os.totalmem() ? os.totalmem() / 1024 / 1024 / 1024 : null; const commands: Record = {}; const versions: Record = {}; for (const command of ["ollama", "openclaw", "node", "npm"]) { const path = resolveCommandPath(command); commands[command] = path; versions[command] = commandVersion(command, path); } commands.openclaw_management = commands.openclaw; versions.openclaw_management = versions.openclaw; const ollamaApi = await probeOllama(ollamaHost); const osName = systemName(); return { os: osName, release: os.release(), arch: os.arch(), shell_context: shellContext(), cpu_count: os.cpus().length, memory_gib: memoryGib === null ? null : Math.round(memoryGib * 100) / 100, commands, versions, ollama_api: ollamaApi, recommended_candidates: recommendCandidates(memoryGib === null ? null : memoryGib), openclaw_install_note: osName === "Windows" ? "Use Mac or Linux directly; use WSL on Windows for current official OpenClaw setup." : "Use the official Ollama OpenClaw launch flow.", }; } function printText(report: Report, ollamaHost: string): void { process.stdout.write("System\n"); process.stdout.write(` OS: ${report.os} ${report.release}\n`); process.stdout.write(` Context: ${report.shell_context}\n`); process.stdout.write(` Arch: ${report.arch}\n`); process.stdout.write(` CPUs: ${report.cpu_count}\n`); process.stdout.write(` RAM GiB: ${report.memory_gib ?? "unknown"}\n\n`); process.stdout.write("Commands\n"); for (const [command, path] of Object.entries(report.commands)) { const version = report.versions[command] ?? null; const label = command === "openclaw_management" ? "openclaw management" : command; if (path) { const suffix = version ? ` (${version})` : ""; process.stdout.write(` ${label}: ${path}${suffix}\n`); } else { process.stdout.write(` ${label}: missing\n`); } } process.stdout.write("\nOllama API\n"); if (report.ollama_api.reachable) { process.stdout.write(` ${ollamaHost}: reachable\n`); process.stdout.write( ` Models: ${report.ollama_api.models.length ? report.ollama_api.models.join(", ") : "none pulled"}\n`, ); } else { process.stdout.write(` ${ollamaHost}: unreachable\n`); } process.stdout.write("\nRecommended candidates\n"); for (const model of report.recommended_candidates) { process.stdout.write(` - ${model}\n`); } process.stdout.write(`\nOpenClaw install note\n ${report.openclaw_install_note}\n`); } async function main(): Promise { const parsed = parseArgs({ args: process.argv.slice(2), options: { "ollama-host": { type: "string", default: "http://127.0.0.1:11434" }, json: { type: "boolean", default: false }, help: { type: "boolean", default: false }, }, }); if (parsed.values.help) { process.stdout.write(`${usageText()}\n`); return 0; } const ollamaHost = String(parsed.values["ollama-host"] ?? "").trim() || "http://127.0.0.1:11434"; const report = await buildReport(ollamaHost); if (parsed.values.json) { process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); } else { printText(report, ollamaHost); } return 0; } void main() .then((code) => { process.exitCode = code; }) .catch((err) => { const message = err instanceof Error ? err.message : String(err); process.stderr.write(`${message}\n`); process.exitCode = 1; });