import pc from "picocolors"; import type { Command } from "commander"; import { readConfig } from "../../config/store.js"; import { readContext, resolveProfile, type ClientContextProfile } from "../../client/context.js"; import { ApiRequestError, PaperclipApiClient } from "../../client/http.js"; export interface BaseClientOptions { config?: string; dataDir?: string; context?: string; profile?: string; apiBase?: string; apiKey?: string; companyId?: string; json?: boolean; } export interface ResolvedClientContext { api: PaperclipApiClient; companyId?: string; profileName: string; profile: ClientContextProfile; json: boolean; } export function addCommonClientOptions(command: Command, opts?: { includeCompany?: boolean }): Command { command .option("-c, --config ", "Path to Paperclip config file") .option("-d, --data-dir ", "Paperclip data directory root (isolates state from ~/.paperclip)") .option("--context ", "Path to CLI context file") .option("--profile ", "CLI context profile name") .option("--api-base ", "Base URL for the Paperclip API") .option("--api-key ", "Bearer token for agent-authenticated calls") .option("--json", "Output raw JSON"); if (opts?.includeCompany) { command.option("-C, --company-id ", "Company ID (overrides context default)"); } return command; } export function resolveCommandContext( options: BaseClientOptions, opts?: { requireCompany?: boolean }, ): ResolvedClientContext { const context = readContext(options.context); const { name: profileName, profile } = resolveProfile(context, options.profile); const apiBase = options.apiBase?.trim() || process.env.PAPERCLIP_API_URL?.trim() || profile.apiBase || inferApiBaseFromConfig(options.config); const apiKey = options.apiKey?.trim() || process.env.PAPERCLIP_API_KEY?.trim() || readKeyFromProfileEnv(profile); const companyId = options.companyId?.trim() || process.env.PAPERCLIP_COMPANY_ID?.trim() || profile.companyId; if (opts?.requireCompany && !companyId) { throw new Error( "Company ID is required. Pass --company-id, set PAPERCLIP_COMPANY_ID, or set context profile companyId via `paperclipai context set`.", ); } const api = new PaperclipApiClient({ apiBase, apiKey }); return { api, companyId, profileName, profile, json: Boolean(options.json), }; } export function printOutput(data: unknown, opts: { json?: boolean; label?: string } = {}): void { if (opts.json) { console.log(JSON.stringify(data, null, 2)); return; } if (opts.label) { console.log(pc.bold(opts.label)); } if (Array.isArray(data)) { if (data.length === 0) { console.log(pc.dim("(empty)")); return; } for (const item of data) { if (typeof item === "object" && item !== null) { console.log(formatInlineRecord(item as Record)); } else { console.log(String(item)); } } return; } if (typeof data === "object" && data !== null) { console.log(JSON.stringify(data, null, 2)); return; } if (data === undefined || data === null) { console.log(pc.dim("(null)")); return; } console.log(String(data)); } export function formatInlineRecord(record: Record): string { const keyOrder = ["identifier", "id", "name", "status", "priority", "title", "action"]; const seen = new Set(); const parts: string[] = []; for (const key of keyOrder) { if (!(key in record)) continue; parts.push(`${key}=${renderValue(record[key])}`); seen.add(key); } for (const [key, value] of Object.entries(record)) { if (seen.has(key)) continue; if (typeof value === "object") continue; parts.push(`${key}=${renderValue(value)}`); } return parts.join(" "); } function renderValue(value: unknown): string { if (value === null || value === undefined) return "-"; if (typeof value === "string") { const compact = value.replace(/\s+/g, " ").trim(); return compact.length > 90 ? `${compact.slice(0, 87)}...` : compact; } if (typeof value === "number" || typeof value === "boolean") { return String(value); } return "[object]"; } function inferApiBaseFromConfig(configPath?: string): string { const envHost = process.env.PAPERCLIP_SERVER_HOST?.trim() || "localhost"; let port = Number(process.env.PAPERCLIP_SERVER_PORT || ""); if (!Number.isFinite(port) || port <= 0) { try { const config = readConfig(configPath); port = Number(config?.server?.port ?? 3100); } catch { port = 3100; } } if (!Number.isFinite(port) || port <= 0) { port = 3100; } return `http://${envHost}:${port}`; } function readKeyFromProfileEnv(profile: ClientContextProfile): string | undefined { if (!profile.apiKeyEnvVarName) return undefined; return process.env[profile.apiKeyEnvVarName]?.trim() || undefined; } export function handleCommandError(error: unknown): never { if (error instanceof ApiRequestError) { const detailSuffix = error.details !== undefined ? ` details=${JSON.stringify(error.details)}` : ""; console.error(pc.red(`API error ${error.status}: ${error.message}${detailSuffix}`)); process.exit(1); } const message = error instanceof Error ? error.message : String(error); console.error(pc.red(message)); process.exit(1); }