@evantahler/mcpx 0.18.3 → 0.18.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +63 -63
- package/src/cli.ts +46 -54
- package/src/client/browser.ts +15 -15
- package/src/client/debug-fetch.ts +53 -56
- package/src/client/elicitation.ts +279 -291
- package/src/client/http.ts +1 -1
- package/src/client/manager.ts +481 -514
- package/src/client/oauth.ts +272 -282
- package/src/client/sse.ts +1 -1
- package/src/client/stdio.ts +7 -7
- package/src/client/trace.ts +146 -152
- package/src/client/transport-options.ts +20 -20
- package/src/commands/add.ts +160 -165
- package/src/commands/allow.ts +141 -142
- package/src/commands/auth.ts +86 -90
- package/src/commands/check-update.ts +49 -53
- package/src/commands/deny.ts +114 -117
- package/src/commands/exec.ts +218 -222
- package/src/commands/index.ts +41 -41
- package/src/commands/info.ts +48 -50
- package/src/commands/list.ts +49 -49
- package/src/commands/ping.ts +47 -50
- package/src/commands/prompt.ts +40 -50
- package/src/commands/remove.ts +54 -56
- package/src/commands/resource.ts +31 -36
- package/src/commands/search.ts +35 -39
- package/src/commands/servers.ts +44 -48
- package/src/commands/skill.ts +89 -95
- package/src/commands/task.ts +50 -60
- package/src/commands/upgrade.ts +191 -208
- package/src/commands/with-command.ts +27 -29
- package/src/config/env.ts +26 -28
- package/src/config/loader.ts +99 -102
- package/src/config/schemas.ts +78 -87
- package/src/constants.ts +17 -17
- package/src/context.ts +51 -51
- package/src/lib/client-settings.ts +127 -140
- package/src/lib/input.ts +23 -26
- package/src/output/format-output.ts +12 -16
- package/src/output/format-table.ts +39 -42
- package/src/output/formatter.ts +790 -814
- package/src/output/logger.ts +140 -152
- package/src/sdk.ts +283 -291
- package/src/search/index.ts +50 -54
- package/src/search/indexer.ts +65 -65
- package/src/search/keyword.ts +54 -54
- package/src/search/semantic.ts +39 -39
- package/src/search/staleness.ts +3 -3
- package/src/search/types.ts +4 -4
- package/src/update/background.ts +51 -51
- package/src/update/cache.ts +21 -21
- package/src/update/checker.ts +81 -86
- package/src/validation/schema.ts +52 -58
package/src/output/formatter.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import ansis, { bold, cyan, dim, green, red, yellow } from "ansis";
|
|
2
|
-
import type {
|
|
3
|
-
import type {
|
|
4
|
-
import type { ValidationError } from "../validation/schema.ts";
|
|
2
|
+
import type { PromptWithServer, ResourceWithServer, ToolWithServer } from "../client/manager.ts";
|
|
3
|
+
import type { Prompt, Resource, Tool } from "../config/schemas.ts";
|
|
5
4
|
import type { SearchResult } from "../search/index.ts";
|
|
5
|
+
import type { ValidationError } from "../validation/schema.ts";
|
|
6
6
|
import { formatOutput } from "./format-output.ts";
|
|
7
7
|
import { formatTable } from "./format-table.ts";
|
|
8
8
|
|
|
@@ -11,68 +11,68 @@ export const VALID_FORMATS = ["json", "markdown"] as const;
|
|
|
11
11
|
export type OutputFormat = (typeof VALID_FORMATS)[number];
|
|
12
12
|
|
|
13
13
|
export interface FormatOptions {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
json?: boolean;
|
|
15
|
+
withDescriptions?: boolean;
|
|
16
|
+
verbose?: boolean;
|
|
17
|
+
showSecrets?: boolean;
|
|
18
|
+
logLevel?: string;
|
|
19
|
+
format?: OutputFormat;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export interface UnifiedItem {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
server: string;
|
|
24
|
+
type: "tool" | "resource" | "prompt";
|
|
25
|
+
name: string;
|
|
26
|
+
description?: string;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/** Check if stdout is a TTY (interactive terminal) */
|
|
30
30
|
export function isInteractive(options: FormatOptions): boolean {
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
if (options.json) return false;
|
|
32
|
+
return process.stdout.isTTY ?? false;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/** Get terminal width, or undefined if not a TTY. Subtracts 1 for safety margin. */
|
|
36
36
|
function getTerminalWidth(): number | undefined {
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
if (process.stdout.isTTY) return Math.max(process.stdout.columns - 1, 40);
|
|
38
|
+
return undefined;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/** Measure visible length of a string (excluding ANSI escape codes) */
|
|
42
42
|
function visibleLength(s: string): number {
|
|
43
|
-
|
|
43
|
+
return ansis.strip(s).length;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/** Word-wrap text to a max width, hard-breaking words that exceed it */
|
|
47
47
|
function wrapLines(text: string, maxWidth: number): string[] {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
48
|
+
const words = text.split(/\s+/).filter((w) => w.length > 0);
|
|
49
|
+
if (words.length === 0) return [""];
|
|
50
|
+
|
|
51
|
+
const lines: string[] = [];
|
|
52
|
+
let current = "";
|
|
53
|
+
|
|
54
|
+
for (const word of words) {
|
|
55
|
+
if (word.length > maxWidth) {
|
|
56
|
+
if (current) {
|
|
57
|
+
lines.push(current);
|
|
58
|
+
current = "";
|
|
59
|
+
}
|
|
60
|
+
for (let j = 0; j < word.length; j += maxWidth) {
|
|
61
|
+
lines.push(word.slice(j, j + maxWidth));
|
|
62
|
+
}
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (current.length === 0) {
|
|
66
|
+
current = word;
|
|
67
|
+
} else if (current.length + 1 + word.length <= maxWidth) {
|
|
68
|
+
current += ` ${word}`;
|
|
69
|
+
} else {
|
|
70
|
+
lines.push(current);
|
|
71
|
+
current = word;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (current) lines.push(current);
|
|
75
|
+
return lines;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
/**
|
|
@@ -83,350 +83,340 @@ function wrapLines(text: string, maxWidth: number): string[] {
|
|
|
83
83
|
* @param termWidth - terminal width in columns
|
|
84
84
|
*/
|
|
85
85
|
export function wrapDescription(text: string, prefixWidth: number, termWidth: number): string {
|
|
86
|
-
|
|
86
|
+
const available = termWidth - prefixWidth;
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
88
|
+
// If prefix is so wide there's barely room, wrap onto the next line with a small indent
|
|
89
|
+
if (available < 20) {
|
|
90
|
+
const fallbackIndent = Math.min(prefixWidth, 4);
|
|
91
|
+
const fallbackAvail = termWidth - fallbackIndent;
|
|
92
|
+
if (fallbackAvail < 20) {
|
|
93
|
+
return dim(text.length > termWidth ? `${text.slice(0, termWidth - 3)}...` : text);
|
|
94
|
+
}
|
|
95
|
+
const wrapped = wrapLines(text, fallbackAvail);
|
|
96
|
+
const indent = " ".repeat(fallbackIndent);
|
|
97
|
+
return wrapped.map((l) => `\n${indent}${dim(l)}`).join("");
|
|
98
|
+
}
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
const wrapped = wrapLines(text, available);
|
|
101
|
+
const indent = " ".repeat(prefixWidth);
|
|
102
|
+
return wrapped.map((l, i) => (i === 0 ? dim(l) : `\n${indent}${dim(l)}`)).join("");
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
export interface ServerOverview {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
106
|
+
serverName: string;
|
|
107
|
+
version?: { name: string; version: string };
|
|
108
|
+
capabilities?: Record<string, unknown>;
|
|
109
|
+
instructions?: string;
|
|
110
|
+
tools: Tool[];
|
|
111
|
+
resourceCount: number;
|
|
112
|
+
promptCount: number;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
const KNOWN_CAPABILITIES = ["tools", "resources", "prompts", "logging", "completions", "tasks"];
|
|
116
116
|
|
|
117
117
|
/** Format a full server overview (version, capabilities, tools, counts) */
|
|
118
118
|
export function formatServerOverview(overview: ServerOverview, options: FormatOptions): string {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
return lines.join("\n");
|
|
193
|
-
},
|
|
194
|
-
options,
|
|
195
|
-
);
|
|
119
|
+
return formatOutput(
|
|
120
|
+
{
|
|
121
|
+
server: overview.serverName,
|
|
122
|
+
version: overview.version ?? null,
|
|
123
|
+
capabilities: overview.capabilities ?? null,
|
|
124
|
+
instructions: overview.instructions ?? null,
|
|
125
|
+
tools: overview.tools.map((t) => ({ name: t.name, description: t.description ?? "" })),
|
|
126
|
+
resourceCount: overview.resourceCount,
|
|
127
|
+
promptCount: overview.promptCount,
|
|
128
|
+
},
|
|
129
|
+
() => {
|
|
130
|
+
const lines: string[] = [];
|
|
131
|
+
|
|
132
|
+
// Header: server name + version
|
|
133
|
+
const header = cyan.bold(overview.serverName);
|
|
134
|
+
if (overview.version) {
|
|
135
|
+
lines.push(`${header} ${dim(`v${overview.version.version}`)} ${dim(`(${overview.version.name})`)}`);
|
|
136
|
+
} else {
|
|
137
|
+
lines.push(header);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Capabilities
|
|
141
|
+
if (overview.capabilities) {
|
|
142
|
+
lines.push("");
|
|
143
|
+
lines.push(bold("Capabilities:"));
|
|
144
|
+
const caps = overview.capabilities;
|
|
145
|
+
const present = KNOWN_CAPABILITIES.filter((k) => k in caps);
|
|
146
|
+
const absent = KNOWN_CAPABILITIES.filter((k) => !(k in caps));
|
|
147
|
+
for (const k of present) lines.push(` ${green("✓")} ${k}`);
|
|
148
|
+
for (const k of absent) lines.push(` ${dim("✗")} ${dim(k)}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Instructions
|
|
152
|
+
if (overview.instructions) {
|
|
153
|
+
lines.push("");
|
|
154
|
+
lines.push(bold("Instructions:"));
|
|
155
|
+
lines.push(` ${dim(overview.instructions)}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Tools
|
|
159
|
+
lines.push("");
|
|
160
|
+
if (overview.tools.length === 0) {
|
|
161
|
+
lines.push(`${bold("Tools:")} ${dim("none")}`);
|
|
162
|
+
} else {
|
|
163
|
+
lines.push(bold(`Tools (${overview.tools.length}):`));
|
|
164
|
+
const maxName = Math.max(...overview.tools.map((t) => t.name.length));
|
|
165
|
+
const termWidth = getTerminalWidth();
|
|
166
|
+
for (let i = 0; i < overview.tools.length; i++) {
|
|
167
|
+
const t = overview.tools[i]!;
|
|
168
|
+
if (i > 0) lines.push("");
|
|
169
|
+
const name = ` ${bold(t.name.padEnd(maxName))}`;
|
|
170
|
+
if (t.description) {
|
|
171
|
+
const pw = visibleLength(name) + 2;
|
|
172
|
+
const desc = termWidth != null ? wrapDescription(t.description, pw, termWidth) : dim(t.description);
|
|
173
|
+
lines.push(`${name} ${desc}`);
|
|
174
|
+
} else {
|
|
175
|
+
lines.push(name);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Resource/prompt counts
|
|
181
|
+
const counts: string[] = [];
|
|
182
|
+
counts.push(`Resources: ${overview.resourceCount}`);
|
|
183
|
+
counts.push(`Prompts: ${overview.promptCount}`);
|
|
184
|
+
lines.push("");
|
|
185
|
+
lines.push(dim(counts.join(" | ")));
|
|
186
|
+
|
|
187
|
+
return lines.join("\n");
|
|
188
|
+
},
|
|
189
|
+
options,
|
|
190
|
+
);
|
|
196
191
|
}
|
|
197
192
|
|
|
198
193
|
/** Format a list of tools with server names */
|
|
199
194
|
export function formatToolList(tools: ToolWithServer[], options: FormatOptions): string {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
195
|
+
return formatOutput(
|
|
196
|
+
tools.map((t) => ({
|
|
197
|
+
server: t.server,
|
|
198
|
+
tool: t.tool.name,
|
|
199
|
+
...(options.withDescriptions ? { description: t.tool.description ?? "" } : {}),
|
|
200
|
+
})),
|
|
201
|
+
() =>
|
|
202
|
+
formatTable(tools, {
|
|
203
|
+
columns: [
|
|
204
|
+
{ value: (t) => t.server, style: cyan },
|
|
205
|
+
{ value: (t) => t.tool.name, style: bold },
|
|
206
|
+
],
|
|
207
|
+
description: options.withDescriptions ? (t) => t.tool.description : undefined,
|
|
208
|
+
emptyMessage: "No tools found",
|
|
209
|
+
}),
|
|
210
|
+
options,
|
|
211
|
+
);
|
|
217
212
|
}
|
|
218
213
|
|
|
219
214
|
/** Format tools for a single server */
|
|
220
|
-
export function formatServerTools(
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
):
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
return `${header}\n${body}`;
|
|
240
|
-
},
|
|
241
|
-
options,
|
|
242
|
-
);
|
|
215
|
+
export function formatServerTools(serverName: string, tools: Tool[], options: FormatOptions): string {
|
|
216
|
+
return formatOutput(
|
|
217
|
+
{
|
|
218
|
+
server: serverName,
|
|
219
|
+
tools: tools.map((t) => ({ name: t.name, description: t.description ?? "" })),
|
|
220
|
+
},
|
|
221
|
+
() => {
|
|
222
|
+
if (tools.length === 0) {
|
|
223
|
+
return dim(`No tools found for ${serverName}`);
|
|
224
|
+
}
|
|
225
|
+
const header = cyan.bold(serverName);
|
|
226
|
+
const body = formatTable(tools, {
|
|
227
|
+
columns: [{ value: (t) => ` ${t.name}`, style: bold }],
|
|
228
|
+
description: (t) => t.description,
|
|
229
|
+
});
|
|
230
|
+
return `${header}\n${body}`;
|
|
231
|
+
},
|
|
232
|
+
options,
|
|
233
|
+
);
|
|
243
234
|
}
|
|
244
235
|
|
|
245
236
|
/** Format a tool schema */
|
|
246
237
|
export function formatToolSchema(serverName: string, tool: Tool, options: FormatOptions): string {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
238
|
+
return formatOutput(
|
|
239
|
+
{
|
|
240
|
+
server: serverName,
|
|
241
|
+
tool: tool.name,
|
|
242
|
+
description: tool.description ?? "",
|
|
243
|
+
inputSchema: tool.inputSchema,
|
|
244
|
+
},
|
|
245
|
+
() => {
|
|
246
|
+
const lines: string[] = [];
|
|
247
|
+
lines.push(`${cyan(serverName)}/${bold(tool.name)}`);
|
|
248
|
+
if (tool.description) lines.push(dim(tool.description));
|
|
249
|
+
lines.push("");
|
|
250
|
+
lines.push(bold("Input Schema:"));
|
|
251
|
+
lines.push(formatSchema(tool.inputSchema, 2));
|
|
252
|
+
return lines.join("\n");
|
|
253
|
+
},
|
|
254
|
+
options,
|
|
255
|
+
);
|
|
265
256
|
}
|
|
266
257
|
|
|
267
258
|
/** Format a JSON schema as a readable parameter list */
|
|
268
259
|
function formatSchema(schema: Tool["inputSchema"], indent: number): string {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
260
|
+
const pad = " ".repeat(indent);
|
|
261
|
+
const properties = schema.properties ?? {};
|
|
262
|
+
const required = new Set(schema.required ?? []);
|
|
272
263
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
264
|
+
if (Object.keys(properties).length === 0) {
|
|
265
|
+
return `${pad}${dim("(no parameters)")}`;
|
|
266
|
+
}
|
|
276
267
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
268
|
+
return Object.entries(properties)
|
|
269
|
+
.map(([name, prop]) => {
|
|
270
|
+
const p = prop as Record<string, unknown>;
|
|
271
|
+
const type = (p.type as string) ?? "any";
|
|
272
|
+
const req = required.has(name) ? red("*") : "";
|
|
273
|
+
const desc = p.description ? ` ${dim(String(p.description))}` : "";
|
|
274
|
+
return `${pad}${green(name)}${req} ${dim(`(${type})`)}${desc}`;
|
|
275
|
+
})
|
|
276
|
+
.join("\n");
|
|
286
277
|
}
|
|
287
278
|
|
|
288
279
|
/** Format detailed tool help with example payload */
|
|
289
280
|
export function formatToolHelp(serverName: string, tool: Tool, options: FormatOptions): string {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
281
|
+
return formatOutput(
|
|
282
|
+
{
|
|
283
|
+
server: serverName,
|
|
284
|
+
tool: tool.name,
|
|
285
|
+
description: tool.description ?? "",
|
|
286
|
+
inputSchema: tool.inputSchema,
|
|
287
|
+
example: generateExample(tool.inputSchema),
|
|
288
|
+
},
|
|
289
|
+
() => {
|
|
290
|
+
const lines: string[] = [];
|
|
291
|
+
lines.push(`${cyan(serverName)}/${bold(tool.name)}`);
|
|
292
|
+
if (tool.description) lines.push(dim(tool.description));
|
|
293
|
+
lines.push("");
|
|
294
|
+
lines.push(bold("Parameters:"));
|
|
295
|
+
lines.push(formatSchema(tool.inputSchema, 2));
|
|
296
|
+
const example = generateExample(tool.inputSchema);
|
|
297
|
+
lines.push("");
|
|
298
|
+
lines.push(bold("Example:"));
|
|
299
|
+
lines.push(dim(` mcpx call ${serverName} ${tool.name} '${JSON.stringify(example)}'`));
|
|
300
|
+
return lines.join("\n");
|
|
301
|
+
},
|
|
302
|
+
options,
|
|
303
|
+
);
|
|
313
304
|
}
|
|
314
305
|
|
|
315
306
|
/** Generate an example payload from a JSON schema */
|
|
316
307
|
function generateExample(schema: Tool["inputSchema"]): Record<string, unknown> {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
308
|
+
const properties = schema.properties ?? {};
|
|
309
|
+
const required = new Set(schema.required ?? []);
|
|
310
|
+
const example: Record<string, unknown> = {};
|
|
320
311
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
312
|
+
for (const [name, prop] of Object.entries(properties)) {
|
|
313
|
+
const p = prop as Record<string, unknown>;
|
|
314
|
+
if (required.has(name) || Object.keys(example).length < 3) {
|
|
315
|
+
example[name] = exampleValue(name, p);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
327
318
|
|
|
328
|
-
|
|
319
|
+
return example;
|
|
329
320
|
}
|
|
330
321
|
|
|
331
322
|
function exampleValue(name: string, prop: Record<string, unknown>): unknown {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
323
|
+
if (Array.isArray(prop.enum) && prop.enum.length > 0) return prop.enum[0];
|
|
324
|
+
if (prop.default !== undefined) return prop.default;
|
|
325
|
+
|
|
326
|
+
const type = prop.type as string | undefined;
|
|
327
|
+
switch (type) {
|
|
328
|
+
case "string":
|
|
329
|
+
return `<${name}>`;
|
|
330
|
+
case "number":
|
|
331
|
+
case "integer":
|
|
332
|
+
return 0;
|
|
333
|
+
case "boolean":
|
|
334
|
+
return true;
|
|
335
|
+
case "array":
|
|
336
|
+
return [];
|
|
337
|
+
case "object":
|
|
338
|
+
return {};
|
|
339
|
+
default:
|
|
340
|
+
return `<${name}>`;
|
|
341
|
+
}
|
|
351
342
|
}
|
|
352
343
|
|
|
353
344
|
/** Format a tool call result, dispatching on the --format option */
|
|
354
345
|
export function formatCallResult(result: unknown, options: FormatOptions): string {
|
|
355
|
-
|
|
346
|
+
const format = options.format ?? "json";
|
|
356
347
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
}
|
|
348
|
+
switch (format) {
|
|
349
|
+
case "markdown":
|
|
350
|
+
return formatCallResultAsMarkdown(result);
|
|
351
|
+
default:
|
|
352
|
+
return JSON.stringify(parseNestedJson(result), null, 2);
|
|
353
|
+
}
|
|
364
354
|
}
|
|
365
355
|
|
|
366
356
|
/** Render an MCP tool call result as styled markdown for terminal output */
|
|
367
357
|
function formatCallResultAsMarkdown(result: unknown): string {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
358
|
+
const r = result as {
|
|
359
|
+
content?: Array<{
|
|
360
|
+
type: string;
|
|
361
|
+
text?: string;
|
|
362
|
+
data?: string;
|
|
363
|
+
mimeType?: string;
|
|
364
|
+
uri?: string;
|
|
365
|
+
}>;
|
|
366
|
+
isError?: boolean;
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
if (!r.content || !Array.isArray(r.content) || r.content.length === 0) {
|
|
370
|
+
return renderMarkdownToAnsi(jsonToMarkdown(result));
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const parts: string[] = [];
|
|
374
|
+
|
|
375
|
+
for (const block of r.content) {
|
|
376
|
+
switch (block.type) {
|
|
377
|
+
case "text":
|
|
378
|
+
if (block.text !== undefined) {
|
|
379
|
+
try {
|
|
380
|
+
const parsed = JSON.parse(block.text);
|
|
381
|
+
parts.push(jsonToMarkdown(parsed));
|
|
382
|
+
} catch {
|
|
383
|
+
// Plain text / already markdown — pass through as-is
|
|
384
|
+
parts.push(block.text);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
break;
|
|
388
|
+
case "image":
|
|
389
|
+
parts.push(
|
|
390
|
+
`[image: ${block.mimeType ?? "unknown type"}, ${block.data ? Math.ceil((block.data.length * 3) / 4) : 0} bytes]`,
|
|
391
|
+
);
|
|
392
|
+
break;
|
|
393
|
+
case "resource":
|
|
394
|
+
parts.push(`[resource: ${block.uri ?? "unknown"}]`);
|
|
395
|
+
break;
|
|
396
|
+
default:
|
|
397
|
+
parts.push(`[${block.type}]`);
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
let output = parts.join("\n\n");
|
|
403
|
+
if (r.isError) {
|
|
404
|
+
output = `**error:** ${output}`;
|
|
405
|
+
}
|
|
406
|
+
return renderMarkdownToAnsi(output);
|
|
417
407
|
}
|
|
418
408
|
|
|
419
409
|
/** Convert a key name like "display_name" to "Display Name" */
|
|
420
410
|
function humanizeKey(key: string): string {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
411
|
+
return key
|
|
412
|
+
.replace(/[_-]/g, " ")
|
|
413
|
+
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
414
|
+
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
425
415
|
}
|
|
426
416
|
|
|
427
417
|
/** Check if a value is a plain primitive (string, number, boolean, null) */
|
|
428
418
|
function isPrimitive(value: unknown): value is string | number | boolean | null {
|
|
429
|
-
|
|
419
|
+
return value === null || typeof value !== "object";
|
|
430
420
|
}
|
|
431
421
|
|
|
432
422
|
/**
|
|
@@ -437,107 +427,107 @@ let urlCounter = 0;
|
|
|
437
427
|
let urlMap = new Map<string, string>();
|
|
438
428
|
|
|
439
429
|
function resetUrlPlaceholders(): void {
|
|
440
|
-
|
|
441
|
-
|
|
430
|
+
urlCounter = 0;
|
|
431
|
+
urlMap = new Map();
|
|
442
432
|
}
|
|
443
433
|
|
|
444
434
|
function restoreUrlPlaceholders(ansiOutput: string): string {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
435
|
+
for (const [token, url] of urlMap) {
|
|
436
|
+
ansiOutput = ansiOutput.replace(token, `\x1b[34m\x1b[4m${url}\x1b[24m\x1b[39m`);
|
|
437
|
+
}
|
|
438
|
+
return ansiOutput;
|
|
449
439
|
}
|
|
450
440
|
|
|
451
441
|
/** Format a primitive value, replacing URLs with placeholders to avoid mangling */
|
|
452
442
|
function formatPrimitive(value: string | number | boolean | null): string {
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
443
|
+
const str = String(value ?? "null");
|
|
444
|
+
if (typeof value === "string" && /^https?:\/\/\S+$/.test(value)) {
|
|
445
|
+
const token = `URLPLACEHOLDER${urlCounter++}`;
|
|
446
|
+
urlMap.set(token, str);
|
|
447
|
+
return token;
|
|
448
|
+
}
|
|
449
|
+
return str;
|
|
460
450
|
}
|
|
461
451
|
|
|
462
452
|
/** Normalize a key for label matching: lowercase, strip underscores/hyphens */
|
|
463
453
|
function normalizeKey(key: string): string {
|
|
464
|
-
|
|
454
|
+
return key.replace(/[_-]/g, "").toLowerCase();
|
|
465
455
|
}
|
|
466
456
|
|
|
467
457
|
/** Priority-ordered label keys (checked after normalization) */
|
|
468
458
|
const LABEL_KEYS = [
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
459
|
+
"name",
|
|
460
|
+
"displayname",
|
|
461
|
+
"fullname",
|
|
462
|
+
"username",
|
|
463
|
+
"screenname",
|
|
464
|
+
"title",
|
|
465
|
+
"subject",
|
|
466
|
+
"headline",
|
|
467
|
+
"heading",
|
|
468
|
+
"label",
|
|
469
|
+
"description",
|
|
470
|
+
"summary",
|
|
471
|
+
"email",
|
|
472
|
+
"url",
|
|
473
|
+
"slug",
|
|
474
|
+
"key",
|
|
475
|
+
"identifier",
|
|
486
476
|
];
|
|
487
477
|
|
|
488
478
|
/** Find the best label field in an object, returning { originalKey, value } or null */
|
|
489
479
|
function findLabel(obj: Record<string, unknown>): { originalKey: string; value: string } | null {
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
480
|
+
const entries = Object.entries(obj);
|
|
481
|
+
for (const candidate of LABEL_KEYS) {
|
|
482
|
+
for (const [key, val] of entries) {
|
|
483
|
+
if (normalizeKey(key) === candidate && typeof val === "string" && val.length > 0) {
|
|
484
|
+
return { originalKey: key, value: val };
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return null;
|
|
499
489
|
}
|
|
500
490
|
|
|
501
491
|
/** Render object entries as an indented bullet list */
|
|
502
492
|
function objectToBullets(entries: [string, unknown][], indent: number, skipKey?: string): string {
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
493
|
+
const prefix = " ".repeat(indent);
|
|
494
|
+
const lines: string[] = [];
|
|
495
|
+
|
|
496
|
+
for (const [key, val] of entries) {
|
|
497
|
+
if (key === skipKey) continue;
|
|
498
|
+
const heading = humanizeKey(key);
|
|
499
|
+
|
|
500
|
+
if (isPrimitive(val)) {
|
|
501
|
+
lines.push(`${prefix}- **${heading}:** ${formatPrimitive(val)}`);
|
|
502
|
+
} else if (Array.isArray(val) && val.every(isPrimitive)) {
|
|
503
|
+
lines.push(`${prefix}- **${heading}:**`);
|
|
504
|
+
for (const v of val) {
|
|
505
|
+
lines.push(`${prefix} - ${formatPrimitive(v)}`);
|
|
506
|
+
}
|
|
507
|
+
} else if (Array.isArray(val)) {
|
|
508
|
+
lines.push(`${prefix}- **${heading}:**`);
|
|
509
|
+
for (const item of val) {
|
|
510
|
+
if (isPrimitive(item)) {
|
|
511
|
+
lines.push(`${prefix} - ${formatPrimitive(item)}`);
|
|
512
|
+
} else {
|
|
513
|
+
const itemObj = item as Record<string, unknown>;
|
|
514
|
+
const label = findLabel(itemObj);
|
|
515
|
+
if (label) {
|
|
516
|
+
lines.push(`${prefix} - ${label.value}`);
|
|
517
|
+
lines.push(objectToBullets(Object.entries(itemObj), indent + 4, label.originalKey));
|
|
518
|
+
} else {
|
|
519
|
+
lines.push(`${prefix} -`);
|
|
520
|
+
lines.push(objectToBullets(Object.entries(itemObj), indent + 4));
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
} else {
|
|
525
|
+
lines.push(`${prefix}- **${heading}:**`);
|
|
526
|
+
lines.push(objectToBullets(Object.entries(val as Record<string, unknown>), indent + 2));
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return lines.join("\n");
|
|
541
531
|
}
|
|
542
532
|
|
|
543
533
|
/**
|
|
@@ -546,471 +536,457 @@ function objectToBullets(entries: [string, unknown][], indent: number, skipKey?:
|
|
|
546
536
|
* Arrays of objects use a label field (name, title, etc.) in the heading when available.
|
|
547
537
|
*/
|
|
548
538
|
export function jsonToMarkdown(value: unknown, depth: number = 1, skipKey?: string): string {
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
539
|
+
if (isPrimitive(value)) {
|
|
540
|
+
return formatPrimitive(value);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// At depth >= 3, switch to bullet-list rendering
|
|
544
|
+
if (depth >= 3) {
|
|
545
|
+
if (Array.isArray(value)) {
|
|
546
|
+
if (value.every(isPrimitive)) {
|
|
547
|
+
return value.map((v) => `- ${formatPrimitive(v)}`).join("\n");
|
|
548
|
+
}
|
|
549
|
+
return value
|
|
550
|
+
.map((item) => {
|
|
551
|
+
if (isPrimitive(item)) return `- ${formatPrimitive(item)}`;
|
|
552
|
+
const obj = item as Record<string, unknown>;
|
|
553
|
+
const label = findLabel(obj);
|
|
554
|
+
const header = label ? `- ${label.value}` : `-`;
|
|
555
|
+
return `${header}\n${objectToBullets(Object.entries(obj), 2, label?.originalKey)}`;
|
|
556
|
+
})
|
|
557
|
+
.join("\n");
|
|
558
|
+
}
|
|
559
|
+
return objectToBullets(Object.entries(value as Record<string, unknown>), 0, skipKey);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (Array.isArray(value)) {
|
|
563
|
+
// Array of all primitives → bullet list
|
|
564
|
+
if (value.every(isPrimitive)) {
|
|
565
|
+
return value.map((v) => `- ${formatPrimitive(v)}`).join("\n");
|
|
566
|
+
}
|
|
567
|
+
// Array of objects → numbered sub-sections with label
|
|
568
|
+
return value
|
|
569
|
+
.map((item, i) => {
|
|
570
|
+
if (isPrimitive(item)) {
|
|
571
|
+
return `- ${formatPrimitive(item)}`;
|
|
572
|
+
}
|
|
573
|
+
const obj = item as Record<string, unknown>;
|
|
574
|
+
const labelInfo = findLabel(obj);
|
|
575
|
+
const numberLabel = labelInfo ? `${i + 1}. ${labelInfo.value}` : `${i + 1}`;
|
|
576
|
+
const heading = depth <= 6 ? `${"#".repeat(depth)} ${numberLabel}` : `**${numberLabel}**`;
|
|
577
|
+
return `${heading}\n\n${jsonToMarkdown(item, depth + 1, labelInfo?.originalKey)}`;
|
|
578
|
+
})
|
|
579
|
+
.join("\n\n");
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Object → each key becomes a heading
|
|
583
|
+
const entries = Object.entries(value as Record<string, unknown>);
|
|
584
|
+
const lines: string[] = [];
|
|
585
|
+
|
|
586
|
+
for (const [key, val] of entries) {
|
|
587
|
+
const heading = humanizeKey(key);
|
|
588
|
+
|
|
589
|
+
if (isPrimitive(val)) {
|
|
590
|
+
if (depth <= 6) {
|
|
591
|
+
lines.push(`${"#".repeat(depth)} ${heading}\n\n${formatPrimitive(val)}`);
|
|
592
|
+
} else {
|
|
593
|
+
lines.push(`**${heading}:** ${formatPrimitive(val)}`);
|
|
594
|
+
}
|
|
595
|
+
} else if (Array.isArray(val) && val.every(isPrimitive)) {
|
|
596
|
+
// Array of primitives: heading then bullet list
|
|
597
|
+
const list = val.map((v) => `- ${formatPrimitive(v)}`).join("\n");
|
|
598
|
+
if (depth <= 6) {
|
|
599
|
+
lines.push(`${"#".repeat(depth)} ${heading}\n\n${list}`);
|
|
600
|
+
} else {
|
|
601
|
+
lines.push(`**${heading}:**\n${list}`);
|
|
602
|
+
}
|
|
603
|
+
} else {
|
|
604
|
+
// Nested object or array of objects
|
|
605
|
+
const label = depth <= 6 ? `${"#".repeat(depth)} ${heading}` : `**${heading}**`;
|
|
606
|
+
lines.push(`${label}\n\n${jsonToMarkdown(val, depth + 1)}`);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return lines.join("\n\n");
|
|
621
611
|
}
|
|
622
612
|
|
|
623
613
|
/** Render a markdown string to ANSI-styled terminal output using Bun's built-in renderer */
|
|
624
614
|
export function renderMarkdownToAnsi(input: string): string {
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
615
|
+
// biome-ignore lint/suspicious/noExplicitAny: Bun.markdown.ansi is not yet in @types/bun
|
|
616
|
+
const result = (Bun as any).markdown.ansi(input) as string;
|
|
617
|
+
const restored = restoreUrlPlaceholders(result);
|
|
618
|
+
resetUrlPlaceholders();
|
|
619
|
+
return restored;
|
|
629
620
|
}
|
|
630
621
|
|
|
631
622
|
/** Recursively parse JSON strings inside MCP content blocks */
|
|
632
623
|
function parseNestedJson(value: unknown): unknown {
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
624
|
+
if (typeof value === "string") {
|
|
625
|
+
try {
|
|
626
|
+
return parseNestedJson(JSON.parse(value));
|
|
627
|
+
} catch {
|
|
628
|
+
return value;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
if (Array.isArray(value)) {
|
|
632
|
+
return value.map(parseNestedJson);
|
|
633
|
+
}
|
|
634
|
+
if (typeof value === "object" && value !== null) {
|
|
635
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, parseNestedJson(v)]));
|
|
636
|
+
}
|
|
637
|
+
return value;
|
|
647
638
|
}
|
|
648
639
|
|
|
649
640
|
/** Format validation errors for tool input */
|
|
650
641
|
export function formatValidationErrors(
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
642
|
+
serverName: string,
|
|
643
|
+
toolName: string,
|
|
644
|
+
errors: ValidationError[],
|
|
645
|
+
options: FormatOptions,
|
|
655
646
|
): string {
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
647
|
+
return formatOutput(
|
|
648
|
+
{ error: "validation", server: serverName, tool: toolName, details: errors },
|
|
649
|
+
() => {
|
|
650
|
+
const header = `${red("error:")} invalid arguments for ${cyan(serverName)}/${bold(toolName)}`;
|
|
651
|
+
const details = errors.map((e) => ` ${yellow(e.path)}: ${e.message}`).join("\n");
|
|
652
|
+
return `${header}\n${details}`;
|
|
653
|
+
},
|
|
654
|
+
options,
|
|
655
|
+
);
|
|
665
656
|
}
|
|
666
657
|
|
|
667
658
|
/** Format search results */
|
|
668
659
|
export function formatSearchResults(results: SearchResult[], options: FormatOptions): string {
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
);
|
|
660
|
+
return formatOutput(
|
|
661
|
+
results,
|
|
662
|
+
() => {
|
|
663
|
+
if (results.length === 0) {
|
|
664
|
+
return dim("No matching tools found");
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const termWidth = getTerminalWidth();
|
|
668
|
+
const descIndent = 2;
|
|
669
|
+
|
|
670
|
+
return results
|
|
671
|
+
.map((r) => {
|
|
672
|
+
const header = `${cyan(r.server)} ${bold(r.tool)} ${yellow(r.score.toFixed(2))}`;
|
|
673
|
+
const fullDesc = r.description
|
|
674
|
+
.split("\n")
|
|
675
|
+
.map((l) => l.trim())
|
|
676
|
+
.filter((l) => l.length > 0)
|
|
677
|
+
.join(" ");
|
|
678
|
+
const indent = " ".repeat(descIndent);
|
|
679
|
+
const desc = termWidth != null ? wrapDescription(fullDesc, descIndent, termWidth) : dim(fullDesc);
|
|
680
|
+
return `${header}\n${indent}${desc}`;
|
|
681
|
+
})
|
|
682
|
+
.join("\n\n");
|
|
683
|
+
},
|
|
684
|
+
options,
|
|
685
|
+
);
|
|
696
686
|
}
|
|
697
687
|
|
|
698
688
|
/** Format a list of resources with server names */
|
|
699
|
-
export function formatResourceList(
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
}),
|
|
719
|
-
options,
|
|
720
|
-
);
|
|
689
|
+
export function formatResourceList(resources: ResourceWithServer[], options: FormatOptions): string {
|
|
690
|
+
return formatOutput(
|
|
691
|
+
resources.map((r) => ({
|
|
692
|
+
server: r.server,
|
|
693
|
+
uri: r.resource.uri,
|
|
694
|
+
name: r.resource.name,
|
|
695
|
+
...(options.withDescriptions ? { description: r.resource.description ?? "" } : {}),
|
|
696
|
+
})),
|
|
697
|
+
() =>
|
|
698
|
+
formatTable(resources, {
|
|
699
|
+
columns: [
|
|
700
|
+
{ value: (r) => r.server, style: cyan },
|
|
701
|
+
{ value: (r) => r.resource.uri, style: bold },
|
|
702
|
+
],
|
|
703
|
+
description: options.withDescriptions ? (r) => r.resource.description : undefined,
|
|
704
|
+
emptyMessage: "No resources found",
|
|
705
|
+
}),
|
|
706
|
+
options,
|
|
707
|
+
);
|
|
721
708
|
}
|
|
722
709
|
|
|
723
710
|
/** Format resources for a single server */
|
|
724
|
-
export function formatServerResources(
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
return `${header}\n${body}`;
|
|
749
|
-
},
|
|
750
|
-
options,
|
|
751
|
-
);
|
|
711
|
+
export function formatServerResources(serverName: string, resources: Resource[], options: FormatOptions): string {
|
|
712
|
+
return formatOutput(
|
|
713
|
+
{
|
|
714
|
+
server: serverName,
|
|
715
|
+
resources: resources.map((r) => ({
|
|
716
|
+
uri: r.uri,
|
|
717
|
+
name: r.name,
|
|
718
|
+
description: r.description ?? "",
|
|
719
|
+
mimeType: r.mimeType ?? "",
|
|
720
|
+
})),
|
|
721
|
+
},
|
|
722
|
+
() => {
|
|
723
|
+
if (resources.length === 0) {
|
|
724
|
+
return dim(`No resources found for ${serverName}`);
|
|
725
|
+
}
|
|
726
|
+
const header = cyan.bold(serverName);
|
|
727
|
+
const body = formatTable(resources, {
|
|
728
|
+
columns: [{ value: (r) => ` ${r.uri}`, style: bold }],
|
|
729
|
+
description: (r) => r.description,
|
|
730
|
+
});
|
|
731
|
+
return `${header}\n${body}`;
|
|
732
|
+
},
|
|
733
|
+
options,
|
|
734
|
+
);
|
|
752
735
|
}
|
|
753
736
|
|
|
754
737
|
/** Format resource contents */
|
|
755
738
|
export function formatResourceContents(
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
739
|
+
serverName: string,
|
|
740
|
+
uri: string,
|
|
741
|
+
result: unknown,
|
|
742
|
+
options: FormatOptions,
|
|
760
743
|
): string {
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
);
|
|
744
|
+
return formatOutput(
|
|
745
|
+
{ server: serverName, uri, contents: (result as { contents: unknown })?.contents ?? result },
|
|
746
|
+
() => {
|
|
747
|
+
const contents =
|
|
748
|
+
(result as { contents?: Array<{ text?: string; blob?: string; mimeType?: string }> })?.contents ?? [];
|
|
749
|
+
const lines: string[] = [];
|
|
750
|
+
lines.push(`${cyan(serverName)}/${bold(uri)}`);
|
|
751
|
+
lines.push("");
|
|
752
|
+
|
|
753
|
+
if (contents.length === 0) {
|
|
754
|
+
lines.push(dim("(empty)"));
|
|
755
|
+
} else {
|
|
756
|
+
for (const item of contents) {
|
|
757
|
+
if (item.text !== undefined) {
|
|
758
|
+
lines.push(item.text);
|
|
759
|
+
} else if (item.blob !== undefined) {
|
|
760
|
+
lines.push(dim(`<binary blob, ${item.blob.length} bytes base64>`));
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
return lines.join("\n");
|
|
766
|
+
},
|
|
767
|
+
options,
|
|
768
|
+
);
|
|
787
769
|
}
|
|
788
770
|
|
|
789
771
|
/** Format a list of prompts with server names */
|
|
790
772
|
export function formatPromptList(prompts: PromptWithServer[], options: FormatOptions): string {
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
773
|
+
return formatOutput(
|
|
774
|
+
prompts.map((p) => ({
|
|
775
|
+
server: p.server,
|
|
776
|
+
name: p.prompt.name,
|
|
777
|
+
...(options.withDescriptions ? { description: p.prompt.description ?? "" } : {}),
|
|
778
|
+
})),
|
|
779
|
+
() =>
|
|
780
|
+
formatTable(prompts, {
|
|
781
|
+
columns: [
|
|
782
|
+
{ value: (p) => p.server, style: cyan },
|
|
783
|
+
{ value: (p) => p.prompt.name, style: bold },
|
|
784
|
+
],
|
|
785
|
+
description: options.withDescriptions ? (p) => p.prompt.description : undefined,
|
|
786
|
+
emptyMessage: "No prompts found",
|
|
787
|
+
}),
|
|
788
|
+
options,
|
|
789
|
+
);
|
|
808
790
|
}
|
|
809
791
|
|
|
810
792
|
/** Format prompts for a single server */
|
|
811
|
-
export function formatServerPrompts(
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
return [header, ...lines].join("\n");
|
|
851
|
-
},
|
|
852
|
-
options,
|
|
853
|
-
);
|
|
793
|
+
export function formatServerPrompts(serverName: string, prompts: Prompt[], options: FormatOptions): string {
|
|
794
|
+
return formatOutput(
|
|
795
|
+
{
|
|
796
|
+
server: serverName,
|
|
797
|
+
prompts: prompts.map((p) => ({
|
|
798
|
+
name: p.name,
|
|
799
|
+
description: p.description ?? "",
|
|
800
|
+
arguments: p.arguments ?? [],
|
|
801
|
+
})),
|
|
802
|
+
},
|
|
803
|
+
() => {
|
|
804
|
+
if (prompts.length === 0) {
|
|
805
|
+
return dim(`No prompts found for ${serverName}`);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const header = cyan.bold(serverName);
|
|
809
|
+
const maxName = Math.max(...prompts.map((p) => p.name.length));
|
|
810
|
+
const termWidth = getTerminalWidth();
|
|
811
|
+
|
|
812
|
+
const lines = prompts.map((p) => {
|
|
813
|
+
const name = ` ${bold(p.name.padEnd(maxName))}`;
|
|
814
|
+
const args =
|
|
815
|
+
p.arguments && p.arguments.length > 0
|
|
816
|
+
? ` ${dim(`(${p.arguments.map((a) => (a.required ? a.name : `[${a.name}]`)).join(", ")})`)}`
|
|
817
|
+
: "";
|
|
818
|
+
if (p.description) {
|
|
819
|
+
const prefix = `${name}${args}`;
|
|
820
|
+
const pw = visibleLength(prefix) + 2;
|
|
821
|
+
const desc = termWidth != null ? wrapDescription(p.description, pw, termWidth) : dim(p.description);
|
|
822
|
+
return `${prefix} ${desc}`;
|
|
823
|
+
}
|
|
824
|
+
return `${name}${args}`;
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
return [header, ...lines].join("\n");
|
|
828
|
+
},
|
|
829
|
+
options,
|
|
830
|
+
);
|
|
854
831
|
}
|
|
855
832
|
|
|
856
833
|
/** Format prompt messages */
|
|
857
834
|
export function formatPromptMessages(
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
835
|
+
serverName: string,
|
|
836
|
+
name: string,
|
|
837
|
+
result: unknown,
|
|
838
|
+
options: FormatOptions,
|
|
862
839
|
): string {
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
840
|
+
return formatOutput(
|
|
841
|
+
{ server: serverName, prompt: name, ...(result as object) },
|
|
842
|
+
() => {
|
|
843
|
+
const r = result as {
|
|
844
|
+
description?: string;
|
|
845
|
+
messages?: Array<{ role: string; content: { type: string; text?: string } }>;
|
|
846
|
+
};
|
|
847
|
+
const lines: string[] = [];
|
|
848
|
+
lines.push(`${cyan(serverName)}/${bold(name)}`);
|
|
849
|
+
if (r.description) lines.push(dim(r.description));
|
|
850
|
+
lines.push("");
|
|
851
|
+
for (const msg of r.messages ?? []) {
|
|
852
|
+
lines.push(`${bold(msg.role)}:`);
|
|
853
|
+
if (msg.content.text !== undefined) {
|
|
854
|
+
lines.push(` ${msg.content.text}`);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
return lines.join("\n");
|
|
858
|
+
},
|
|
859
|
+
options,
|
|
860
|
+
);
|
|
884
861
|
}
|
|
885
862
|
|
|
886
863
|
/** Format a unified list of tools, resources, and prompts across servers */
|
|
887
864
|
export function formatUnifiedList(items: UnifiedItem[], options: FormatOptions): string {
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
865
|
+
const typeLabel = (t: string) => {
|
|
866
|
+
if (t === "tool") return green(t);
|
|
867
|
+
if (t === "resource") return cyan(t);
|
|
868
|
+
return yellow(t);
|
|
869
|
+
};
|
|
870
|
+
|
|
871
|
+
return formatOutput(
|
|
872
|
+
items.map((i) => ({
|
|
873
|
+
server: i.server,
|
|
874
|
+
type: i.type,
|
|
875
|
+
name: i.name,
|
|
876
|
+
...(options.withDescriptions ? { description: i.description ?? "" } : {}),
|
|
877
|
+
})),
|
|
878
|
+
() =>
|
|
879
|
+
formatTable(items, {
|
|
880
|
+
columns: [
|
|
881
|
+
{ value: (i) => i.server, style: cyan },
|
|
882
|
+
{ value: (i) => i.type, style: typeLabel },
|
|
883
|
+
{ value: (i) => i.name, style: bold },
|
|
884
|
+
],
|
|
885
|
+
description: options.withDescriptions ? (i) => i.description : undefined,
|
|
886
|
+
emptyMessage: "No tools, resources, or prompts found",
|
|
887
|
+
}),
|
|
888
|
+
options,
|
|
889
|
+
);
|
|
913
890
|
}
|
|
914
891
|
|
|
915
892
|
/** Format a single task status */
|
|
916
893
|
export function formatTaskStatus(
|
|
917
|
-
|
|
918
|
-
|
|
894
|
+
task: { taskId: string; status: string; [key: string]: unknown },
|
|
895
|
+
options: FormatOptions,
|
|
919
896
|
): string {
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
);
|
|
897
|
+
return formatOutput(
|
|
898
|
+
task,
|
|
899
|
+
() => {
|
|
900
|
+
const statusColor = (s: string) => {
|
|
901
|
+
switch (s) {
|
|
902
|
+
case "completed":
|
|
903
|
+
return green(s);
|
|
904
|
+
case "working":
|
|
905
|
+
return yellow(s);
|
|
906
|
+
case "failed":
|
|
907
|
+
case "cancelled":
|
|
908
|
+
return red(s);
|
|
909
|
+
case "input_required":
|
|
910
|
+
return yellow(s);
|
|
911
|
+
default:
|
|
912
|
+
return s;
|
|
913
|
+
}
|
|
914
|
+
};
|
|
915
|
+
|
|
916
|
+
const lines: string[] = [];
|
|
917
|
+
lines.push(`${bold("Task:")} ${cyan(task.taskId)}`);
|
|
918
|
+
lines.push(`${bold("Status:")} ${statusColor(task.status)}`);
|
|
919
|
+
if (task.statusMessage) lines.push(`${bold("Message:")} ${dim(String(task.statusMessage))}`);
|
|
920
|
+
if (task.createdAt) lines.push(`${bold("Created:")} ${dim(String(task.createdAt))}`);
|
|
921
|
+
if (task.lastUpdatedAt) lines.push(`${bold("Updated:")} ${dim(String(task.lastUpdatedAt))}`);
|
|
922
|
+
if (task.ttl != null) lines.push(`${bold("TTL:")} ${dim(`${String(task.ttl)}ms`)}`);
|
|
923
|
+
if (task.pollInterval != null) lines.push(`${bold("Poll interval:")} ${dim(`${String(task.pollInterval)}ms`)}`);
|
|
924
|
+
return lines.join("\n");
|
|
925
|
+
},
|
|
926
|
+
options,
|
|
927
|
+
);
|
|
952
928
|
}
|
|
953
929
|
|
|
954
930
|
/** Format a list of tasks */
|
|
955
931
|
export function formatTasksList(
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
932
|
+
tasks: Array<{ taskId: string; status: string; [key: string]: unknown }>,
|
|
933
|
+
nextCursor: string | undefined,
|
|
934
|
+
options: FormatOptions,
|
|
959
935
|
): string {
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
936
|
+
return formatOutput(
|
|
937
|
+
{ tasks, ...(nextCursor ? { nextCursor } : {}) },
|
|
938
|
+
() => {
|
|
939
|
+
if (tasks.length === 0) {
|
|
940
|
+
return dim("No tasks found");
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
const statusColor = (s: string) => {
|
|
944
|
+
switch (s) {
|
|
945
|
+
case "completed":
|
|
946
|
+
return green(s.padEnd(14));
|
|
947
|
+
case "working":
|
|
948
|
+
return yellow(s.padEnd(14));
|
|
949
|
+
case "failed":
|
|
950
|
+
case "cancelled":
|
|
951
|
+
return red(s.padEnd(14));
|
|
952
|
+
default:
|
|
953
|
+
return s.padEnd(14);
|
|
954
|
+
}
|
|
955
|
+
};
|
|
956
|
+
|
|
957
|
+
const maxId = Math.max(...tasks.map((t) => t.taskId.length));
|
|
958
|
+
|
|
959
|
+
const lines = tasks.map((t) => {
|
|
960
|
+
const id = cyan(t.taskId.padEnd(maxId));
|
|
961
|
+
const status = statusColor(t.status);
|
|
962
|
+
const updated = t.lastUpdatedAt ? dim(String(t.lastUpdatedAt)) : "";
|
|
963
|
+
return `${id} ${status} ${updated}`;
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
if (nextCursor) {
|
|
967
|
+
lines.push("");
|
|
968
|
+
lines.push(dim(`Next cursor: ${nextCursor}`));
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
return lines.join("\n");
|
|
972
|
+
},
|
|
973
|
+
options,
|
|
974
|
+
);
|
|
999
975
|
}
|
|
1000
976
|
|
|
1001
977
|
/** Format task creation output (for --no-wait) */
|
|
1002
978
|
export function formatTaskCreated(
|
|
1003
|
-
|
|
1004
|
-
|
|
979
|
+
task: { taskId: string; status: string; [key: string]: unknown },
|
|
980
|
+
options: FormatOptions,
|
|
1005
981
|
): string {
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
982
|
+
return formatOutput(
|
|
983
|
+
{ task },
|
|
984
|
+
() => `${green("Task created:")} ${cyan(task.taskId)} ${dim(`(status: ${task.status})`)}`,
|
|
985
|
+
options,
|
|
986
|
+
);
|
|
1011
987
|
}
|
|
1012
988
|
|
|
1013
989
|
/** Format an error message */
|
|
1014
990
|
export function formatError(message: string, options: FormatOptions): string {
|
|
1015
|
-
|
|
991
|
+
return formatOutput({ error: message }, () => `${red("error:")} ${message}`, options);
|
|
1016
992
|
}
|