/** * Environment test for the DeepSeek (via Hermes) adapter. */ import { execFile } from "node:child_process"; import { promisify } from "node:util"; import fs from "node:fs/promises"; import path from "node:path"; import { HERMES_CLI, ADAPTER_TYPE, DEFAULT_PROFILE_HOME, } from "../shared/constants.js"; const execFileAsync = promisify(execFile); function asString(v) { return typeof v === "string" ? v : undefined; } async function checkCliInstalled(command) { try { await execFileAsync(command, ["--version"], { timeout: 10_000 }); return null; } catch (err) { if (err && err.code === "ENOENT") { return { level: "error", message: `Hermes CLI "${command}" not found in PATH`, hint: "Install Hermes Agent: pip install hermes-agent", code: "deepseek_hermes_cli_not_found", }; } return null; } } async function checkProfile(profileHome) { try { const stat = await fs.stat(profileHome); if (!stat.isDirectory()) { return { level: "error", message: `Profile path is not a directory: ${profileHome}`, hint: "Create the directory or override hermesProfileHome in adapter config.", code: "deepseek_profile_not_dir", }; } } catch { return { level: "error", message: `Hermes profile dir does not exist: ${profileHome}`, hint: "Create the profile dir with config.yaml + .env (DEEPSEEK_API_KEY).", code: "deepseek_profile_missing", }; } const configPath = path.join(profileHome, "config.yaml"); try { await fs.stat(configPath); } catch { return { level: "error", message: `Profile is missing config.yaml: ${configPath}`, hint: "Add config.yaml with model.default + model.base_url + model.key_env.", code: "deepseek_profile_no_config", }; } return { level: "info", message: `Profile resolved: ${profileHome}`, code: "deepseek_profile_ok", }; } async function checkApiKey(profileHome, configEnv) { // 1. config.env (resolved by Paperclip from secrets) if (configEnv && typeof configEnv === "object" && asString(configEnv.DEEPSEEK_API_KEY)) { return { level: "info", message: "DEEPSEEK_API_KEY found in adapter env config", code: "deepseek_api_key_in_config", }; } // 2. Profile-local .env try { const envFile = path.join(profileHome, ".env"); const text = await fs.readFile(envFile, "utf-8"); if (/^\s*DEEPSEEK_API_KEY=/m.test(text)) { return { level: "info", message: `DEEPSEEK_API_KEY found in ${envFile}`, code: "deepseek_api_key_in_profile", }; } } catch { // ignore } // 3. Process env if (process.env.DEEPSEEK_API_KEY) { return { level: "info", message: "DEEPSEEK_API_KEY found in Paperclip process env", code: "deepseek_api_key_in_process", }; } return { level: "error", message: "DEEPSEEK_API_KEY not found in adapter env, profile .env, or process env", hint: "Add DEEPSEEK_API_KEY to /.env or to the agent's env secrets.", code: "deepseek_api_key_missing", }; } export async function testEnvironment(ctx) { const config = ctx.config ?? {}; const command = asString(config.hermesCommand) || HERMES_CLI; const profileHome = asString(config.hermesProfileHome) || DEFAULT_PROFILE_HOME; const checks = []; const cliCheck = await checkCliInstalled(command); if (cliCheck) { checks.push(cliCheck); if (cliCheck.level === "error") { return { adapterType: ADAPTER_TYPE, status: "fail", checks, testedAt: new Date().toISOString(), }; } } const profileCheck = await checkProfile(profileHome); checks.push(profileCheck); if (profileCheck.level === "error") { return { adapterType: ADAPTER_TYPE, status: "fail", checks, testedAt: new Date().toISOString(), }; } const apiKeyCheck = await checkApiKey(profileHome, config.env); checks.push(apiKeyCheck); const model = asString(config.model); checks.push({ level: "info", message: model ? `Model: ${model}` : "Using profile default model", code: "deepseek_model", }); const hasErrors = checks.some((c) => c.level === "error"); const hasWarnings = checks.some((c) => c.level === "warn"); return { adapterType: ADAPTER_TYPE, status: hasErrors ? "fail" : hasWarnings ? "warn" : "pass", checks, testedAt: new Date().toISOString(), }; }