toolcraft 0.0.17 → 0.0.18

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.
Files changed (81) hide show
  1. package/dist/cli.d.ts +2 -0
  2. package/dist/cli.js +833 -124
  3. package/dist/error-report.d.ts +39 -0
  4. package/dist/error-report.js +330 -0
  5. package/dist/human-in-loop/approval-tasks.js +11 -8
  6. package/dist/human-in-loop/approvals-commands.js +21 -20
  7. package/dist/human-in-loop/default-provider.js +5 -3
  8. package/dist/human-in-loop/runner.js +45 -4
  9. package/dist/index.d.ts +2 -2
  10. package/dist/index.js +55 -35
  11. package/dist/json-schema-converter.d.ts +1 -0
  12. package/dist/json-schema-converter.js +102 -52
  13. package/dist/mcp-proxy.d.ts +1 -0
  14. package/dist/mcp-proxy.js +13 -6
  15. package/dist/mcp.d.ts +2 -0
  16. package/dist/mcp.js +131 -55
  17. package/dist/sdk.d.ts +4 -2
  18. package/dist/sdk.js +132 -48
  19. package/dist/source-snippet.d.ts +8 -0
  20. package/dist/source-snippet.js +42 -0
  21. package/dist/stack-trim.d.ts +4 -0
  22. package/dist/stack-trim.js +70 -0
  23. package/dist/suggest.d.ts +4 -0
  24. package/dist/suggest.js +46 -0
  25. package/dist/user-error.d.ts +3 -0
  26. package/dist/user-error.js +7 -1
  27. package/dist/validation-errors.d.ts +5 -0
  28. package/dist/validation-errors.js +18 -0
  29. package/node_modules/@poe-code/design-system/dist/components/help-formatter-plain.d.ts +1 -0
  30. package/node_modules/@poe-code/design-system/dist/components/help-formatter-plain.js +1 -1
  31. package/node_modules/@poe-code/design-system/dist/components/text.d.ts +1 -0
  32. package/node_modules/@poe-code/design-system/dist/components/text.js +8 -0
  33. package/node_modules/@poe-code/design-system/dist/dashboard/buffer.js +8 -1
  34. package/node_modules/@poe-code/design-system/dist/dashboard/keymap.d.ts +5 -0
  35. package/node_modules/@poe-code/design-system/dist/dashboard/keymap.js +146 -12
  36. package/node_modules/@poe-code/design-system/dist/dashboard/terminal.js +31 -0
  37. package/node_modules/@poe-code/design-system/dist/dashboard/types.d.ts +1 -0
  38. package/node_modules/@poe-code/design-system/dist/explorer/actions.d.ts +16 -0
  39. package/node_modules/@poe-code/design-system/dist/explorer/actions.js +39 -0
  40. package/node_modules/@poe-code/design-system/dist/explorer/demo.d.ts +13 -0
  41. package/node_modules/@poe-code/design-system/dist/explorer/demo.js +297 -0
  42. package/node_modules/@poe-code/design-system/dist/explorer/events.d.ts +61 -0
  43. package/node_modules/@poe-code/design-system/dist/explorer/events.js +1 -0
  44. package/node_modules/@poe-code/design-system/dist/explorer/filter.d.ts +10 -0
  45. package/node_modules/@poe-code/design-system/dist/explorer/filter.js +95 -0
  46. package/node_modules/@poe-code/design-system/dist/explorer/index.d.ts +8 -0
  47. package/node_modules/@poe-code/design-system/dist/explorer/index.js +8 -0
  48. package/node_modules/@poe-code/design-system/dist/explorer/jobs.d.ts +7 -0
  49. package/node_modules/@poe-code/design-system/dist/explorer/jobs.js +59 -0
  50. package/node_modules/@poe-code/design-system/dist/explorer/keymap.d.ts +21 -0
  51. package/node_modules/@poe-code/design-system/dist/explorer/keymap.js +363 -0
  52. package/node_modules/@poe-code/design-system/dist/explorer/layout.d.ts +20 -0
  53. package/node_modules/@poe-code/design-system/dist/explorer/layout.js +73 -0
  54. package/node_modules/@poe-code/design-system/dist/explorer/reducer.d.ts +9 -0
  55. package/node_modules/@poe-code/design-system/dist/explorer/reducer.js +704 -0
  56. package/node_modules/@poe-code/design-system/dist/explorer/render/detail.d.ts +4 -0
  57. package/node_modules/@poe-code/design-system/dist/explorer/render/detail.js +96 -0
  58. package/node_modules/@poe-code/design-system/dist/explorer/render/footer.d.ts +4 -0
  59. package/node_modules/@poe-code/design-system/dist/explorer/render/footer.js +49 -0
  60. package/node_modules/@poe-code/design-system/dist/explorer/render/header.d.ts +4 -0
  61. package/node_modules/@poe-code/design-system/dist/explorer/render/header.js +56 -0
  62. package/node_modules/@poe-code/design-system/dist/explorer/render/index.d.ts +8 -0
  63. package/node_modules/@poe-code/design-system/dist/explorer/render/index.js +61 -0
  64. package/node_modules/@poe-code/design-system/dist/explorer/render/list.d.ts +4 -0
  65. package/node_modules/@poe-code/design-system/dist/explorer/render/list.js +106 -0
  66. package/node_modules/@poe-code/design-system/dist/explorer/render/modal.d.ts +3 -0
  67. package/node_modules/@poe-code/design-system/dist/explorer/render/modal.js +91 -0
  68. package/node_modules/@poe-code/design-system/dist/explorer/render/test-fixtures.d.ts +8 -0
  69. package/node_modules/@poe-code/design-system/dist/explorer/render/test-fixtures.js +156 -0
  70. package/node_modules/@poe-code/design-system/dist/explorer/runtime.d.ts +2 -0
  71. package/node_modules/@poe-code/design-system/dist/explorer/runtime.js +282 -0
  72. package/node_modules/@poe-code/design-system/dist/explorer/runtime.test-helpers.d.ts +50 -0
  73. package/node_modules/@poe-code/design-system/dist/explorer/runtime.test-helpers.js +101 -0
  74. package/node_modules/@poe-code/design-system/dist/explorer/state.d.ts +130 -0
  75. package/node_modules/@poe-code/design-system/dist/explorer/state.js +87 -0
  76. package/node_modules/@poe-code/design-system/dist/explorer/theme.d.ts +27 -0
  77. package/node_modules/@poe-code/design-system/dist/explorer/theme.js +97 -0
  78. package/node_modules/@poe-code/design-system/dist/index.d.ts +3 -0
  79. package/node_modules/@poe-code/design-system/dist/index.js +3 -0
  80. package/node_modules/@poe-code/design-system/package.json +1 -0
  81. package/package.json +6 -2
@@ -0,0 +1,39 @@
1
+ import type { Command } from "./index.js";
2
+ export type ErrorReportsOption = boolean | {
3
+ dir?: string;
4
+ };
5
+ export interface ErrorReportContext {
6
+ argv?: readonly string[];
7
+ command?: Command<any, any, any, any>;
8
+ commandPath?: string;
9
+ env?: Record<string, string | undefined>;
10
+ error: unknown;
11
+ errorReports?: ErrorReportsOption;
12
+ params?: unknown;
13
+ projectRoot?: string;
14
+ secrets?: Record<string, string | undefined>;
15
+ version?: string;
16
+ }
17
+ export interface ErrorReportResult {
18
+ absolutePath: string;
19
+ displayPath: string;
20
+ }
21
+ interface HttpErrorLike {
22
+ name: "HttpError";
23
+ message: string;
24
+ request: {
25
+ method: string;
26
+ url: string;
27
+ headers: Record<string, string>;
28
+ body?: unknown;
29
+ };
30
+ response: {
31
+ status: number;
32
+ statusText: string;
33
+ headers: Record<string, string>;
34
+ body: unknown;
35
+ };
36
+ }
37
+ declare function hasHttpContext(error: unknown): error is HttpErrorLike;
38
+ export declare function writeErrorReport(context: ErrorReportContext): Promise<ErrorReportResult | undefined>;
39
+ export { hasHttpContext };
@@ -0,0 +1,330 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { CommanderError } from "commander";
5
+ import { ApprovalDeclinedError } from "./human-in-loop/types.js";
6
+ import { findProjectRoot } from "./mcp-proxy.js";
7
+ import { findPackageMetadata } from "./package-metadata.js";
8
+ import { UserError } from "./user-error.js";
9
+ const ERROR_REPORTS_ENV = "TOOLCRAFT_ERROR_REPORTS";
10
+ const DEFAULT_SENSITIVE_NAMES = ["password", "token", "apikey", "secret"];
11
+ function isPlainObject(value) {
12
+ return typeof value === "object" && value !== null && !Array.isArray(value);
13
+ }
14
+ function unwrapOptional(schema) {
15
+ if (schema.kind === "optional") {
16
+ return unwrapOptional(schema.inner);
17
+ }
18
+ return schema;
19
+ }
20
+ function hasHttpContext(error) {
21
+ return (error instanceof Error &&
22
+ error.name === "HttpError" &&
23
+ isPlainObject(error.request) &&
24
+ isPlainObject(error.response));
25
+ }
26
+ function isSkippedError(error) {
27
+ if (error instanceof ApprovalDeclinedError) {
28
+ return true;
29
+ }
30
+ if (error instanceof CommanderError &&
31
+ (error.code === "commander.helpDisplayed" || error.code === "commander.version")) {
32
+ return true;
33
+ }
34
+ return error instanceof UserError && error.cause === undefined && !hasHttpContext(error);
35
+ }
36
+ function reportsEnabled(option, env) {
37
+ if (env[ERROR_REPORTS_ENV] === "1") {
38
+ return true;
39
+ }
40
+ return option !== undefined && option !== false;
41
+ }
42
+ function resolveReportDir(option, projectRoot) {
43
+ const configuredDir = typeof option === "object" ? option.dir : undefined;
44
+ if (configuredDir === undefined || configuredDir.length === 0) {
45
+ return path.join(projectRoot, ".toolcraft", "errors");
46
+ }
47
+ return path.isAbsolute(configuredDir) ? configuredDir : path.join(projectRoot, configuredDir);
48
+ }
49
+ function resolveProjectRoot(projectRoot) {
50
+ if (projectRoot !== undefined) {
51
+ return projectRoot;
52
+ }
53
+ return findProjectRoot() ?? os.tmpdir();
54
+ }
55
+ function formatTimestamp(date) {
56
+ const isoMinute = date.toISOString().slice(0, 16);
57
+ const colonIndex = isoMinute.indexOf(":");
58
+ if (colonIndex === -1) {
59
+ return isoMinute;
60
+ }
61
+ return `${isoMinute.slice(0, colonIndex)}${isoMinute.slice(colonIndex + 1)}`;
62
+ }
63
+ function slugifyCommandPath(commandPath) {
64
+ const source = commandPath === undefined || commandPath.length === 0 ? "root" : commandPath;
65
+ let output = "";
66
+ let previousWasDash = false;
67
+ for (const char of source) {
68
+ const lower = char.toLowerCase();
69
+ const isWord = (lower >= "a" && lower <= "z") || (lower >= "0" && lower <= "9");
70
+ if (isWord) {
71
+ output += lower;
72
+ previousWasDash = false;
73
+ continue;
74
+ }
75
+ if (!previousWasDash) {
76
+ output += "-";
77
+ previousWasDash = true;
78
+ }
79
+ }
80
+ while (output.startsWith("-")) {
81
+ output = output.slice(1);
82
+ }
83
+ while (output.endsWith("-")) {
84
+ output = output.slice(0, -1);
85
+ }
86
+ return output.length === 0 ? "root" : output;
87
+ }
88
+ function relativeDisplayPath(projectRoot, absolutePath) {
89
+ const relative = path.relative(projectRoot, absolutePath);
90
+ return relative.length === 0 || relative.startsWith("..") ? absolutePath : relative;
91
+ }
92
+ function redactValue(value) {
93
+ if (value === undefined) {
94
+ return "<unset>";
95
+ }
96
+ return `<set, ${value.length} chars>`;
97
+ }
98
+ function isSensitiveName(name) {
99
+ const normalized = name.toLowerCase();
100
+ return DEFAULT_SENSITIVE_NAMES.some((candidate) => normalized.includes(candidate));
101
+ }
102
+ function schemaSecretValue(schema) {
103
+ const unwrapped = unwrapOptional(schema);
104
+ if (unwrapped.kind === "string" || unwrapped.kind === "number") {
105
+ return unwrapped.secret;
106
+ }
107
+ return undefined;
108
+ }
109
+ function shouldRedactParam(name, schema) {
110
+ const secret = schemaSecretValue(schema);
111
+ if (secret !== undefined) {
112
+ return secret;
113
+ }
114
+ return isSensitiveName(name);
115
+ }
116
+ function redactParamsValue(value, schema, name) {
117
+ if (shouldRedactParam(name, schema)) {
118
+ return "<redacted>";
119
+ }
120
+ const unwrapped = unwrapOptional(schema);
121
+ if (unwrapped.kind === "object" && isPlainObject(value)) {
122
+ return Object.fromEntries(Object.entries(value).map(([key, childValue]) => {
123
+ const childSchema = unwrapped.shape[key];
124
+ return [
125
+ key,
126
+ childSchema === undefined ? childValue : redactParamsValue(childValue, childSchema, key)
127
+ ];
128
+ }));
129
+ }
130
+ if (unwrapped.kind === "array" && Array.isArray(value)) {
131
+ return value.map((entry) => redactParamsValue(entry, unwrapped.item, name));
132
+ }
133
+ return value;
134
+ }
135
+ function redactParams(params, command) {
136
+ if (command === undefined) {
137
+ return params;
138
+ }
139
+ return redactParamsValue(params, command.params, "");
140
+ }
141
+ function commandSecretEnvNames(secrets) {
142
+ if (secrets === undefined) {
143
+ return [];
144
+ }
145
+ return Object.values(secrets).map((secret) => secret.env);
146
+ }
147
+ function redactArgv(argv, options) {
148
+ if (argv === undefined) {
149
+ return [];
150
+ }
151
+ const secretValues = new Set(Object.values(options.secrets ?? {}).filter((value) => value !== undefined && value.length > 0));
152
+ const secretNames = new Set([
153
+ ...Object.keys(options.secrets ?? {}),
154
+ ...commandSecretEnvNames(options.command?.secrets)
155
+ ]);
156
+ const output = [];
157
+ let redactNext = false;
158
+ for (const arg of argv) {
159
+ if (redactNext) {
160
+ output.push("<redacted>");
161
+ redactNext = false;
162
+ continue;
163
+ }
164
+ const equalsIndex = arg.indexOf("=");
165
+ const optionName = equalsIndex === -1 ? arg : arg.slice(0, equalsIndex);
166
+ const normalizedOptionName = optionName.replaceAll("-", "");
167
+ const sensitiveByName = isSensitiveName(normalizedOptionName) ||
168
+ [...secretNames].some((name) => normalizedOptionName.toLowerCase().includes(name.toLowerCase()));
169
+ if (equalsIndex !== -1 && sensitiveByName) {
170
+ output.push(`${optionName}=<redacted>`);
171
+ continue;
172
+ }
173
+ if (arg.startsWith("-") && sensitiveByName) {
174
+ output.push(arg);
175
+ redactNext = true;
176
+ continue;
177
+ }
178
+ let redactedArg = arg;
179
+ for (const secretValue of secretValues) {
180
+ redactedArg = redactedArg.split(secretValue).join("<redacted>");
181
+ }
182
+ output.push(redactedArg);
183
+ }
184
+ return output;
185
+ }
186
+ function stableJson(value) {
187
+ return JSON.stringify(value, null, 2) ?? "undefined";
188
+ }
189
+ function redactStructuredErrorField(name, value) {
190
+ if (typeof value === "string" && name.toLowerCase() === "authorization") {
191
+ return "Bearer ****";
192
+ }
193
+ if (Array.isArray(value)) {
194
+ return value.map((entry) => redactStructuredErrorField(name, entry));
195
+ }
196
+ if (isPlainObject(value)) {
197
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, redactStructuredErrorField(key, entry)]));
198
+ }
199
+ return value;
200
+ }
201
+ function ownStructuredFields(error) {
202
+ const fields = {};
203
+ for (const key of Object.keys(error)) {
204
+ if (key === "name" || key === "message" || key === "stack" || key === "cause") {
205
+ continue;
206
+ }
207
+ fields[key] = redactStructuredErrorField(key, error[key]);
208
+ }
209
+ return fields;
210
+ }
211
+ function formatStackChain(error) {
212
+ const lines = [];
213
+ let current = error;
214
+ let index = 0;
215
+ while (current !== undefined) {
216
+ if (current instanceof Error) {
217
+ lines.push(index === 0
218
+ ? (current.stack ?? String(current))
219
+ : `Caused by: ${current.stack ?? String(current)}`);
220
+ current = current.cause;
221
+ }
222
+ else {
223
+ lines.push(index === 0 ? String(current) : `Caused by: ${String(current)}`);
224
+ current = undefined;
225
+ }
226
+ index += 1;
227
+ }
228
+ return lines.join("\n");
229
+ }
230
+ function formatHeaderValue(name, value) {
231
+ return name.toLowerCase() === "authorization" ? "Bearer ****" : value;
232
+ }
233
+ function formatHeaders(headers) {
234
+ return Object.entries(headers)
235
+ .map(([name, value]) => `${name}: ${formatHeaderValue(name, value)}`)
236
+ .join("\n");
237
+ }
238
+ function formatBody(body) {
239
+ if (typeof body === "string") {
240
+ return body;
241
+ }
242
+ return stableJson(body);
243
+ }
244
+ function formatHttpTranscript(error) {
245
+ const requestLines = [
246
+ `${error.request.method} ${error.request.url}`,
247
+ formatHeaders(error.request.headers)
248
+ ].filter((line) => line.length > 0);
249
+ if (error.request.body !== undefined) {
250
+ requestLines.push("", formatBody(error.request.body));
251
+ }
252
+ return [
253
+ "Request:",
254
+ ...requestLines,
255
+ "",
256
+ "Response:",
257
+ `${error.response.status} ${error.response.statusText}`,
258
+ formatHeaders(error.response.headers),
259
+ "",
260
+ formatBody(error.response.body)
261
+ ].join("\n");
262
+ }
263
+ function resolveToolcraftVersion(version) {
264
+ return (version ??
265
+ findPackageMetadata(new URL("./error-report.ts", import.meta.url))?.version ??
266
+ "unknown");
267
+ }
268
+ function buildReport(context) {
269
+ const env = context.env ?? process.env;
270
+ const error = context.error;
271
+ const errorName = error instanceof Error ? error.name : typeof error;
272
+ const errorMessage = error instanceof Error ? error.message : String(error);
273
+ const structuredFields = error instanceof Error ? ownStructuredFields(error) : {};
274
+ const secretLines = Object.entries(context.command?.secrets ?? {}).map(([name, secret]) => {
275
+ const value = context.secrets?.[name] ?? env[secret.env];
276
+ return `${secret.env}=${redactValue(value)}`;
277
+ });
278
+ const lines = [
279
+ "Toolcraft Error Report",
280
+ "",
281
+ "Runtime",
282
+ `toolcraft version: ${resolveToolcraftVersion(context.version)}`,
283
+ `node version: ${process.version}`,
284
+ `platform: ${process.platform} ${process.arch}`,
285
+ "",
286
+ "Argv",
287
+ stableJson(redactArgv(context.argv, { command: context.command, secrets: context.secrets })),
288
+ "",
289
+ "Resolved Secrets",
290
+ ...(secretLines.length === 0 ? ["<none>"] : secretLines),
291
+ "",
292
+ "Command Path",
293
+ context.commandPath === undefined || context.commandPath.length === 0
294
+ ? "root"
295
+ : context.commandPath,
296
+ "",
297
+ "Parsed Params",
298
+ stableJson(redactParams(context.params, context.command)),
299
+ "",
300
+ "Error",
301
+ `name: ${errorName}`,
302
+ `message: ${errorMessage}`,
303
+ "structured fields:",
304
+ stableJson(structuredFields),
305
+ "",
306
+ "Stack",
307
+ formatStackChain(error)
308
+ ];
309
+ if (hasHttpContext(error)) {
310
+ lines.push("", "HTTP Transcript", formatHttpTranscript(error));
311
+ }
312
+ return `${lines.join("\n")}\n`;
313
+ }
314
+ export async function writeErrorReport(context) {
315
+ const env = context.env ?? process.env;
316
+ if (!reportsEnabled(context.errorReports, env) || isSkippedError(context.error)) {
317
+ return undefined;
318
+ }
319
+ const projectRoot = resolveProjectRoot(context.projectRoot);
320
+ const reportDir = resolveReportDir(context.errorReports, projectRoot);
321
+ const fileName = `${formatTimestamp(new Date())}-${slugifyCommandPath(context.commandPath)}.log`;
322
+ const absolutePath = path.join(reportDir, fileName);
323
+ await mkdir(reportDir, { recursive: true });
324
+ await writeFile(absolutePath, buildReport(context));
325
+ return {
326
+ absolutePath,
327
+ displayPath: relativeDisplayPath(projectRoot, absolutePath)
328
+ };
329
+ }
330
+ export { hasHttpContext };
@@ -14,14 +14,14 @@ export async function ensureApprovalList(runtimeOptions, deps = {}) {
14
14
  const tasks = taskList.list(listName);
15
15
  if (!isListValidated(runtimeOptions, listName)) {
16
16
  if (!isApprovalStateMachine(tasks.stateMachine)) {
17
- throw new UserError("approvals task-list configured with a different state machine; pass approvalStateMachine when opening the list");
17
+ throw new UserError(`Approvals task list was created with a different version of toolcraft. Delete the task list directory (${getTaskListDirectory(runtimeOptions.taskList)}) or pass a matching approvalStateMachine.`);
18
18
  }
19
19
  cacheValidatedList(runtimeOptions, listName);
20
20
  }
21
21
  return {
22
22
  taskList,
23
23
  listName,
24
- tasks,
24
+ tasks
25
25
  };
26
26
  }
27
27
  export async function enqueueApproval(ctx) {
@@ -64,7 +64,7 @@ async function resolveTaskList(runtimeOptions, taskList, openTaskListFn) {
64
64
  create: true,
65
65
  type: taskList.format,
66
66
  path: taskList.dir,
67
- stateMachine: approvalStateMachine,
67
+ stateMachine: approvalStateMachine
68
68
  });
69
69
  openedTaskListsByRuntime.set(runtimeOptions, openedTaskList);
70
70
  return openedTaskList;
@@ -95,21 +95,21 @@ function createApprovalRecord(payload, enqueuedAt) {
95
95
  enqueuedAt,
96
96
  pid: null,
97
97
  result: null,
98
- error: null,
98
+ error: null
99
99
  },
100
100
  pending: {
101
101
  status: "pending-approval",
102
102
  approvalId,
103
103
  message: payload.message,
104
- enqueuedAt,
105
- },
104
+ enqueuedAt
105
+ }
106
106
  };
107
107
  }
108
108
  async function createApprovalTask(tasks, approval) {
109
109
  await tasks.create({
110
110
  id: approval.approvalId,
111
111
  name: approval.name,
112
- metadata: approval.metadata,
112
+ metadata: approval.metadata
113
113
  });
114
114
  }
115
115
  function isApprovalStateMachine(stateMachine) {
@@ -121,6 +121,9 @@ function isApprovalStateMachine(stateMachine) {
121
121
  function isTaskListConfig(taskList) {
122
122
  return taskList !== undefined && "dir" in taskList;
123
123
  }
124
+ function getTaskListDirectory(taskList) {
125
+ return isTaskListConfig(taskList) ? taskList.dir : "unknown";
126
+ }
124
127
  function isDeepEqualStateMachine(left, right) {
125
128
  if (!areEqualStrings(left.states, right.states)) {
126
129
  return false;
@@ -196,6 +199,6 @@ function approvalPayloadFromTask(task) {
196
199
  enqueuedAt: metadata.enqueuedAt,
197
200
  pid: typeof metadata.pid === "number" || metadata.pid === null ? metadata.pid : undefined,
198
201
  result: metadata.result,
199
- error: metadata.error,
202
+ error: metadata.error
200
203
  };
201
204
  }
@@ -6,10 +6,10 @@ const approvalsGroupSymbol = Symbol("toolcraft.humanInLoop.approvalsBuiltIn");
6
6
  const listScope = ["cli", "mcp", "sdk"];
7
7
  const runScope = ["cli"];
8
8
  const listParams = S.Object({
9
- state: S.Optional(S.String()),
9
+ state: S.Optional(S.String())
10
10
  });
11
11
  const showParams = S.Object({
12
- approvalId: S.String(),
12
+ approvalId: S.String()
13
13
  });
14
14
  export const approvalsGroup = markApprovalsBuiltIn(defineGroup({
15
15
  name: "approvals",
@@ -27,8 +27,8 @@ export const approvalsGroup = markApprovalsBuiltIn(defineGroup({
27
27
  render: {
28
28
  rich: (result, primitives) => renderApprovalList(result, primitives),
29
29
  markdown: (result) => renderApprovalListMarkdown(result),
30
- json: (result) => result,
31
- },
30
+ json: (result) => result
31
+ }
32
32
  }),
33
33
  defineCommand({
34
34
  name: "show",
@@ -42,17 +42,17 @@ export const approvalsGroup = markApprovalsBuiltIn(defineGroup({
42
42
  render: {
43
43
  rich: (result, primitives) => renderApprovalDetails(result, primitives),
44
44
  markdown: (result) => renderApprovalDetailsMarkdown(result),
45
- json: (result) => result,
46
- },
45
+ json: (result) => result
46
+ }
47
47
  }),
48
48
  defineCommand({
49
49
  name: "run",
50
50
  description: "Run one queued approval.",
51
51
  scope: runScope,
52
52
  params: showParams,
53
- handler: async ({ params, runtimeOptions, root }) => runApproval(params.approvalId, runtimeOptions, root),
54
- }),
55
- ],
53
+ handler: async ({ params, runtimeOptions, root }) => runApproval(params.approvalId, runtimeOptions, root)
54
+ })
55
+ ]
56
56
  }));
57
57
  export function mergeApprovalsGroup(root) {
58
58
  const existing = root.children.find((child) => child.name === approvalsGroup.name);
@@ -60,7 +60,7 @@ export function mergeApprovalsGroup(root) {
60
60
  if (isApprovalsBuiltIn(existing)) {
61
61
  return root;
62
62
  }
63
- throw new UserError("Error: 'approvals' is reserved for human-in-loop built-ins");
63
+ throw new UserError("'approvals' is reserved for human-in-loop built-ins");
64
64
  }
65
65
  root.children = [...root.children, approvalsGroup];
66
66
  return root;
@@ -70,12 +70,13 @@ function markApprovalsBuiltIn(group) {
70
70
  configurable: false,
71
71
  enumerable: false,
72
72
  value: true,
73
- writable: false,
73
+ writable: false
74
74
  });
75
75
  return group;
76
76
  }
77
77
  function isApprovalsBuiltIn(node) {
78
- return node.kind === "group" && node[approvalsGroupSymbol] === true;
78
+ return (node.kind === "group" &&
79
+ node[approvalsGroupSymbol] === true);
79
80
  }
80
81
  async function loadApprovals(tasks, stateFilter) {
81
82
  const states = splitStateFilter(stateFilter);
@@ -86,7 +87,7 @@ async function loadApprovals(tasks, stateFilter) {
86
87
  const approvals = [];
87
88
  for (const state of states) {
88
89
  const matching = await tasks.all({
89
- state,
90
+ state
90
91
  });
91
92
  for (const task of matching) {
92
93
  if (seenIds.has(task.qualifiedId)) {
@@ -124,13 +125,13 @@ function renderApprovalList(result, { logger, renderTable, getTheme }) {
124
125
  columns: [
125
126
  { name: "id", title: "ID", alignment: "left", maxLen: 24 },
126
127
  { name: "state", title: "State", alignment: "left", maxLen: 18 },
127
- { name: "name", title: "Name", alignment: "left", maxLen: 60 },
128
+ { name: "name", title: "Name", alignment: "left", maxLen: 60 }
128
129
  ],
129
130
  rows: result.map((task) => ({
130
131
  id: task.id,
131
132
  state: task.state,
132
- name: task.name,
133
- })),
133
+ name: task.name
134
+ }))
134
135
  }));
135
136
  }
136
137
  function renderApprovalListMarkdown(result) {
@@ -148,12 +149,12 @@ function renderApprovalDetails(result, { logger, renderTable, getTheme }) {
148
149
  theme: getTheme(),
149
150
  columns: [
150
151
  { name: "key", title: "Key", alignment: "left", maxLen: 18 },
151
- { name: "value", title: "Value", alignment: "left", maxLen: 80 },
152
+ { name: "value", title: "Value", alignment: "left", maxLen: 80 }
152
153
  ],
153
154
  rows: Object.entries(taskToRecord(result)).map(([key, value]) => ({
154
155
  key,
155
- value: stringifyValue(value),
156
- })),
156
+ value: stringifyValue(value)
157
+ }))
157
158
  }));
158
159
  }
159
160
  function renderApprovalDetailsMarkdown(result) {
@@ -169,7 +170,7 @@ function taskToRecord(task) {
169
170
  name: task.name,
170
171
  state: task.state,
171
172
  description: task.description,
172
- metadata: task.metadata,
173
+ metadata: task.metadata
173
174
  };
174
175
  }
175
176
  function stringifyValue(value) {
@@ -5,8 +5,8 @@ function noProviderConfigured() {
5
5
  return {
6
6
  id: "noProviderConfigured",
7
7
  async requestApproval() {
8
- throw new UserError("no human-in-loop provider configured for this platform pass humanInLoop.provider to the runtime");
9
- },
8
+ throw new UserError("No human-in-loop provider is configured. Pass {humanInLoop: {provider: ...}} to runCLI / createMCPServer / createSDK, or run on macOS to use the default osascript provider.");
9
+ }
10
10
  };
11
11
  }
12
12
  function createDefaultProviderFactory() {
@@ -16,7 +16,9 @@ function createDefaultProviderFactory() {
16
16
  return provider;
17
17
  }
18
18
  provider =
19
- process.platform === "darwin" ? osascriptProvider({ title: "Approval needed" }) : noProviderConfigured();
19
+ process.platform === "darwin"
20
+ ? osascriptProvider({ title: "Approval needed" })
21
+ : noProviderConfigured();
20
22
  return provider;
21
23
  };
22
24
  }
@@ -2,6 +2,7 @@ import { access, readFile, writeFile } from "node:fs/promises";
2
2
  import { UserError, resolveCommandSecrets } from "../index.js";
3
3
  import { ensureApprovalList } from "./approval-tasks.js";
4
4
  import { resolveProvider } from "./gate.js";
5
+ const MAX_AVAILABLE_COMMAND_PATHS = 20;
5
6
  export async function runApproval(approvalId, runtimeOptions, root) {
6
7
  const { tasks } = await ensureApprovalList(runtimeOptions);
7
8
  const task = await tasks.get(approvalId);
@@ -95,25 +96,65 @@ function readApprovalPayload(task) {
95
96
  }
96
97
  function findCommand(root, commandPath) {
97
98
  const pathSegments = commandPath.split(".").filter((segment) => segment.length > 0);
99
+ const unknownCommandPathError = () => new UserError(`Unknown approval command path "${commandPath}". ${formatAvailableApprovalCommandPaths(root)}`);
98
100
  if (pathSegments.length === 0) {
99
- throw new UserError(`Unknown approval command path "${commandPath}".`);
101
+ throw unknownCommandPathError();
100
102
  }
101
103
  let current = root;
102
104
  for (const segment of pathSegments) {
103
105
  if (current.kind !== "group") {
104
- throw new UserError(`Unknown approval command path "${commandPath}".`);
106
+ throw unknownCommandPathError();
105
107
  }
106
108
  const next = current.children.find((child) => child.name === segment);
107
109
  if (next === undefined) {
108
- throw new UserError(`Unknown approval command path "${commandPath}".`);
110
+ throw unknownCommandPathError();
109
111
  }
110
112
  current = next;
111
113
  }
112
114
  if (current.kind !== "command") {
113
- throw new UserError(`Unknown approval command path "${commandPath}".`);
115
+ throw unknownCommandPathError();
114
116
  }
115
117
  return current;
116
118
  }
119
+ function formatAvailableApprovalCommandPaths(root) {
120
+ const paths = enumerateApprovalCommandPaths(root);
121
+ const visiblePaths = paths.slice(0, MAX_AVAILABLE_COMMAND_PATHS);
122
+ const remaining = paths.length - visiblePaths.length;
123
+ const suffix = remaining > 0 ? `, … and ${remaining} more` : "";
124
+ return `Available: ${visiblePaths.join(", ")}${suffix}.`;
125
+ }
126
+ function enumerateApprovalCommandPaths(root) {
127
+ const paths = [];
128
+ const visit = (node, path) => {
129
+ if (node.kind === "command") {
130
+ paths.push(path.join("."));
131
+ return;
132
+ }
133
+ for (const child of getVisibleCliChildren(node)) {
134
+ visit(child, [...path, child.name]);
135
+ }
136
+ };
137
+ if (root.kind === "command") {
138
+ visit(root, [root.name]);
139
+ return paths.sort();
140
+ }
141
+ for (const child of getVisibleCliChildren(root)) {
142
+ visit(child, [child.name]);
143
+ }
144
+ return paths.sort();
145
+ }
146
+ function isNodeVisibleInCli(node) {
147
+ if (node.kind === "command") {
148
+ return node.scope.includes("cli");
149
+ }
150
+ return (getVisibleCliChildren(node).length > 0 ||
151
+ Boolean(node.default && node.default.scope.includes("cli")) ||
152
+ node.scope === undefined ||
153
+ node.scope.includes("cli"));
154
+ }
155
+ function getVisibleCliChildren(root) {
156
+ return root.kind === "group" ? root.children.filter(isNodeVisibleInCli) : [];
157
+ }
117
158
  function createHandlerContext(command, params) {
118
159
  return {
119
160
  params,
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@ import type { ObjectSchema, Static } from "toolcraft-schema";
3
3
  import type { LoggerOutput, RenderTableOptions, ThemePalette } from "@poe-code/design-system";
4
4
  import { ApprovalDeclinedError } from "./human-in-loop/types.js";
5
5
  import type { HumanInLoopConfig, HumanInLoopPending, HumanInLoopRuntimeOptions } from "./human-in-loop/types.js";
6
- import { UserError } from "./user-error.js";
6
+ import { ToolcraftBugError, UserError } from "./user-error.js";
7
7
  type ScopeValue = "cli" | "mcp" | "sdk";
8
8
  type AnyObjectSchema = ObjectSchema<Record<string, never>>;
9
9
  type EmptyServices = Record<string, never>;
@@ -178,7 +178,7 @@ export declare function defineGroup<TServices extends object = EmptyServices, TN
178
178
  }): Group<TServices> & TypedGroupMetadata<TServices, TName, TChildren, TOwnScope, ResolveOwnHumanInLoopMode<TOwnHumanInLoop>>;
179
179
  export declare function getCommandSourcePath(command: Command<any, any, any, any>): string | undefined;
180
180
  export { S, toJsonSchema } from "toolcraft-schema";
181
- export { ApprovalDeclinedError, UserError };
181
+ export { ApprovalDeclinedError, ToolcraftBugError, UserError };
182
182
  export { findPackageMetadata, packageMetadata } from "./package-metadata.js";
183
183
  export type { PackageMetadata } from "./package-metadata.js";
184
184
  export type { AnySchema, ArraySchema, BooleanSchema, EnumSchema, JsonSchema, NumberSchema, ObjectSchema, OptionalSchema, Static, StringSchema } from "toolcraft-schema";