acpx
Version:
Headless CLI client for the Agent Client Protocol (ACP) — talk to coding agents from the command line
1,055 lines (1,054 loc) • 39.4 kB
JavaScript
import { A as findSession, I as pruneSessions, N as listSessions, P as listSessionsForAgent, St as isProcessAlive, ct as parsePermissionNotice, j as findSessionByDirectoryWalk, k as findGitRepositoryRoot, lt as parsePromptStopReason, rt as extractSessionUpdateNotification, st as parseJsonRpcErrorMessage } from "./ipc-CQUNQ8QQ.js";
import { C as setSessionModeOnOwner, S as setSessionMode, T as setSessionModelOnOwner, _ as cancelSessionPrompt, a as runQueuedTask, at as normalizeQueueOwnerTtlMs, b as setSessionConfigOption, dt as TimeoutError, g as listAgentSessions, h as ensureSession, i as runOnce, it as DEFAULT_QUEUE_OWNER_TTL_MS, m as createSessionWithClient, n as runSessionQueueOwner, o as sendSessionDirect, p as createSession, r as sendSession, t as queueOwnerRuntimeTestInternals, ut as InterruptedError, v as closeSession, w as setSessionModel, x as setSessionConfigOptionOnOwner, y as sessionControlTestInternals } from "./queue-owner-runtime-ucHvXJ33.js";
// \0rolldown/runtime.js
var __defProp = Object.defineProperty;
var __exportAll = (all, no_symbols) => {
let target = {};
for (var name in all) __defProp(target, name, {
get: all[name],
enumerable: true
});
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
return target;
};
// src/session/session.ts
var session_exports = /* @__PURE__ */ __exportAll({
DEFAULT_HISTORY_LIMIT: () => 20,
DEFAULT_QUEUE_OWNER_TTL_MS: () => DEFAULT_QUEUE_OWNER_TTL_MS,
InterruptedError: () => InterruptedError,
TimeoutError: () => TimeoutError,
cancelSessionPrompt: () => cancelSessionPrompt,
closeSession: () => closeSession,
createSession: () => createSession,
createSessionWithClient: () => createSessionWithClient,
ensureSession: () => ensureSession,
findGitRepositoryRoot: () => findGitRepositoryRoot,
findSession: () => findSession,
findSessionByDirectoryWalk: () => findSessionByDirectoryWalk,
isProcessAlive: () => isProcessAlive,
listAgentSessions: () => listAgentSessions,
listSessions: () => listSessions,
listSessionsForAgent: () => listSessionsForAgent,
normalizeQueueOwnerTtlMs: () => normalizeQueueOwnerTtlMs,
pruneSessions: () => pruneSessions,
queueOwnerRuntimeTestInternals: () => queueOwnerRuntimeTestInternals,
runOnce: () => runOnce,
runQueuedTask: () => runQueuedTask,
runSessionQueueOwner: () => runSessionQueueOwner,
sendSession: () => sendSession,
sendSessionDirect: () => sendSessionDirect,
sessionControlTestInternals: () => sessionControlTestInternals,
setSessionConfigOption: () => setSessionConfigOption,
setSessionConfigOptionOnOwner: () => setSessionConfigOptionOnOwner,
setSessionMode: () => setSessionMode,
setSessionModeOnOwner: () => setSessionModeOnOwner,
setSessionModel: () => setSessionModel,
setSessionModelOnOwner: () => setSessionModelOnOwner
});
// src/cli/timer-duration.ts
const MAX_TIMER_DELAY_MS = 2147483647;
function toTimerMilliseconds(seconds, allowZero) {
if (allowZero && seconds === 0) return 0;
const milliseconds = Math.max(1, Math.round(seconds * 1e3));
return milliseconds <= 2147483647 ? milliseconds : void 0;
}
// src/acp/jsonrpc-error.ts
const OUTPUT_ERROR_JSONRPC_CODES = {
NO_SESSION: -32002,
TIMEOUT: -32070,
PERMISSION_DENIED: -32071,
PERMISSION_PROMPT_UNAVAILABLE: -32072,
EXEC_DISABLED: -32603,
RUNTIME: -32603,
USAGE: -32602
};
function hasValidAcpError(acp) {
return Boolean(acp && Number.isFinite(acp.code) && typeof acp.message === "string" && acp.message.trim().length > 0);
}
function buildFallbackData(params) {
const data = {
acpxCode: params.outputCode,
detailCode: params.detailCode,
origin: params.origin,
retryable: params.retryable,
timestamp: params.timestamp,
sessionId: params.sessionId
};
for (const [key, value] of Object.entries(data)) if (value === void 0) delete data[key];
return data;
}
function mergeAcpErrorData(acpData, fallbackData) {
if (Object.keys(fallbackData).length === 0) return acpData;
if (acpData === void 0) return fallbackData;
if (acpData && typeof acpData === "object" && !Array.isArray(acpData)) return {
...fallbackData,
...acpData
};
return acpData;
}
function buildErrorObject(params) {
const fallbackData = buildFallbackData(params);
if (hasValidAcpError(params.acp)) {
const data = mergeAcpErrorData(params.acp.data, fallbackData);
return {
code: params.acp.code,
message: params.acp.message,
...data !== void 0 ? { data } : {}
};
}
const data = fallbackData;
return {
code: OUTPUT_ERROR_JSONRPC_CODES[params.outputCode] ?? -32603,
message: params.message,
...Object.keys(data).length > 0 ? { data } : {}
};
}
function buildJsonRpcErrorResponse(params) {
return {
jsonrpc: "2.0",
id: params.id ?? null,
error: buildErrorObject(params)
};
}
// src/cli/output/read-suppression.ts
const SUPPRESSED_READ_OUTPUT = "[read output suppressed]";
function inferToolKindFromTitle(title) {
const normalized = title?.trim().toLowerCase();
if (!normalized) return;
const head = normalized.split(/[:\s]/, 1)[0];
if (!head) return;
if ([
"read",
"cat",
"open",
"view"
].includes(head)) return "read";
}
function isReadLikeTool(tool) {
return tool.kind?.trim().toLowerCase() === "read" || inferToolKindFromTitle(tool.title) === "read";
}
// src/cli/output/json-formatter.ts
const DEFAULT_JSON_SESSION_ID = "unknown";
function asRecord$1(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) return;
return value;
}
function jsonRpcIdKey(value) {
if (typeof value === "string") return `s:${value}`;
if (typeof value === "number" && Number.isFinite(value)) return `n:${value}`;
}
function requestCorrelationKey(id, direction) {
const idKey = jsonRpcIdKey(id);
return idKey === void 0 ? void 0 : `${direction ?? "unknown"}:${idKey}`;
}
function reverseDirection(direction) {
if (direction === void 0) return;
return direction === "inbound" ? "outbound" : "inbound";
}
function sanitizeReadResult(result) {
const record = asRecord$1(result);
if (!record || typeof record.content !== "string") return result;
return {
...record,
content: SUPPRESSED_READ_OUTPUT
};
}
function sanitizeToolContent(content) {
if (!Array.isArray(content)) return content;
return [{
type: "content",
content: {
type: "text",
text: SUPPRESSED_READ_OUTPUT
}
}];
}
function sanitizeToolMessage(message) {
const root = asRecord$1(message);
const params = asRecord$1(root?.params);
const update = asRecord$1(params?.update);
if (!root || !params || !update) return message;
return {
...root,
params: {
...params,
update: sanitizeToolUpdate(update)
}
};
}
function sanitizeToolUpdate(update) {
return {
...update,
rawOutput: sanitizedRawOutput(update),
content: sanitizedContent(update)
};
}
function sanitizedRawOutput(update) {
if (Object.prototype.hasOwnProperty.call(update, "rawOutput") && update.rawOutput !== void 0) return { content: SUPPRESSED_READ_OUTPUT };
return update.rawOutput;
}
function sanitizedContent(update) {
if (Object.prototype.hasOwnProperty.call(update, "content") && update.content !== void 0) return sanitizeToolContent(update.content);
return update.content;
}
var JsonMessageSanitizer = class {
suppressReads;
partialHistory;
requestMethodById = /* @__PURE__ */ new Map();
toolStateById = /* @__PURE__ */ new Map();
constructor(suppressReads, partialHistory = false) {
this.suppressReads = suppressReads;
this.partialHistory = partialHistory;
}
sanitize(message, direction) {
if (!this.suppressReads) return message;
const sanitizedResponse = this.sanitizeReadResponse(message, direction);
if (sanitizedResponse !== message) return sanitizedResponse;
const sanitizedToolMessage = this.sanitizeReadToolMessage(message);
if (sanitizedToolMessage !== message) return sanitizedToolMessage;
this.trackRequestMethod(message, direction);
return message;
}
trackRequestMethod(message, direction) {
const candidate = message;
if (typeof candidate.method !== "string") return;
const idKey = requestCorrelationKey(candidate.id, direction);
if (!idKey) return;
this.requestMethodById.set(idKey, candidate.method);
}
sanitizeReadResponse(message, direction) {
const candidate = message;
const idKey = requestCorrelationKey(candidate.id, reverseDirection(direction));
if (!idKey) return message;
const hasResult = Object.hasOwn(candidate, "result");
const hasError = Object.hasOwn(candidate, "error");
if (!hasResult && !hasError) return message;
const method = this.requestMethodById.get(idKey);
this.requestMethodById.delete(idKey);
if (!hasResult || !this.suppressReadResponse(method)) return message;
const root = asRecord$1(message);
if (!root) return message;
return {
...root,
result: sanitizeReadResult(candidate.result)
};
}
suppressReadResponse(method) {
return method === "fs/read_text_file" || this.partialHistory && method === void 0;
}
sanitizeReadToolMessage(message) {
const update = this.readToolUpdate(message);
if (!update) return message;
const toolCallId = typeof update.toolCallId === "string" ? update.toolCallId : void 0;
if (!toolCallId) return message;
const current = this.mergeToolState(toolCallId, update);
this.toolStateById.set(toolCallId, current);
const unclassified = this.partialHistory && !current.kind && !current.title;
return isReadLikeTool(current) || unclassified ? sanitizeToolMessage(message) : message;
}
readToolUpdate(message) {
const root = asRecord$1(message);
if (root?.method !== "session/update") return;
const params = asRecord$1(root.params);
const update = asRecord$1(params?.update);
if (!params || !update) return;
const sessionUpdate = update.sessionUpdate;
if (sessionUpdate !== "tool_call" && sessionUpdate !== "tool_call_update") return;
return update;
}
mergeToolState(toolCallId, update) {
const previous = this.toolStateById.get(toolCallId) ?? {};
return {
title: typeof update.title === "string" ? update.title : previous.title,
kind: typeof update.kind === "string" || update.kind === null ? update.kind : previous.kind
};
}
};
var JsonOutputFormatter = class {
stdout;
sanitizer;
sessionId;
constructor(stdout, suppressReads, context) {
this.stdout = stdout;
this.sanitizer = new JsonMessageSanitizer(suppressReads);
this.sessionId = context?.sessionId?.trim() || DEFAULT_JSON_SESSION_ID;
}
setContext(context) {
this.sessionId = context.sessionId?.trim() || this.sessionId || DEFAULT_JSON_SESSION_ID;
}
onAcpMessage(message, direction) {
this.stdout.write(`${JSON.stringify(this.sanitizer.sanitize(message, direction))}\n`);
}
onError(params) {
this.stdout.write(`${JSON.stringify(buildJsonRpcErrorResponse({
outputCode: params.code,
detailCode: params.detailCode,
origin: params.origin,
message: params.message,
retryable: params.retryable,
timestamp: params.timestamp,
sessionId: this.sessionId,
acp: params.acp
}))}\n`);
}
onPermissionEscalation() {}
flush() {}
};
function createJsonOutputFormatter(stdout, suppressReads = false, context) {
return new JsonOutputFormatter(stdout, suppressReads, context);
}
// src/cli/output/output.ts
var output_exports = /* @__PURE__ */ __exportAll({
createOutputFormatter: () => createOutputFormatter,
getTextErrorRemediationHints: () => getTextErrorRemediationHints
});
const MAX_THOUGHT_CHARS = 900;
const MAX_INLINE_CHARS = 220;
const MAX_OUTPUT_CHARS = 2e3;
const MAX_OUTPUT_LINES = 28;
const MAX_LOCATION_ITEMS = 5;
const OUTPUT_PRIORITY_KEYS = [
"stdout",
"stderr",
"output",
"content",
"text",
"message",
"result",
"response",
"value"
];
function asStatus(status) {
return status ?? "unknown";
}
function isFinalStatus(status) {
return status === "completed" || status === "failed";
}
function toStatusLabel(status) {
return isFinalStatus(status) || status === "pending" ? status : "running";
}
function asRecord(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) return;
return value;
}
function parseJsonRpcErrorSummary(message) {
const fallback = parseJsonRpcErrorMessage(message);
if (!fallback) return;
return preferredAcpErrorDetails(asRecord(message.error)?.data) ?? fallback;
}
function extractJsonRpcMethod(message) {
return Object.hasOwn(message, "method") ? message.method?.toString() : void 0;
}
function collapseWhitespace(value) {
return value.replace(/\s+/g, " ").trim();
}
function normalizeLineEndings(value) {
return value.replace(/\r\n?/g, "\n");
}
function truncate(value, maxChars) {
if (value.length <= maxChars) return value;
if (maxChars <= 3) return value.slice(0, maxChars);
return `${value.slice(0, maxChars - 3)}...`;
}
function toInline(value, maxChars = MAX_INLINE_CHARS) {
return truncate(collapseWhitespace(value), maxChars);
}
function indentBlock(value, prefix) {
return value.split("\n").map((line) => `${prefix}${line}`).join("\n");
}
function dedupeStrings(values) {
return [...new Set(values)];
}
function safeJson(value, spacing) {
const seen = /* @__PURE__ */ new WeakSet();
try {
return JSON.stringify(value, (_key, entry) => {
if (typeof entry === "bigint") return `${entry}n`;
if (typeof entry === "function") return `[Function ${entry.name || "anonymous"}]`;
if (typeof entry === "symbol") return entry.toString();
if (entry && typeof entry === "object") {
if (seen.has(entry)) return "[Circular]";
seen.add(entry);
}
return entry;
}, spacing);
} catch {
return;
}
}
function readFirstString(source, keys) {
for (const key of keys) {
const value = source[key];
if (typeof value === "string" && value.trim()) return value.trim();
}
}
function readFirstFiniteNumber(source, keys) {
for (const key of keys) {
const value = source[key];
if (typeof value === "number" && Number.isFinite(value)) return value;
}
}
function formatMetadataNumber(value) {
return Number.isInteger(value) ? String(value) : String(Number(value.toFixed(8)));
}
function readFirstStringArray(source, keys) {
for (const key of keys) {
const value = source[key];
if (!Array.isArray(value)) continue;
const entries = value.map((entry) => typeof entry === "string" ? entry.trim() : "").filter((entry) => entry.length > 0);
if (entries.length > 0) return entries;
}
}
function formatDisjunction(values) {
if (values.length <= 1) return values[0] ?? "";
if (values.length === 2) return `${values[0]} or ${values[1]}`;
return `${values.slice(0, -1).join(", ")}, or ${values.at(-1)}`;
}
function parseAuthMethodIdsFromMessage(message) {
const methods = [];
const methodListMatch = message.match(/auth methods \[([^\]]+)\]/iu);
if (methodListMatch) methods.push(...methodListMatch[1].split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0));
const singleMethodMatch = message.match(/auth method ([\w.-]+)/iu);
if (singleMethodMatch) methods.push(singleMethodMatch[1]);
return dedupeStrings(methods);
}
function parseAuthMethodIdsFromAcpData(data) {
const record = asRecord(data);
if (!record) return [];
const methodIds = [];
if (typeof record.methodId === "string" && record.methodId.trim().length > 0) methodIds.push(record.methodId.trim());
if (Array.isArray(record.methods)) for (const entry of record.methods) {
const id = parseAuthMethodIdEntry(entry);
if (id) methodIds.push(id);
}
return dedupeStrings(methodIds);
}
function parseAuthMethodIdEntry(entry) {
if (typeof entry === "string" && entry.trim().length > 0) return entry.trim();
const id = asRecord(entry)?.id;
return typeof id === "string" && id.trim().length > 0 ? id.trim() : void 0;
}
function renderAuthRequiredHint(params) {
const methodIds = dedupeStrings([...parseAuthMethodIdsFromAcpData(params.acp?.data), ...parseAuthMethodIdsFromMessage(params.message)]);
if (methodIds.length === 0) return "hint: run `acpx config show` to locate the active config, then add the required credential under `auth` and retry.";
return `hint: run \`acpx config show\` to locate the active config, then add ${formatDisjunction(methodIds.map((methodId) => `\`auth.${methodId}\``))} and retry.`;
}
function getTextErrorRemediationHints(params) {
const lowerMessage = (preferredAcpErrorDetails(params.acp?.data) ?? params.message).toLowerCase();
if (params.detailCode === "AUTH_REQUIRED") return [renderAuthRequiredHint(params)];
if (params.code === "TIMEOUT") return ["hint: increase `--timeout <seconds>` for long-running prompts, or check whether the agent/provider is stalled."];
if (params.code === "NO_SESSION") return noSessionHints(lowerMessage);
return matchingTextErrorRule(params, lowerMessage)?.hints ?? [];
}
const TEXT_ERROR_HINT_RULES = [
{
matches: (_params, lowerMessage) => isUnsupportedSessionLoadError(lowerMessage),
hints: ["hint: this adapter cannot resume saved ACP sessions; create a fresh one with `acpx <agent> sessions new` instead of reusing `--resume-session`."]
},
{
matches: (_params, lowerMessage) => isSessionLoadError(lowerMessage),
hints: ["hint: rerun with `--verbose` to capture the ACP load failure details.", "hint: if you do not need the old backend session, start a fresh one with `acpx <agent> sessions new` and retry."]
},
{
matches: (_params, lowerMessage) => isRateLimitError(lowerMessage),
hints: ["hint: the provider appears rate-limited; retry later, switch model, or check provider quota/billing."]
},
{
matches: (_params, lowerMessage) => isModelLookupError(lowerMessage),
hints: ["hint: check the configured model name for this agent, then retry with `--model <model>` or `sessions set-model <model>`."]
},
{
matches: (_params, lowerMessage) => isSessionConfigMethodError(lowerMessage),
hints: ["hint: rerun with `--verbose` to capture the ACP method/error details before retrying."]
},
{
matches: isRuntimeAcpProtocolError,
hints: ["hint: rerun with `--verbose` to capture the underlying ACP error details."]
}
];
function matchingTextErrorRule(params, lowerMessage) {
return TEXT_ERROR_HINT_RULES.find((rule) => rule.matches(params, lowerMessage));
}
function noSessionHints(lowerMessage) {
if (lowerMessage.includes("create one:")) return [];
return ["hint: the saved ACP session is missing or stale; start a fresh session with `acpx <agent> sessions new`, then retry."];
}
function isUnsupportedSessionLoadError(lowerMessage) {
return lowerMessage.includes("does not support session/resume") || lowerMessage.includes("does not support session/load");
}
function isSessionLoadError(lowerMessage) {
return lowerMessage.includes("failed to resume acp session") || lowerMessage.includes("session/resume") || lowerMessage.includes("session/load");
}
function isRateLimitError(lowerMessage) {
return /\b429\b/u.test(lowerMessage) || [
"rate limit",
"quota exceeded",
"usage limit"
].some((text) => lowerMessage.includes(text));
}
function isModelLookupError(lowerMessage) {
return [
"model not found",
"unknown model",
"invalid model"
].some((text) => lowerMessage.includes(text));
}
function isSessionConfigMethodError(lowerMessage) {
return [
"session/set_mode",
"session/set_model",
"session/set_config_option"
].some((text) => lowerMessage.includes(text));
}
function isRuntimeAcpProtocolError(params, lowerMessage) {
return params.origin === "acp" && params.code === "RUNTIME" && (params.acp?.code === -32602 || params.acp?.code === -32603 || lowerMessage.includes("internal error"));
}
function summarizeToolInput(rawInput) {
if (rawInput == null) return;
if (isScalarToolInput(rawInput)) return toInline(String(rawInput));
const record = asRecord(rawInput);
if (record) return summarizeToolInputRecord(record) ?? summarizeToolInputJson(rawInput);
return summarizeToolInputJson(rawInput);
}
function isScalarToolInput(value) {
return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
}
function summarizeToolInputRecord(record) {
const command = readFirstString(record, [
"command",
"cmd",
"program"
]);
const args = readFirstStringArray(record, ["args", "arguments"]);
if (command) return toInline([command, ...args ?? []].join(" "));
const location = readFirstString(record, [
"path",
"file",
"filePath",
"filepath",
"target",
"uri",
"url"
]);
const query = readFirstString(record, [
"query",
"pattern",
"text",
"search"
]);
return location || query ? toInline(location ?? query ?? "") : void 0;
}
function summarizeToolInputJson(rawInput) {
const json = safeJson(rawInput, 0);
return json ? toInline(json) : void 0;
}
function malformedToolUpdateMessage(update) {
if (update.locations != null && !validToolLocations(update.locations)) return "Malformed tool call locations";
return malformedToolContentMessage(update.content);
}
function validToolLocations(locations) {
return Array.isArray(locations) && locations.every(validToolLocation);
}
function validToolLocation(location) {
const record = asRecord(location);
if (!record) return false;
if (record.path != null && typeof record.path !== "string") return false;
return record.line == null || typeof record.line === "number" && Number.isFinite(record.line);
}
function malformedToolContentMessage(content) {
if (content == null) return;
if (!Array.isArray(content)) return "Malformed tool call content";
for (const entry of content) {
const error = malformedToolContentEntry(entry);
if (error) return error;
}
}
function malformedToolContentEntry(entry) {
const record = asRecord(entry);
if (!record) return "Malformed tool call content";
if (record.type === "diff") return malformedDiffMessage(record);
if (record.type === "content" && !validToolContentBlock(record.content)) return "Malformed tool call content";
if (record.type === "terminal" && typeof record.terminalId !== "string") return "Malformed tool call content";
}
function malformedDiffMessage(record) {
if (typeof record.path === "string" && typeof record.newText === "string" && (record.oldText == null || typeof record.oldText === "string")) return;
return "Malformed tool call diff";
}
function validToolContentBlock(content) {
const block = asRecord(content);
if (!block || typeof block.type !== "string") return false;
if (block.type === "text") return typeof block.text === "string";
if (block.type === "resource") return asRecord(block.resource) !== void 0;
return true;
}
function formatLocations(locations) {
if (!Array.isArray(locations) || locations.length === 0) return;
const unique = /* @__PURE__ */ new Set();
for (const location of locations) {
const formatted = formatLocation(location);
if (formatted) unique.add(formatted);
}
const items = [...unique];
if (items.length === 0) return;
const visible = items.slice(0, MAX_LOCATION_ITEMS);
const hidden = items.length - visible.length;
if (hidden <= 0) return visible.join(", ");
return `${visible.join(", ")}, +${hidden} more`;
}
function formatLocation(location) {
if (!location || typeof location !== "object") return;
const rawPath = location.path;
if (typeof rawPath !== "string") return;
const path = rawPath.trim();
if (!path) return;
return `${path}${typeof location.line === "number" && Number.isFinite(location.line) ? `:${Math.max(1, Math.trunc(location.line))}` : ""}`;
}
function summarizeDiff(path, oldText, newText) {
const oldLines = oldText ? oldText.split("\n").length : 0;
const delta = newText.split("\n").length - oldLines;
if (delta === 0) return `diff ${path} (line count unchanged)`;
return `diff ${path} (${`${delta > 0 ? "+" : ""}${delta}`} lines)`;
}
function textFromContentBlock(content) {
switch (content.type) {
case "text": return content.text;
case "resource_link": return content.title ?? content.name ?? content.uri;
case "resource": return textFromResourceBlock(content);
case "image": return `[image] ${content.mimeType}`;
case "audio": return `[audio] ${content.mimeType}`;
default: return;
}
}
function textFromResourceBlock(content) {
if (!content.resource || typeof content.resource !== "object") return "[resource]";
if ("text" in content.resource && typeof content.resource.text === "string") return content.resource.text;
const uri = content.resource.uri;
const mimeType = content.resource.mimeType;
return `[resource] ${uri}${mimeType ? ` (${mimeType})` : ""}`;
}
function summarizeToolContent(content) {
if (!Array.isArray(content) || content.length === 0) return;
const fragments = [];
for (const entry of content) {
const fragment = summarizeToolContentEntry(entry);
if (fragment) fragments.push(fragment);
}
const unique = dedupeStrings(fragments.map((fragment) => fragment.trim()).filter((fragment) => fragment.length > 0));
if (unique.length === 0) return;
return unique.join("\n\n");
}
function summarizeToolContentEntry(entry) {
if (!entry || typeof entry !== "object") return;
if (entry.type === "content") return summarizeContentEntry(entry);
if (entry.type === "diff") return summarizeDiffEntry(entry);
if (entry.type === "terminal") return `[terminal] ${entry.terminalId}`;
}
function summarizeContentEntry(entry) {
if (!entry.content || typeof entry.content !== "object") return;
const text = textFromContentBlock(entry.content);
return typeof text === "string" && text.trim() ? text.trimEnd() : void 0;
}
function summarizeDiffEntry(entry) {
if (typeof entry.path !== "string" || typeof entry.newText !== "string") return;
const oldText = typeof entry.oldText === "string" ? entry.oldText : void 0;
return summarizeDiff(entry.path, oldText, entry.newText);
}
function extractOutputText(value, depth = 0, seen = /* @__PURE__ */ new Set()) {
if (value == null) return;
const scalar = extractScalarOutputText(value);
if (scalar !== null) return scalar;
if (depth >= 4) return;
if (Array.isArray(value)) return extractOutputTextArray(value, depth, seen);
return extractOutputTextRecord(value, depth, seen);
}
function extractOutputTextRecord(value, depth, seen) {
const record = asRecord(value);
if (!record || seen.has(record)) return;
seen.add(record);
return extractPreferredOutputText(record, depth, seen) ?? extractJsonOutputText(record);
}
function extractScalarOutputText(value) {
if (typeof value === "string") {
const trimmed = value.trimEnd();
return trimmed.length > 0 ? trimmed : void 0;
}
if (typeof value === "number" || typeof value === "boolean") return String(value);
return null;
}
function extractOutputTextArray(value, depth, seen) {
const parts = value.map((entry) => extractOutputText(entry, depth + 1, seen)).filter((entry) => Boolean(entry));
return parts.length > 0 ? dedupeStrings(parts).join("\n") : void 0;
}
function extractPreferredOutputText(record, depth, seen) {
const uniquePreferred = dedupeStrings(OUTPUT_PRIORITY_KEYS.flatMap((key) => {
if (!(key in record)) return [];
const extracted = extractOutputText(record[key], depth + 1, seen);
return extracted ? [extracted] : [];
}));
return uniquePreferred.length > 0 ? uniquePreferred.join("\n") : void 0;
}
function extractJsonOutputText(record) {
const json = safeJson(record, 2);
return !json || json === "{}" ? void 0 : json;
}
function summarizeToolOutput(rawOutput, content) {
const fragments = dedupeStrings([extractOutputText(rawOutput), summarizeToolContent(content)].map((fragment) => fragment?.trim()).filter((fragment) => Boolean(fragment)));
if (fragments.length === 0) return;
return fragments.join("\n\n");
}
function renderToolOutput(state, suppressReads) {
if (suppressReads && isReadLikeTool(state)) return SUPPRESSED_READ_OUTPUT;
return summarizeToolOutput(state.rawOutput, state.content);
}
function limitOutputBlock(value) {
const normalized = value.replace(/\r\n/g, "\n").trim();
if (!normalized) return "";
const lines = normalized.split("\n");
const visible = lines.slice(0, MAX_OUTPUT_LINES);
let result = visible.join("\n");
if (lines.length > visible.length) {
const hidden = lines.length - visible.length;
result += `\n... (${hidden} more lines)`;
}
if (result.length > MAX_OUTPUT_CHARS) result = `${result.slice(0, 1997)}...`;
return result;
}
var TextOutputFormatter = class {
stdout;
useColor;
suppressReads;
toolStates = /* @__PURE__ */ new Map();
thoughtBuffer = "";
wroteAny = false;
atLineStart = true;
constructor(stdout, suppressReads) {
this.stdout = stdout;
this.useColor = Boolean(stdout.isTTY);
this.suppressReads = suppressReads;
}
setContext(_context) {}
onAcpMessage(message) {
const notice = parsePermissionNotice(message);
if (notice) {
this.beginSection();
this.writeLine(`${this.bold("[permission]")} ${notice}`);
return;
}
const notification = extractSessionUpdateNotification(message);
if (notification) {
this.renderSessionUpdate(notification);
return;
}
const method = extractJsonRpcMethod(message);
if (method && ![
"session/prompt",
"session/cancel",
"session/update"
].includes(method)) {
this.flushThoughtBuffer();
this.beginSection();
this.writeLine(`${this.bold("[client]")} ${method} (${this.colorStatus("running", "in_progress")})`);
return;
}
const stopReason = parsePromptStopReason(message);
if (stopReason) {
this.renderDone(stopReason);
return;
}
const errorMessage = parseJsonRpcErrorSummary(message);
if (errorMessage) this.onError({
code: "RUNTIME",
origin: "acp",
message: errorMessage
});
}
renderSessionUpdate(notification) {
const update = notification.update;
if (update.sessionUpdate !== "agent_thought_chunk") this.flushThoughtBuffer();
this.renderSessionUpdateBody(update);
}
renderSessionUpdateBody(update) {
switch (update.sessionUpdate) {
case "agent_message_chunk":
if (update.content.type === "text") this.write(update.content.text);
return;
case "agent_thought_chunk":
if (update.content.type === "text") this.thoughtBuffer += update.content.text;
return;
case "tool_call":
this.renderToolUpdate(update);
return;
case "tool_call_update":
this.renderToolUpdate(update);
return;
case "plan":
this.renderPlanUpdate(update.entries);
return;
default: return;
}
}
renderPlanUpdate(entries) {
this.beginSection();
this.writeLine(this.bold("[plan]"));
for (const entry of entries) this.writeLine(` - [${entry.status}] ${entry.content}`);
}
renderDone(stopReason) {
this.flushThoughtBuffer();
this.beginSection();
this.writeLine(this.dim(`[done] ${stopReason}`));
}
onError(params) {
this.flushThoughtBuffer();
this.beginSection();
const message = preferredAcpErrorDetails(params.acp?.data) ?? params.message;
this.writeLine(this.formatAnsi(`[error] ${params.code}: ${message}`, "31"));
for (const hint of getTextErrorRemediationHints(params)) this.writeLine(this.dim(hint));
}
onPermissionEscalation(event) {
this.flushThoughtBuffer();
this.beginSection();
this.writeLine(`${this.bold("[permission]")} ${event.message}`);
const details = [
`sessionId: ${event.sessionId}`,
`toolCallId: ${event.toolCallId}`,
event.toolName ? `toolName: ${event.toolName}` : void 0,
`toolTitle: ${event.toolTitle}`,
event.toolInput !== void 0 ? `toolInput: ${summarizeToolInput(event.toolInput) ?? "(structured input)"}` : void 0,
event.toolKind ? `toolKind: ${event.toolKind}` : void 0,
event.matchedRule ? `matchedRule: ${event.matchedRule}` : void 0
].filter((line) => Boolean(line));
this.writeLine(indentBlock(details.join("\n"), " "));
}
flush() {
this.flushThoughtBuffer();
if (!this.atLineStart) this.write("\n");
}
write(chunk) {
if (!chunk) return;
this.stdout.write(chunk);
this.wroteAny = true;
this.atLineStart = chunk.endsWith("\n");
}
writeLine(line) {
this.write(`${line}\n`);
}
beginSection() {
if (!this.atLineStart) this.write("\n");
if (this.wroteAny) this.write("\n");
}
flushThoughtBuffer() {
const thought = truncate(normalizeLineEndings(this.thoughtBuffer).trim(), MAX_THOUGHT_CHARS);
this.thoughtBuffer = "";
if (!thought) return;
this.beginSection();
const [firstLine, ...restLines] = thought.split("\n");
this.writeLine(this.dim(`[thinking] ${firstLine}`));
for (const line of restLines) this.writeLine(this.dim(` ${line}`));
}
renderToolUpdate(update) {
const shapeError = malformedToolUpdateMessage(update);
if (shapeError) {
this.onError({
code: "RUNTIME",
origin: "acp",
message: shapeError
});
return;
}
const state = this.getOrCreateToolState(update.toolCallId);
this.mergeToolState(state, update);
const status = asStatus(state.status);
const final = isFinalStatus(status);
if (!final && state.startedPrinted) return;
const display = {
input: summarizeToolInput(state.rawInput),
files: formatLocations(state.locations),
output: final ? renderToolOutput(state, this.suppressReads) : void 0
};
if (final) {
const payload = {
title: state.title,
status: state.status,
kind: state.kind,
...display
};
const signature = safeJson(payload, 0) ?? JSON.stringify(payload);
if (signature === state.finalSignature) return;
state.finalSignature = signature;
} else state.startedPrinted = true;
this.renderToolState(state, status, display);
}
getOrCreateToolState(toolCallId) {
const existing = this.toolStates.get(toolCallId);
if (existing) return existing;
const created = {
id: toolCallId,
startedPrinted: false
};
this.toolStates.set(toolCallId, created);
return created;
}
mergeToolState(state, update) {
this.mergeToolTitle(state, update.title);
this.mergeToolPayloadState(state, update);
}
mergeToolTitle(state, title) {
if (typeof title === "string" && title.trim().length > 0) state.title = title;
}
mergeToolPayloadState(state, update) {
if (update.status !== void 0) state.status = update.status;
if (update.kind !== void 0) state.kind = update.kind;
if (update.locations !== void 0) state.locations = update.locations;
if (update.rawInput !== void 0) state.rawInput = update.rawInput;
if (update.rawOutput !== void 0) state.rawOutput = update.rawOutput;
if (update.content !== void 0) state.content = update.content;
}
renderToolState(state, status, display) {
this.beginSection();
const title = state.title ?? state.id;
const statusText = this.colorStatus(toStatusLabel(status), status);
this.writeLine(`${this.bold("[tool]")} ${title} (${statusText})`);
if (isFinalStatus(status) && state.kind) this.writeLine(` kind: ${state.kind}`);
if (display.input) this.writeLine(` input: ${display.input}`);
if (display.files) this.writeLine(` files: ${display.files}`);
if (display.output) {
this.writeLine(" output:");
this.writeLine(indentBlock(limitOutputBlock(display.output), " "));
}
}
formatAnsi(text, code) {
if (!this.useColor) return text;
return `\u001b[${code}m${text}\u001b[0m`;
}
bold(text) {
return this.formatAnsi(text, "1");
}
dim(text) {
return this.formatAnsi(text, "2");
}
colorStatus(text, status) {
if (!this.useColor) return text;
switch (status) {
case "completed": return this.formatAnsi(text, "32");
case "failed": return this.formatAnsi(text, "31");
default: return this.formatAnsi(text, "33");
}
}
};
var QuietOutputFormatter = class {
stdout;
stderr;
chunks = [];
finished = false;
outputWritten = false;
metadataFlushed = false;
constructor(stdout, stderr) {
this.stdout = stdout;
this.stderr = stderr;
}
setContext(_context) {}
onAcpMessage(message) {
const notice = parsePermissionNotice(message);
if (notice) {
this.stderr.write(`[acpx] permission: ${notice.replace(/\r\n?|\n/g, " ")}\n`);
return;
}
const update = extractSessionUpdateNotification(message);
if (update?.update.sessionUpdate === "agent_message_chunk" && update.update.content.type === "text") {
if (!this.finished) this.chunks.push(update.update.content.text);
return;
}
if (parsePromptStopReason(message)) {
if (!this.finished) {
this.finished = true;
this.flushBufferedOutput(true);
}
this.flushMetadata(message);
}
}
onError(params) {
this.flush();
const qualifier = params.detailCode ? `${params.code} ${params.detailCode}` : params.code;
const message = preferredAcpErrorDetails(params.acp?.data) ?? params.message;
this.stderr.write(`[acpx] error: ${qualifier} ${message.replace(/\r\n?|\n/g, " ")}\n`);
}
onPermissionEscalation(_event) {}
flush() {
this.flushBufferedOutput();
}
flushBufferedOutput(allowEmpty = false) {
const text = this.chunks.join("");
this.chunks = [];
if (text.length === 0 && (!allowEmpty || this.outputWritten)) return;
this.outputWritten = true;
this.stdout.write(text.endsWith("\n") ? text : `${text}\n`);
}
flushMetadata(message) {
if (this.metadataFlushed) return;
this.metadataFlushed = true;
const result = asRecord(message.result);
if (!result) return;
const usageLine = this.formatUsageLine(asRecord(result.usage));
if (usageLine) this.stderr.write(`${usageLine}\n`);
const costLine = this.formatCostLine(result.cost);
if (costLine) this.stderr.write(`${costLine}\n`);
}
formatUsageLine(usage) {
if (!usage) return;
const parts = [];
for (const [label, keys] of [
["input", ["inputTokens", "input_tokens"]],
["output", ["outputTokens", "output_tokens"]],
["cache_read", [
"cachedReadTokens",
"cacheReadInputTokens",
"cache_read_input_tokens"
]],
["cache_write", [
"cachedWriteTokens",
"cacheCreationInputTokens",
"cache_creation_input_tokens"
]],
["total", ["totalTokens", "total_tokens"]]
]) {
const value = readFirstFiniteNumber(usage, keys);
if (value !== void 0) parts.push(`${label}=${formatMetadataNumber(value)}`);
}
return parts.length > 0 ? `[acpx] tokens: ${parts.join(" ")}` : void 0;
}
formatCostLine(cost) {
const scalar = this.formatScalarCostLine(cost);
if (scalar) return scalar;
const record = asRecord(cost);
if (!record) return;
const amount = readFirstFiniteNumber(record, [
"amount",
"value",
"total"
]);
if (amount === void 0) return;
const currency = typeof record.currency === "string" && record.currency.trim() ? ` ${record.currency.trim()}` : "";
return `[acpx] cost: ${formatMetadataNumber(amount)}${currency}`;
}
formatScalarCostLine(cost) {
if (typeof cost === "number" && Number.isFinite(cost)) return `[acpx] cost: ${formatMetadataNumber(cost)}`;
if (typeof cost === "string" && cost.trim()) return `[acpx] cost: ${cost.trim()}`;
}
};
function preferredAcpErrorDetails(data) {
const record = asRecord(data);
for (const value of [record?.details, record?.message]) if (typeof value === "string" && value.trim().length > 0) return value.trim();
}
function createOutputFormatter(format, options = {}) {
const stdout = options.stdout ?? process.stdout;
const stderr = options.stderr ?? process.stderr;
const suppressReads = options.suppressReads === true;
switch (format) {
case "text": return new TextOutputFormatter(stdout, suppressReads);
case "json": return createJsonOutputFormatter(stdout, suppressReads, options.jsonContext);
case "quiet": return new QuietOutputFormatter(stdout, stderr);
default: throw new Error("Unsupported output format");
}
}
export { MAX_TIMER_DELAY_MS as a, __exportAll as c, JsonMessageSanitizer as i, getTextErrorRemediationHints as n, toTimerMilliseconds as o, output_exports as r, session_exports as s, createOutputFormatter as t };
//# sourceMappingURL=output-C_T39Ap1.js.map