@mesh-tech/mesh-cli
Version:
CLI for Mesh platform development utilities
181 lines (180 loc) • 7.73 kB
JavaScript
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();
}
});
}