mirror of
https://github.com/khoaliber/paperclip.git
synced 2026-04-19 17:14:40 +00:00
Rename all workspace packages from @paperclip/* to @paperclipai/* and the CLI binary from `paperclip` to `paperclipai` in preparation for npm publishing. Bump CLI version to 0.1.0 and add package metadata (description, keywords, license, repository, files). Update all imports, documentation, user-facing messages, and tests accordingly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
188 lines
5.3 KiB
TypeScript
188 lines
5.3 KiB
TypeScript
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);
|
|
}
|