UNPKG

@mesh-tech/mesh-cli

Version:

CLI for Mesh platform development utilities

181 lines (180 loc) 7.73 kB
import { renderMessageContent } from "@mesh-tech/agent-targets"; import { buildVcsEnricher } from "./vcs-enrich.js"; import { logError, logInfo, logWarn } from "../utils/log.js"; import { agentApiFetch, describeHttpError, describeNetworkError, resolveTarget, TargetResolutionError, } from "./agent-api-client.js"; const MAX_TITLE_LEN = 40; function truncate(s, max) { return s.length > max ? s.slice(0, max - 1) + "…" : s; } export function formatConversations(list, asJson) { if (asJson) { return JSON.stringify(list); } if (list.length === 0) { return "(no conversations)"; } const rows = list.map((c) => ({ id: c.id ?? "", title: truncate(c.title ?? "", MAX_TITLE_LEN), updated: c.updatedAt ?? c.lastUpdated ?? "", })); const idWidth = Math.max("ID".length, ...rows.map((r) => r.id.length)); const titleWidth = Math.max("TITLE".length, ...rows.map((r) => r.title.length)); const pad = (s, width) => s.padEnd(width); const header = `${pad("ID", idWidth)} ${pad("TITLE", titleWidth)} UPDATED`; const lines = rows.map((r) => `${pad(r.id, idWidth)} ${pad(r.title, titleWidth)} ${r.updated}`); return [header, ...lines].join("\n"); } function summarizeRefs(items) { return items .map((item) => { if (typeof item === "string") return item; if (item && typeof item === "object") { const rec = item; const label = rec.id ?? rec.name ?? rec.toolName; if (typeof label === "string") return label; } return "?"; }) .join(", "); } export function formatTranscript(body, asJson) { if (asJson) { return JSON.stringify(body); } const messages = body.messages ?? []; if (messages.length === 0) { return "(no messages)"; } return messages .map((m) => { const lines = [`${m.role}: ${renderMessageContent(m.content)}`]; if (Array.isArray(m.toolCalls) && m.toolCalls.length > 0) { lines.push(` tool-calls: ${summarizeRefs(m.toolCalls)}`); } if (Array.isArray(m.artifactRefs) && m.artifactRefs.length > 0) { lines.push(` artifacts: ${summarizeRefs(m.artifactRefs)}`); } return lines.join("\n"); }) .join("\n\n"); } export function formatArtifacts(list, asJson, refHint) { if (asJson) { return JSON.stringify(list); } if (list.length === 0) { return "(no artifacts)"; } const rows = list.map((a) => ({ id: a.id ?? "", kind: a.kind ?? "", title: a.title ?? "", })); const idWidth = Math.max("ARTIFACT-ID".length, ...rows.map((r) => r.id.length)); const kindWidth = Math.max("KIND".length, ...rows.map((r) => r.kind.length)); const pad = (s, width) => s.padEnd(width); const header = `${pad("ARTIFACT-ID", idWidth)} · ${pad("KIND", kindWidth)} · TITLE`; const lines = rows.map((r) => `${pad(r.id, idWidth)} · ${pad(r.kind, kindWidth)} · ${r.title}`); const exampleId = rows[0]?.id ?? ""; return [header, ...lines, "", `Download: mesh artifacts get ${refHint}:${exampleId}`].join("\n"); } function withTargetOptions(cmd) { return cmd .option("--target <name>", "Named agent target from the agent-targets registry") .option("--api-url <url>", "Agent API URL (overrides --target; ad-hoc, no registry lookup)") .option("--context <ctx>", "Zitadel auth context, used with --api-url (default: mesh.dev)") .option("--json", "Emit JSON (default when stdout is not a TTY)"); } async function runConversationsVerb(opts, apiPath, format, ctx) { let target; try { target = resolveTarget(opts); const res = await agentApiFetch(target, apiPath); if (!res.ok) { throw new Error(await describeHttpError(res, target, ctx)); } const body = (await res.json()); const asJson = opts.json ?? !process.stdout.isTTY; process.stdout.write(format(body, asJson) + "\n"); } catch (error) { reportCliError(error, target); } } function reportCliError(error, target) { if (error instanceof TargetResolutionError) { logError(error.message); } else { const message = error instanceof Error ? error.message : String(error); logError(message); if (target) { const hint = describeNetworkError(error, target); if (hint !== message) logWarn(hint); } } process.exitCode = 1; } export function registerConversationsCommands(program) { const group = program .command("conversations") .alias("conv") .description("Read a deployed Mesh agent's conversations (list, show, artifacts)"); withTargetOptions(group .command("list") .description("List the caller's conversations on an agent target")).action(async (opts) => { await runConversationsVerb(opts, "/v1/conversations", (body, asJson) => formatConversations(body.conversations ?? [], asJson)); }); withTargetOptions(group .command("show <id>") .description("Render a conversation's transcript")).action(async (id, opts) => { await runConversationsVerb(opts, `/v1/conversations/${encodeURIComponent(id)}`, (body, asJson) => formatTranscript(body, asJson), { id }); }); withTargetOptions(group .command("artifacts <id>") .description("List a conversation's artifacts")).action(async (id, opts) => { await runConversationsVerb(opts, `/v1/conversations/${encodeURIComponent(id)}/artifacts`, (body, asJson) => formatArtifacts(body.artifacts ?? [], asJson, id), { id }); }); group .command("pull <id>") .description("Pull a conversation + its designs/UI + recursive subagent transcripts into a bundle") .option("--target <name>", "Named agent target from the agent-targets registry") .option("--api-url <url>", "Agent API URL (overrides --target)") .option("--context <ctx>", "Zitadel auth context (default: mesh.dev)") .option("-o, --output <dir>", "Write the bundle directory here (prints JSON when omitted)") .option("--no-recursive", "Only pull direct subagents (default: recurse the delegate tree)") .option("--no-vcs", "Skip pulling each design's own vcs folder (docs/manifest); IR-only") .action(async (id, opts) => { const { pullConversation, writeBundle } = await import("@mesh-tech/agent-targets"); let target; let enricher; try { target = resolveTarget(opts); const resolved = target; enricher = opts.vcs === false ? undefined : await buildVcsEnricher(resolved); const bundle = await pullConversation({ fetchApi: (apiPath) => agentApiFetch(resolved, apiPath), enrichVcs: enricher?.enrichVcs }, id, { recursive: opts.recursive !== false, hintTarget: opts.target }); if (opts.output) { const written = await writeBundle(bundle, opts.output); logInfo(`wrote ${written.length} files → ${opts.output}`); if (bundle.warnings.length) { logWarn(`${bundle.warnings.length} warning(s): ${bundle.warnings .map((w) => `${w.stage}:${w.ref ?? ""}`) .join(", ")}`); } } else { process.stdout.write(JSON.stringify(bundle) + "\n"); } } catch (error) { reportCliError(error, target); } finally { await enricher?.cleanup(); } }); }