toolcraft 0.0.60 → 0.0.61

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.
@@ -0,0 +1,53 @@
1
+ export interface HttpErrorLike {
2
+ name: string;
3
+ message: string;
4
+ request: {
5
+ method: string;
6
+ url: string;
7
+ headers: Record<string, string>;
8
+ body?: unknown;
9
+ };
10
+ response: {
11
+ status: number;
12
+ statusText: string;
13
+ headers: Record<string, string>;
14
+ body: unknown;
15
+ };
16
+ }
17
+ export interface ApiErrorSummary {
18
+ status: number;
19
+ statusText: string;
20
+ requestMethod: string;
21
+ requestUrl: string;
22
+ requestId?: string;
23
+ code?: string;
24
+ message?: string;
25
+ fieldErrors?: Array<{
26
+ path: string;
27
+ message: string;
28
+ }>;
29
+ retryAfter?: string;
30
+ hint?: string;
31
+ }
32
+ export interface ToolcraftErrorEnvelope {
33
+ kind: "http";
34
+ message: string;
35
+ code?: string;
36
+ requestId?: string;
37
+ http: {
38
+ method: string;
39
+ url: string;
40
+ status: number;
41
+ statusText: string;
42
+ };
43
+ fieldErrors?: Array<{
44
+ path: string;
45
+ message: string;
46
+ }>;
47
+ retryAfter?: string;
48
+ hint?: string;
49
+ reportPath?: string;
50
+ }
51
+ export declare function isHttpErrorLike(error: unknown): error is HttpErrorLike;
52
+ export declare function summarizeHttpError(error: HttpErrorLike): ApiErrorSummary;
53
+ export declare function createHttpErrorEnvelope(error: HttpErrorLike, reportPath?: string): ToolcraftErrorEnvelope;
@@ -0,0 +1,162 @@
1
+ import { redactHttpBody } from "./redaction.js";
2
+ const REQUEST_ID_FIELDS = ["request_id", "requestId", "id"];
3
+ const CODE_FIELDS = ["code", "error_code", "errorCode", "error"];
4
+ const MESSAGE_FIELDS = ["message", "detail", "title", "error_description"];
5
+ export function isHttpErrorLike(error) {
6
+ if (!isPlainObject(error)) {
7
+ return false;
8
+ }
9
+ if (typeof error.name !== "string" || typeof error.message !== "string") {
10
+ return false;
11
+ }
12
+ const request = error.request;
13
+ const response = error.response;
14
+ return (isPlainObject(request) &&
15
+ typeof request.method === "string" &&
16
+ typeof request.url === "string" &&
17
+ isStringRecord(request.headers) &&
18
+ isPlainObject(response) &&
19
+ typeof response.status === "number" &&
20
+ typeof response.statusText === "string" &&
21
+ isStringRecord(response.headers) &&
22
+ hasOwnProperty(response, "body"));
23
+ }
24
+ export function summarizeHttpError(error) {
25
+ const body = redactHttpBody(error.response.body);
26
+ const retryAfter = getHeader(error.response.headers, "retry-after");
27
+ const message = extractMessage(body);
28
+ const summary = {
29
+ status: error.response.status,
30
+ statusText: error.response.statusText,
31
+ requestMethod: error.request.method,
32
+ requestUrl: error.request.url,
33
+ ...(message === undefined ? {} : { message }),
34
+ ...optionalString("requestId", getHeader(error.response.headers, "x-request-id") ?? extractFirstString(body, REQUEST_ID_FIELDS)),
35
+ ...optionalString("code", extractCode(body)),
36
+ ...optionalString("retryAfter", retryAfter),
37
+ ...optionalArray("fieldErrors", extractFieldErrors(body)),
38
+ ...optionalString("hint", createHttpErrorHint(error.response.status, retryAfter))
39
+ };
40
+ return summary;
41
+ }
42
+ export function createHttpErrorEnvelope(error, reportPath) {
43
+ const summary = summarizeHttpError(error);
44
+ return {
45
+ kind: "http",
46
+ message: summary.message ?? error.message,
47
+ ...(summary.code === undefined ? {} : { code: summary.code }),
48
+ ...(summary.requestId === undefined ? {} : { requestId: summary.requestId }),
49
+ http: {
50
+ method: summary.requestMethod,
51
+ url: summary.requestUrl,
52
+ status: summary.status,
53
+ statusText: summary.statusText
54
+ },
55
+ ...(summary.fieldErrors === undefined ? {} : { fieldErrors: summary.fieldErrors }),
56
+ ...(summary.retryAfter === undefined ? {} : { retryAfter: summary.retryAfter }),
57
+ ...(summary.hint === undefined ? {} : { hint: summary.hint }),
58
+ ...(reportPath === undefined ? {} : { reportPath })
59
+ };
60
+ }
61
+ function createHttpErrorHint(status, retryAfter) {
62
+ if (status === 401 || status === 403) {
63
+ return "Check the configured API credentials and permissions.";
64
+ }
65
+ if ((status === 429 || status === 503) && retryAfter !== undefined) {
66
+ return `Retry after ${retryAfter}.`;
67
+ }
68
+ return undefined;
69
+ }
70
+ function extractMessage(body) {
71
+ if (!isPlainObject(body)) {
72
+ return undefined;
73
+ }
74
+ const graphqlMessage = extractGraphQlMessage(body);
75
+ if (graphqlMessage !== undefined) {
76
+ return graphqlMessage;
77
+ }
78
+ return extractFirstString(body, MESSAGE_FIELDS);
79
+ }
80
+ function extractCode(body) {
81
+ if (!isPlainObject(body)) {
82
+ return undefined;
83
+ }
84
+ const graphqlCode = extractGraphQlCode(body);
85
+ if (graphqlCode !== undefined) {
86
+ return graphqlCode;
87
+ }
88
+ return extractFirstString(body, CODE_FIELDS);
89
+ }
90
+ function extractGraphQlMessage(body) {
91
+ const [first] = Array.isArray(body.errors) ? body.errors : [];
92
+ return isPlainObject(first) && typeof first.message === "string" ? first.message : undefined;
93
+ }
94
+ function extractGraphQlCode(body) {
95
+ const [first] = Array.isArray(body.errors) ? body.errors : [];
96
+ if (!isPlainObject(first) || !isPlainObject(first.extensions)) {
97
+ return undefined;
98
+ }
99
+ return typeof first.extensions.code === "string" ? first.extensions.code : undefined;
100
+ }
101
+ function extractFieldErrors(body) {
102
+ if (!isPlainObject(body)) {
103
+ return undefined;
104
+ }
105
+ const candidates = [body.field_errors, body.fieldErrors, body.errors, body.non_field_errors];
106
+ const flattened = candidates.flatMap((candidate) => flattenFieldErrors(candidate, []));
107
+ return flattened.length === 0 ? undefined : flattened;
108
+ }
109
+ function flattenFieldErrors(value, path) {
110
+ if (typeof value === "string") {
111
+ return [{ path: formatPath(path), message: value }];
112
+ }
113
+ if (Array.isArray(value)) {
114
+ if (value.every((entry) => typeof entry === "string")) {
115
+ return value.map((message) => ({ path: formatPath(path), message }));
116
+ }
117
+ return value.flatMap((entry, index) => flattenFieldErrors(entry, [...path, String(index)]));
118
+ }
119
+ if (!isPlainObject(value)) {
120
+ return [];
121
+ }
122
+ return Object.entries(value).flatMap(([key, nested]) => flattenFieldErrors(nested, [...path, key]));
123
+ }
124
+ function formatPath(path) {
125
+ return path.length === 0 ? "error" : path.join(".");
126
+ }
127
+ function extractFirstString(body, fields) {
128
+ if (!isPlainObject(body)) {
129
+ return undefined;
130
+ }
131
+ for (const field of fields) {
132
+ const value = body[field];
133
+ if (typeof value === "string" && value.length > 0) {
134
+ return value;
135
+ }
136
+ }
137
+ return undefined;
138
+ }
139
+ function getHeader(headers, name) {
140
+ const lowerName = name.toLowerCase();
141
+ for (const [key, value] of Object.entries(headers)) {
142
+ if (key.toLowerCase() === lowerName && value.length > 0) {
143
+ return value;
144
+ }
145
+ }
146
+ return undefined;
147
+ }
148
+ function optionalString(name, value) {
149
+ return value === undefined ? {} : { [name]: value };
150
+ }
151
+ function optionalArray(name, value) {
152
+ return value === undefined ? {} : { [name]: value };
153
+ }
154
+ function isStringRecord(value) {
155
+ return isPlainObject(value) && Object.values(value).every((entry) => typeof entry === "string");
156
+ }
157
+ function isPlainObject(value) {
158
+ return typeof value === "object" && value !== null && !Array.isArray(value);
159
+ }
160
+ function hasOwnProperty(value, name) {
161
+ return Object.prototype.hasOwnProperty.call(value, name);
162
+ }
package/dist/cli.js CHANGED
@@ -4,6 +4,7 @@ import { Command as CommanderCommand, CommanderError, InvalidArgumentError, Opti
4
4
  import { cancel, configureTheme, confirm, createLogger, formatCommandList, formatOptionList, getTheme, helpFormatterPlain, isCancel, note, promptText, renderTable, resetOutputFormatCache, select, text } from "toolcraft-design";
5
5
  import { ApprovalDeclinedError, UserError, assertCommandRequirements, getCommandSourcePath, resolveCommandSecrets } from "./index.js";
6
6
  import { hasOwnErrorCode } from "./error-codes.js";
7
+ import { summarizeHttpError } from "./api-error-summary.js";
7
8
  import { mergeApprovalsGroup } from "./human-in-loop/approvals-commands.js";
8
9
  import { writeErrorReport } from "./error-report.js";
9
10
  import { invokeWithHumanInLoop } from "./human-in-loop/index.js";
@@ -746,7 +747,7 @@ function getHelpChildren(group, scope) {
746
747
  function findVisibleChild(group, token, scope) {
747
748
  return getVisibleChildren(group, scope).find((child) => child.name === token || child.aliases.includes(token));
748
749
  }
749
- function resolveHelpTarget(root, argv, scope, rootDisplayName) {
750
+ function resolveHelpTarget(root, argv, scope, rootUsageName, rootDisplayName) {
750
751
  const breadcrumb = [rootDisplayName ?? root.name];
751
752
  let current = root;
752
753
  for (const token of argv.slice(2)) {
@@ -758,7 +759,7 @@ function resolveHelpTarget(root, argv, scope, rootDisplayName) {
758
759
  }
759
760
  const child = findVisibleChild(current, token, scope);
760
761
  if (child === undefined) {
761
- break;
762
+ throw new UserError(formatUnknownHelpCommandMessage(current, token, scope, rootUsageName, breadcrumb));
762
763
  }
763
764
  breadcrumb.push(child.name);
764
765
  current = child;
@@ -768,6 +769,12 @@ function resolveHelpTarget(root, argv, scope, rootDisplayName) {
768
769
  node: current
769
770
  };
770
771
  }
772
+ function formatUnknownHelpCommandMessage(group, input, scope, rootUsageName, breadcrumb) {
773
+ const suggestions = suggest(input, getHelpChildren(group, scope).map((child) => child.name));
774
+ const commandPath = breadcrumb.slice(1).join(" ");
775
+ const helpTarget = commandPath.length === 0 ? rootUsageName : `${rootUsageName} ${commandPath}`;
776
+ return `${formatSuggestionMessage(`Unknown command "${input}".`, suggestions)}\nRun ${helpTarget} --help for usage.`;
777
+ }
771
778
  function formatHelpFieldFlags(field, globalLongOptionFlags) {
772
779
  if (field.positionalIndex !== undefined) {
773
780
  return formatPositionalToken(field);
@@ -1213,6 +1220,79 @@ function renderLeafHelp(command, breadcrumb, casing, globalOptions, rootUsageNam
1213
1220
  sections
1214
1221
  });
1215
1222
  }
1223
+ function renderJsonHelp(target, root, casing, globalOptions, rootUsageName) {
1224
+ const globalLongOptionFlags = getGlobalLongOptionFlags(globalOptions.presetsEnabled, globalOptions.showVersion, globalOptions.controls);
1225
+ const node = target.node;
1226
+ if (node.kind === "group") {
1227
+ const commandRows = formatCommandRows(node, "cli", casing, globalLongOptionFlags);
1228
+ const isRoot = node === root;
1229
+ return `${JSON.stringify({
1230
+ schemaVersion: 1,
1231
+ kind: "group",
1232
+ name: target.breadcrumb.at(-1) ?? rootUsageName,
1233
+ path: target.breadcrumb.filter((segment) => segment.length > 0),
1234
+ usage: buildUsageLine(target.breadcrumb, rootUsageName, formatGroupUsageSuffix(node, "cli", casing, globalLongOptionFlags)),
1235
+ ...(node.description === undefined ? {} : { description: node.description }),
1236
+ commands: commandRows.map((row) => ({ name: row.name, description: row.description })),
1237
+ options: isRoot
1238
+ ? collectSchemaGlobalFieldRows(node, "cli", casing, globalLongOptionFlags).map((row) => ({
1239
+ name: row.flags.split(/[ ,]+/)[0]?.replace(/^--/, "") ?? row.flags,
1240
+ flags: row.flags.split(", "),
1241
+ type: "unknown",
1242
+ description: row.description,
1243
+ required: false
1244
+ }))
1245
+ : []
1246
+ }, null, 2)}\n`;
1247
+ }
1248
+ const collected = collectFields(node.params, casing, globalLongOptionFlags);
1249
+ const fields = assignPositionals(collected.fields, node.positional);
1250
+ const positionalFields = fields.filter((field) => field.positionalIndex !== undefined);
1251
+ const usageSuffix = positionalFields.length > 0
1252
+ ? `[OPTIONS] ${positionalFields.map(formatPositionalToken).join(" ")}`
1253
+ : "[OPTIONS]";
1254
+ return `${JSON.stringify({
1255
+ schemaVersion: 1,
1256
+ kind: "command",
1257
+ name: node.name,
1258
+ path: target.breadcrumb.filter((segment) => segment.length > 0),
1259
+ usage: buildUsageLine(target.breadcrumb, rootUsageName, usageSuffix),
1260
+ ...(node.description === undefined ? {} : { description: node.description }),
1261
+ options: fields
1262
+ .filter((field) => field.global !== true)
1263
+ .map((field) => formatJsonHelpOption(field, globalLongOptionFlags)),
1264
+ secrets: Object.entries(node.secrets).map(([name, secret]) => ({
1265
+ name,
1266
+ env: secret.env,
1267
+ required: secret.optional !== true,
1268
+ ...(secret.description === undefined ? {} : { description: secret.description })
1269
+ })),
1270
+ examples: node.examples
1271
+ }, null, 2)}\n`;
1272
+ }
1273
+ function formatJsonHelpOption(field, globalLongOptionFlags) {
1274
+ return {
1275
+ name: field.displayPath,
1276
+ flags: formatHelpFieldFlags(field, globalLongOptionFlags).split(", "),
1277
+ type: formatJsonHelpSchemaType(field.schema),
1278
+ ...(field.description === undefined ? {} : { description: field.description }),
1279
+ required: field.requiredWhenActive,
1280
+ ...(field.hasDefault ? { default: field.defaultValue } : {}),
1281
+ ...(field.positionalIndex === undefined ? {} : { positional: true })
1282
+ };
1283
+ }
1284
+ function formatJsonHelpSchemaType(schema) {
1285
+ if (schema.kind === "enum") {
1286
+ return "enum";
1287
+ }
1288
+ if (schema.kind === "array") {
1289
+ return "array";
1290
+ }
1291
+ if (schema.kind === "json") {
1292
+ return "json";
1293
+ }
1294
+ return schema.kind;
1295
+ }
1216
1296
  function renderHelpDocument(input) {
1217
1297
  const title = input.breadcrumb.filter((segment) => segment.length > 0).join(" ") || input.rootUsageName;
1218
1298
  const description = input.description ?? "";
@@ -1235,11 +1315,19 @@ function renderHelpDocument(input) {
1235
1315
  return `${lines.join("\n").trimEnd()}\n`;
1236
1316
  }
1237
1317
  async function renderGeneratedHelp(root, argv, options) {
1238
- const target = resolveHelpTarget(root, argv, "cli", options.rootDisplayName);
1239
1318
  const output = resolveHelpOutput(argv);
1240
1319
  const casing = options.casing ?? "kebab";
1241
1320
  const rootUsageName = options.rootUsageName ?? inferProgramName(argv);
1321
+ const target = resolveHelpTarget(root, argv, "cli", rootUsageName, options.rootDisplayName);
1242
1322
  const controls = resolveCLIControls(options.controls);
1323
+ if (output === "json") {
1324
+ process.stdout.write(renderJsonHelp(target, root, casing, {
1325
+ controls,
1326
+ showVersion: options.version !== undefined,
1327
+ presetsEnabled: options.presets === true
1328
+ }, rootUsageName));
1329
+ return;
1330
+ }
1243
1331
  await withOutputFormat(output, async () => {
1244
1332
  const rendered = target.node.kind === "group"
1245
1333
  ? renderGroupHelp(target.node, target.breadcrumb, "cli", casing, {
@@ -2082,6 +2170,16 @@ function renderCliErrorPattern(pattern) {
2082
2170
  process.exitCode = 1;
2083
2171
  return;
2084
2172
  }
2173
+ if (pattern.kind === "definition") {
2174
+ logger.error(`Command definition error: ${pattern.error.message}\n` +
2175
+ "This is a bug in the generated command definition, not in your command arguments.\n" +
2176
+ "Run with --debug for a stack trace.");
2177
+ if (pattern.debugStackMode !== undefined && pattern.error.stack) {
2178
+ process.stderr.write(`${formatDebugStack(pattern.error.stack, pattern.debugStackMode)}\n`);
2179
+ }
2180
+ process.exitCode = 1;
2181
+ return;
2182
+ }
2085
2183
  if (pattern.kind === "toolcraft-bug") {
2086
2184
  logger.error(`toolcraft hit an internal invariant: ${pattern.error.message}\n` +
2087
2185
  `This is a bug in toolcraft or in the command definition; ` +
@@ -2755,7 +2853,7 @@ function isHttpErrorLike(error) {
2755
2853
  if (!isPlainObject(error)) {
2756
2854
  return false;
2757
2855
  }
2758
- if (error.name !== "HttpError" || typeof error.message !== "string") {
2856
+ if (typeof error.name !== "string" || typeof error.message !== "string") {
2759
2857
  return false;
2760
2858
  }
2761
2859
  const request = error.request;
@@ -2902,6 +3000,7 @@ function formatHttpErrorSnippet(body) {
2902
3000
  }
2903
3001
  function renderHttpError(error, options) {
2904
3002
  const detailed = options.verbose || options.debugStackMode !== undefined;
3003
+ const summary = summarizeHttpError(error);
2905
3004
  const lines = [
2906
3005
  styleHttpErrorLine(`Request: ${error.request.method} ${error.request.url}`, text.muted)
2907
3006
  ];
@@ -2916,7 +3015,24 @@ function renderHttpError(error, options) {
2916
3015
  lines.push("", "Response headers:", ...formatHttpErrorHeaders(error.response.headers), "", "Response body:", indentHttpErrorBlock(formatHttpErrorBody(error.response.body)));
2917
3016
  }
2918
3017
  else {
2919
- lines.push("", `Response body: ${formatHttpErrorSnippet(error.response.body)}`, "Re-run with --verbose to see headers and full body.");
3018
+ const summaryLines = [
3019
+ summary.code === undefined ? undefined : `Code: ${summary.code}`,
3020
+ summary.message === undefined ? undefined : `Message: ${summary.message}`,
3021
+ summary.requestId === undefined ? undefined : `Request id: ${summary.requestId}`,
3022
+ summary.retryAfter === undefined ? undefined : `Retry after: ${summary.retryAfter}`,
3023
+ summary.hint === undefined ? undefined : `Hint: ${summary.hint}`
3024
+ ].filter((line) => line !== undefined);
3025
+ if (summary.fieldErrors !== undefined && summary.fieldErrors.length > 0) {
3026
+ summaryLines.push("", "Field errors:", ...summary.fieldErrors.map((fieldError) => ` ${fieldError.path}: ${fieldError.message}`));
3027
+ }
3028
+ lines.push("");
3029
+ if (summaryLines.length > 0) {
3030
+ lines.push(...summaryLines);
3031
+ }
3032
+ else {
3033
+ lines.push(`Response body: ${formatHttpErrorSnippet(error.response.body)}`);
3034
+ }
3035
+ lines.push("Re-run with --verbose to see headers and full body.");
2920
3036
  }
2921
3037
  process.stderr.write(`${lines.join("\n")}\n`);
2922
3038
  const stack = error instanceof Error ? error.stack : undefined;
@@ -2928,17 +3044,23 @@ async function handleRunError(error, options) {
2928
3044
  const logger = createLogger();
2929
3045
  await withOutputFormat(options.output, async () => {
2930
3046
  if (error instanceof UserError) {
2931
- renderCliErrorPattern(options.userErrorPattern === "usage"
3047
+ renderCliErrorPattern(options.userErrorPattern === "definition"
2932
3048
  ? {
2933
- kind: "usage",
2934
- message: error.message,
2935
- rootUsageName: options.rootUsageName,
2936
- commandPath: options.commandPath
3049
+ kind: "definition",
3050
+ error,
3051
+ debugStackMode: options.debugStackMode
2937
3052
  }
2938
- : {
2939
- kind: "runtime-user",
2940
- message: error.message
2941
- });
3053
+ : options.userErrorPattern === "usage"
3054
+ ? {
3055
+ kind: "usage",
3056
+ message: error.message,
3057
+ rootUsageName: options.rootUsageName,
3058
+ commandPath: options.commandPath
3059
+ }
3060
+ : {
3061
+ kind: "runtime-user",
3062
+ message: error.message
3063
+ });
2942
3064
  return;
2943
3065
  }
2944
3066
  if (error instanceof Error && error.name === "ToolcraftBugError") {
@@ -3190,77 +3312,82 @@ function configureCommanderSuggestionOutput(command) {
3190
3312
  export async function runCLI(roots, options = {}) {
3191
3313
  enableSourceMaps();
3192
3314
  const argv = [...(options.argv ?? process.argv)];
3193
- const normalizedRoot = normalizeRoots(roots, argv);
3194
- const root = options.approvals === true ? mergeApprovalsGroup(normalizedRoot) : normalizedRoot;
3195
- await resolveMcpProxies(root, { projectRoot: options.projectRoot });
3196
- const casing = options.casing ?? "kebab";
3197
- const services = (options.services ?? {});
3198
- const runtimeOptions = options.humanInLoop ?? {};
3199
- const runtimeFetch = options.fetch ?? globalThis.fetch;
3200
- const version = options.version ?? findEntrypointPackageMetadata(argv[1])?.version;
3201
3315
  const rootUsageName = options.rootUsageName ?? inferProgramName(argv);
3202
- const controls = resolveCLIControls(options.controls);
3203
- const servicesWithBuiltIns = {
3204
- ...services,
3205
- runtimeOptions,
3206
- root
3207
- };
3208
- const requirementOptions = {
3209
- apiVersion: options.apiVersion
3210
- };
3211
- validateServices(services);
3212
- if (hasHelpFlag(argv)) {
3213
- await renderGeneratedHelp(root, argv, { ...options, version });
3214
- return;
3215
- }
3216
- const program = new CommanderCommand();
3217
- program.name(root.name);
3218
- program.exitOverride();
3219
- program.showHelpAfterError();
3220
- program.addHelpCommand(false);
3221
- const presetsEnabled = options.presets === true;
3222
- const globalLongOptionFlags = getGlobalLongOptionFlags(presetsEnabled, version !== undefined, controls);
3223
- addGlobalOptions(program, presetsEnabled, controls);
3224
- if (version !== undefined) {
3225
- program.version(version, "--version");
3226
- }
3227
- Reflect.set(program, "_toolcraftReservedChildNames", root.children
3228
- .filter((child) => !isNodeVisibleInScope(child, "cli"))
3229
- .flatMap((child) => getNodeCommandNames(child)));
3230
3316
  let lastActionCommand;
3231
3317
  let resolvedCommandPath = "";
3318
+ let program;
3319
+ let version;
3320
+ let userErrorPattern = "definition";
3232
3321
  let errorReportContext;
3233
- const execute = async (state) => {
3234
- lastActionCommand = state.actionCommand;
3235
- resolvedCommandPath = formatCliCommandPath(state.commandPath);
3236
- await executeCommand(state, servicesWithBuiltIns, requirementOptions, runtimeFetch, runtimeOptions, (context) => {
3237
- errorReportContext = context;
3238
- });
3239
- };
3240
- const rootChildNames = new Set(root.children
3241
- .filter((candidate) => isNodeVisibleInScope(candidate, "cli"))
3242
- .map((candidate) => candidate.name));
3243
- for (const child of root.children) {
3244
- const command = createNodeCommand(child, casing, globalLongOptionFlags, execute, presetsEnabled, controls);
3245
- if (command === null) {
3246
- continue;
3247
- }
3248
- const isDefaultChild = root.default !== undefined &&
3249
- root.default.scope.includes("cli") &&
3250
- (command.name() === root.default.name || command.aliases().includes(root.default.name));
3251
- addCommanderChild(program, command, isDefaultChild, rootChildNames);
3252
- }
3253
- configureCommanderSuggestionOutput(program);
3254
- const unknownCommand = findUnknownCommanderCommand(program, argv);
3255
- if (unknownCommand !== undefined) {
3256
- createLogger().error(appendUsagePointer(formatUnknownCommandMessage(unknownCommand.input, unknownCommand.currentCommand), {
3257
- rootUsageName,
3258
- commandPath: unknownCommand.commandPath
3259
- }));
3260
- process.exitCode = 1;
3261
- return;
3262
- }
3263
3322
  try {
3323
+ const normalizedRoot = normalizeRoots(roots, argv);
3324
+ const root = options.approvals === true ? mergeApprovalsGroup(normalizedRoot) : normalizedRoot;
3325
+ await resolveMcpProxies(root, { projectRoot: options.projectRoot });
3326
+ const casing = options.casing ?? "kebab";
3327
+ const services = (options.services ?? {});
3328
+ const runtimeOptions = options.humanInLoop ?? {};
3329
+ const runtimeFetch = options.fetch ?? globalThis.fetch;
3330
+ version = options.version ?? findEntrypointPackageMetadata(argv[1])?.version;
3331
+ const controls = resolveCLIControls(options.controls);
3332
+ const servicesWithBuiltIns = {
3333
+ ...services,
3334
+ runtimeOptions,
3335
+ root
3336
+ };
3337
+ const requirementOptions = {
3338
+ apiVersion: options.apiVersion
3339
+ };
3340
+ validateServices(services);
3341
+ if (hasHelpFlag(argv)) {
3342
+ userErrorPattern = "usage";
3343
+ await renderGeneratedHelp(root, argv, { ...options, version });
3344
+ return;
3345
+ }
3346
+ program = new CommanderCommand();
3347
+ program.name(root.name);
3348
+ program.exitOverride();
3349
+ program.showHelpAfterError();
3350
+ program.addHelpCommand(false);
3351
+ const presetsEnabled = options.presets === true;
3352
+ const globalLongOptionFlags = getGlobalLongOptionFlags(presetsEnabled, version !== undefined, controls);
3353
+ addGlobalOptions(program, presetsEnabled, controls);
3354
+ if (version !== undefined) {
3355
+ program.version(version, "--version");
3356
+ }
3357
+ Reflect.set(program, "_toolcraftReservedChildNames", root.children
3358
+ .filter((child) => !isNodeVisibleInScope(child, "cli"))
3359
+ .flatMap((child) => getNodeCommandNames(child)));
3360
+ const execute = async (state) => {
3361
+ lastActionCommand = state.actionCommand;
3362
+ resolvedCommandPath = formatCliCommandPath(state.commandPath);
3363
+ await executeCommand(state, servicesWithBuiltIns, requirementOptions, runtimeFetch, runtimeOptions, (context) => {
3364
+ errorReportContext = context;
3365
+ });
3366
+ };
3367
+ const rootChildNames = new Set(root.children
3368
+ .filter((candidate) => isNodeVisibleInScope(candidate, "cli"))
3369
+ .map((candidate) => candidate.name));
3370
+ for (const child of root.children) {
3371
+ const command = createNodeCommand(child, casing, globalLongOptionFlags, execute, presetsEnabled, controls);
3372
+ if (command === null) {
3373
+ continue;
3374
+ }
3375
+ const isDefaultChild = root.default !== undefined &&
3376
+ root.default.scope.includes("cli") &&
3377
+ (command.name() === root.default.name || command.aliases().includes(root.default.name));
3378
+ addCommanderChild(program, command, isDefaultChild, rootChildNames);
3379
+ }
3380
+ configureCommanderSuggestionOutput(program);
3381
+ const unknownCommand = findUnknownCommanderCommand(program, argv);
3382
+ if (unknownCommand !== undefined) {
3383
+ createLogger().error(appendUsagePointer(formatUnknownCommandMessage(unknownCommand.input, unknownCommand.currentCommand), {
3384
+ rootUsageName,
3385
+ commandPath: unknownCommand.commandPath
3386
+ }));
3387
+ process.exitCode = 1;
3388
+ return;
3389
+ }
3390
+ userErrorPattern = "usage";
3264
3391
  await program.parseAsync(argv);
3265
3392
  }
3266
3393
  catch (error) {
@@ -3296,7 +3423,7 @@ export async function runCLI(roots, options = {}) {
3296
3423
  argv,
3297
3424
  rootUsageName,
3298
3425
  commandPath: resolvedCommandPath,
3299
- userErrorPattern: errorReportContext?.params === undefined ? "usage" : "runtime-user"
3426
+ userErrorPattern: errorReportContext?.params === undefined ? userErrorPattern : "runtime-user"
3300
3427
  });
3301
3428
  }
3302
3429
  }
@@ -14,11 +14,7 @@ const showParams = S.Object({
14
14
  approvalId: S.String()
15
15
  });
16
16
  const runParams = S.Object({
17
- approvalId: S.String(),
18
- dryRun: S.Optional(S.Boolean({
19
- description: "Preview the approval without prompting or executing it",
20
- scope: ["cli"]
21
- }))
17
+ approvalId: S.String()
22
18
  });
23
19
  export const approvalsGroup = markApprovalsBuiltIn(defineGroup({
24
20
  name: "approvals",
@@ -76,10 +72,6 @@ export const approvalsGroup = markApprovalsBuiltIn(defineGroup({
76
72
  scope: runScope,
77
73
  params: runParams,
78
74
  handler: async ({ params, runtimeOptions, root }) => {
79
- if (params.dryRun === true) {
80
- const { tasks } = await ensureApprovalList(runtimeOptions, { create: false });
81
- return tasks.get(params.approvalId);
82
- }
83
75
  return runApproval(params.approvalId, runtimeOptions, root);
84
76
  },
85
77
  render: {
@@ -8,7 +8,7 @@ const ignoredRoot = defineGroup({
8
8
  name: "usage",
9
9
  scope: ["mcp"],
10
10
  params: S.Object({
11
- dryRun: S.Boolean(),
11
+ preview: S.Boolean(),
12
12
  }),
13
13
  handler: async ({ params }) => params,
14
14
  }),
package/dist/mcp.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { access, lstat, readFile, rename, unlink, writeFile } from "node:fs/promises";
2
2
  import { createServer, JSON_RPC_ERROR_CODES, ToolError } from "tiny-stdio-mcp-server";
3
3
  import { toJsonSchema } from "toolcraft-schema";
4
+ import { createHttpErrorEnvelope, isHttpErrorLike } from "./api-error-summary.js";
4
5
  import { ToolcraftBugError, UserError, assertCommandRequirements, resolveCommandSecrets } from "./index.js";
5
6
  import { writeErrorReport } from "./error-report.js";
6
7
  import { mergeApprovalsGroup } from "./human-in-loop/approvals-commands.js";
@@ -730,10 +731,17 @@ function toToolContent(result) {
730
731
  const fallbackText = JSON.stringify(fallbackValue);
731
732
  return [{ type: "text", text: fallbackText }];
732
733
  }
733
- function toToolError(error) {
734
+ function toToolError(error, reportPath) {
734
735
  if (error instanceof ToolError) {
735
736
  return error;
736
737
  }
738
+ if (isHttpErrorLike(error)) {
739
+ const code = error.response.status >= 400 && error.response.status < 500
740
+ ? JSON_RPC_ERROR_CODES.INVALID_PARAMS
741
+ : JSON_RPC_ERROR_CODES.INTERNAL_ERROR;
742
+ const envelope = createHttpErrorEnvelope(error, reportPath);
743
+ return new ToolError(code, envelope.message, envelope);
744
+ }
737
745
  if (error instanceof UserError) {
738
746
  return new ToolError(JSON_RPC_ERROR_CODES.INVALID_PARAMS, error.message);
739
747
  }
@@ -798,7 +806,7 @@ function createResolvedMCPServer(root, options) {
798
806
  if (error instanceof ApprovalDeclinedError) {
799
807
  return renderDeclinedApproval(error);
800
808
  }
801
- await writeErrorReport({
809
+ const report = await writeErrorReport({
802
810
  command: tool.command,
803
811
  commandPath: tool.commandPath,
804
812
  env: process.env,
@@ -808,7 +816,7 @@ function createResolvedMCPServer(root, options) {
808
816
  projectRoot: options.projectRoot,
809
817
  secrets
810
818
  });
811
- throw toToolError(error);
819
+ throw toToolError(error, report?.displayPath);
812
820
  }
813
821
  };
814
822
  if (tool.outputSchema === undefined) {
@@ -100,7 +100,7 @@ export function formatColumns(opts) {
100
100
  return [`${firstIndent}${row.left}`];
101
101
  }
102
102
  const rightLines = wrapWords(row.right, rightWidth);
103
- if (row.left.length > leftWidth) {
103
+ if (row.left.length >= leftWidth) {
104
104
  return [
105
105
  `${firstIndent}${row.left}`,
106
106
  ...rightLines.map((line) => `${continuationIndent}${line}`)
@@ -159,7 +159,7 @@ export function formatColumns(opts) {
159
159
  return [`${firstIndent}${row.left}`];
160
160
  }
161
161
  const rightLines = wrapWords(row.right, rightWidth);
162
- if (visibleWidth(row.left) > leftWidth) {
162
+ if (visibleWidth(row.left) >= leftWidth) {
163
163
  return [
164
164
  `${firstIndent}${row.left}`,
165
165
  ...rightLines.map((line) => `${continuationIndent}${line}`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolcraft",
3
- "version": "0.0.60",
3
+ "version": "0.0.61",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -47,7 +47,7 @@
47
47
  "postpack": "node ../../scripts/manage-bundled-workspace-deps.mjs cleanup . toolcraft-design @poe-code/frontmatter @poe-code/agent-mcp-config @poe-code/agent-human-in-loop @poe-code/task-list @poe-code/agent-defs @poe-code/config-mutations @poe-code/process-runner tiny-mcp-client mcp-oauth auth-store"
48
48
  },
49
49
  "dependencies": {
50
- "toolcraft-schema": "0.0.60",
50
+ "toolcraft-schema": "0.0.61",
51
51
  "commander": "^14.0.3",
52
52
  "fast-string-width": "^3.0.2",
53
53
  "fast-wrap-ansi": "^0.2.0",