import { existsSync, readFileSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; import type { AgentTickChoiceInteractionMode, AgentTickConfig, AgentTickConfirmBeforeSubmit, AgentTickOptionPlacement, AgentTickPresentation, } from "./types"; export const DEFAULT_AGENT_TICK_TIMEOUT_MS = 0; export const DEFAULT_AGENT_TICK_PRESENTATION: AgentTickPresentation = { choiceInteractionMode: "click-to-submit", optionPlacement: "inline-after-content", confirmBeforeSubmit: "always", }; function asNonEmptyString(value: unknown): string | undefined { return typeof value === "string" && value.trim() ? value.trim() : undefined; } function normalizeServer(server: string): string { return server.replace(/\/+$/, ""); } function readJsonConfig(path: string | undefined): Partial | null { if (!path || !existsSync(path)) return null; try { const parsed = JSON.parse(readFileSync(path, "utf8")) as Record; const server = asNonEmptyString(parsed.server ?? parsed.baseUrl ?? parsed.url); const token = asNonEmptyString(parsed.token ?? parsed.agentToken ?? parsed.apiToken); return { ...(server ? { server: normalizeServer(server) } : {}), ...(token ? { token } : {}), }; } catch { return null; } } export function resolveAgentTickConfig( env: Record = process.env, homeDirectory = homedir(), ): AgentTickConfig | null { if (env.PI_AGENT_TICK_DISABLED === "1" || env.PI_AGENT_TICK_DISABLED === "true") return null; const configuredPath = asNonEmptyString(env.AGENT_TICK_CONFIG); const fileConfig = readJsonConfig(configuredPath) ?? readJsonConfig(join(homeDirectory, ".config", "agent-tick", "config.json")); const envServer = asNonEmptyString(env.AGENT_TICK_SERVER); const envToken = asNonEmptyString(env.AGENT_TICK_TOKEN); const server = envServer ? normalizeServer(envServer) : fileConfig?.server; const token = envToken ?? fileConfig?.token; return server && token ? { server, token } : null; } export function getAgentTickTimeoutMs(env: Record = process.env): number { const raw = asNonEmptyString(env.PI_AGENT_TICK_TIMEOUT_MS); if (!raw) return DEFAULT_AGENT_TICK_TIMEOUT_MS; if (["0", "none", "never", "infinite", "unlimited"].includes(raw.toLowerCase())) return 0; const parsed = Number(raw); return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_AGENT_TICK_TIMEOUT_MS; } function parseChoiceInteractionMode(value: string | undefined): AgentTickChoiceInteractionMode | undefined { if (value === "click-to-submit" || value === "select-then-submit") return value; return undefined; } function parseOptionPlacement(value: string | undefined): AgentTickOptionPlacement | undefined { if (value === "sticky-bottom" || value === "inline-after-content") return value; return undefined; } function parseConfirmBeforeSubmit(value: string | undefined): AgentTickConfirmBeforeSubmit | undefined { if (!value) return undefined; const normalized = value.trim().toLowerCase(); if (["1", "true", "yes", "always"].includes(normalized)) return "always"; if (["0", "false", "no", "never"].includes(normalized)) return "never"; return undefined; } export function resolveAgentTickPresentation(input: { choiceInteractionMode?: string; optionPlacement?: string; confirmBeforeSubmit?: boolean | string; } = {}, env: Record = process.env): AgentTickPresentation { const paramConfirm = typeof input.confirmBeforeSubmit === "boolean" ? input.confirmBeforeSubmit ? "always" : "never" : parseConfirmBeforeSubmit(input.confirmBeforeSubmit); return { choiceInteractionMode: parseChoiceInteractionMode(input.choiceInteractionMode) ?? parseChoiceInteractionMode(asNonEmptyString(env.PI_AGENT_TICK_CHOICE_INTERACTION_MODE)) ?? DEFAULT_AGENT_TICK_PRESENTATION.choiceInteractionMode, optionPlacement: parseOptionPlacement(input.optionPlacement) ?? parseOptionPlacement(asNonEmptyString(env.PI_AGENT_TICK_OPTION_PLACEMENT)) ?? DEFAULT_AGENT_TICK_PRESENTATION.optionPlacement, confirmBeforeSubmit: paramConfirm ?? parseConfirmBeforeSubmit(asNonEmptyString(env.PI_AGENT_TICK_CONFIRM_BEFORE_SUBMIT)) ?? DEFAULT_AGENT_TICK_PRESENTATION.confirmBeforeSubmit, }; } export function redactTokenLikeText(text: string): string { return text.replace(/(token|authorization|api[-_]?key|password|secret)(\s*[=:]\s*)([^\s]+)/gi, "$1$2[REDACTED]"); }