mirror of
https://github.com/paperclipai/paperclip
synced 2026-03-25 11:21:48 +00:00
feat(cli): add client commands and home-based local runtime defaults
This commit is contained in:
185
cli/src/commands/client/common.ts
Normal file
185
cli/src/commands/client/common.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
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;
|
||||
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("--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 `paperclip 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);
|
||||
}
|
||||
Reference in New Issue
Block a user