/** * Charm Hyper (https://hyper.charm.land) provider identity and auth for Prime Agent. */ import { readFileSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; import { getJson } from "./http.js"; export const PROVIDER = "hyper"; export const DISPLAY_NAME = "Charm Hyper"; export const SITE_URL = "https://hyper.charm.land"; export const API_BASE_URL = `${SITE_URL}/v1`; export const ENV_VAR = "HYPER_API_KEY"; /** * What Prime Agent resolves when no credential is stored. `!`-prefixed values run through * the shell (bash -c), and an empty result counts as an unresolved key rather than an * empty string, which turns "no key" into a readable error instead of a 401. */ export const API_KEY_CONFIG = `!printf %s "$${ENV_VAR}"`; /** * Prime Agent's agent dir, mirrored here: importing the runtime for it drags a * jiti-hostile module graph into the extension load path. */ export function agentDir(): string { const configured = process.env.PRIME_AGENT_CODING_AGENT_DIR ?? process.env.PI_CODING_AGENT_DIR; if (!configured) return join(homedir(), ".prime", "agent"); if (configured === "~") return homedir(); return configured.startsWith("~/") ? join(homedir(), configured.slice(2)) : configured; } const AUTH_FILE = (): string => join(agentDir(), "auth.json"); export const cachePath = (): string => join(agentDir(), "hyper-catalog.json"); export const isOffline = (): boolean => { const value = process.env.PI_OFFLINE; return value === "1" || value?.toLowerCase() === "true" || value?.toLowerCase() === "yes"; }; /** * The API-key credential in an auth.json payload. A provider entry is one credential or a * list of them (Prime Agent pools accounts: a subscription and an API key coexist), so both * shapes resolve here. */ export function storedKeyFrom(payload: unknown): string | undefined { if (typeof payload !== "object" || payload === null) return undefined; const entry = Object.getOwnPropertyDescriptor(payload, PROVIDER)?.value; for (const credential of Array.isArray(entry) ? entry : [entry]) { if (typeof credential !== "object" || credential === null) continue; const key = Object.getOwnPropertyDescriptor(credential, "key")?.value; if (typeof key === "string" && key !== "") return key; } return undefined; } /** * Key saved by `/login hyper` - a pasted key, or the device-code subscription login. This * and `HYPER_API_KEY` are the whole story: no key file, so the credential has one home, and * a missing key is reported as unresolved rather than sent as a placeholder. */ export function storedKey(): string | undefined { try { return storedKeyFrom(JSON.parse(readFileSync(AUTH_FILE(), "utf-8"))); } catch { return undefined; } } /** Key for the probe scripts: `HYPER_API_KEY`, else the stored credential. */ export const resolveKey = (): string | undefined => process.env[ENV_VAR] ?? storedKey(); /** The account's Hypercredit balance. `GET /v1/credits` is the only account endpoint. */ export async function fetchBalance(apiKey: string): Promise { const payload = await getJson(`${SITE_URL}/v1/credits`, { Authorization: `Bearer ${apiKey}` }); if (typeof payload !== "object" || payload === null) return undefined; for (const field of ["balance", "balance_usd"]) { const value = Object.getOwnPropertyDescriptor(payload, field)?.value; if (typeof value === "number") return value; } return undefined; }