Spaces:
Paused
Paused
| 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>", "Path to Paperclip config file") | |
| .option("-d, --data-dir <path>", "Paperclip data directory root (isolates state from ~/.paperclip)") | |
| .option("--context <path>", "Path to CLI context file") | |
| .option("--profile <name>", "CLI context profile name") | |
| .option("--api-base <url>", "Base URL for the Paperclip API") | |
| .option("--api-key <token>", "Bearer token for agent-authenticated calls") | |
| .option("--json", "Output raw JSON"); | |
| if (opts?.includeCompany) { | |
| command.option("-C, --company-id <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<string, unknown>)); | |
| } 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, unknown>): string { | |
| const keyOrder = ["identifier", "id", "name", "status", "priority", "title", "action"]; | |
| const seen = new Set<string>(); | |
| 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); | |
| } | |