/**
* 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<number | undefined> {
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;
}