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.
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +833 -124
- package/dist/error-report.d.ts +39 -0
- package/dist/error-report.js +330 -0
- package/dist/human-in-loop/approval-tasks.js +11 -8
- package/dist/human-in-loop/approvals-commands.js +21 -20
- package/dist/human-in-loop/default-provider.js +5 -3
- package/dist/human-in-loop/runner.js +45 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +55 -35
- package/dist/json-schema-converter.d.ts +1 -0
- package/dist/json-schema-converter.js +102 -52
- package/dist/mcp-proxy.d.ts +1 -0
- package/dist/mcp-proxy.js +13 -6
- package/dist/mcp.d.ts +2 -0
- package/dist/mcp.js +131 -55
- package/dist/sdk.d.ts +4 -2
- package/dist/sdk.js +132 -48
- package/dist/source-snippet.d.ts +8 -0
- package/dist/source-snippet.js +42 -0
- package/dist/stack-trim.d.ts +4 -0
- package/dist/stack-trim.js +70 -0
- package/dist/suggest.d.ts +4 -0
- package/dist/suggest.js +46 -0
- package/dist/user-error.d.ts +3 -0
- package/dist/user-error.js +7 -1
- package/dist/validation-errors.d.ts +5 -0
- package/dist/validation-errors.js +18 -0
- package/node_modules/@poe-code/design-system/dist/components/help-formatter-plain.d.ts +1 -0
- package/node_modules/@poe-code/design-system/dist/components/help-formatter-plain.js +1 -1
- package/node_modules/@poe-code/design-system/dist/components/text.d.ts +1 -0
- package/node_modules/@poe-code/design-system/dist/components/text.js +8 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/buffer.js +8 -1
- package/node_modules/@poe-code/design-system/dist/dashboard/keymap.d.ts +5 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/keymap.js +146 -12
- package/node_modules/@poe-code/design-system/dist/dashboard/terminal.js +31 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/types.d.ts +1 -0
- package/node_modules/@poe-code/design-system/dist/explorer/actions.d.ts +16 -0
- package/node_modules/@poe-code/design-system/dist/explorer/actions.js +39 -0
- package/node_modules/@poe-code/design-system/dist/explorer/demo.d.ts +13 -0
- package/node_modules/@poe-code/design-system/dist/explorer/demo.js +297 -0
- package/node_modules/@poe-code/design-system/dist/explorer/events.d.ts +61 -0
- package/node_modules/@poe-code/design-system/dist/explorer/events.js +1 -0
- package/node_modules/@poe-code/design-system/dist/explorer/filter.d.ts +10 -0
- package/node_modules/@poe-code/design-system/dist/explorer/filter.js +95 -0
- package/node_modules/@poe-code/design-system/dist/explorer/index.d.ts +8 -0
- package/node_modules/@poe-code/design-system/dist/explorer/index.js +8 -0
- package/node_modules/@poe-code/design-system/dist/explorer/jobs.d.ts +7 -0
- package/node_modules/@poe-code/design-system/dist/explorer/jobs.js +59 -0
- package/node_modules/@poe-code/design-system/dist/explorer/keymap.d.ts +21 -0
- package/node_modules/@poe-code/design-system/dist/explorer/keymap.js +363 -0
- package/node_modules/@poe-code/design-system/dist/explorer/layout.d.ts +20 -0
- package/node_modules/@poe-code/design-system/dist/explorer/layout.js +73 -0
- package/node_modules/@poe-code/design-system/dist/explorer/reducer.d.ts +9 -0
- package/node_modules/@poe-code/design-system/dist/explorer/reducer.js +704 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/detail.d.ts +4 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/detail.js +96 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/footer.d.ts +4 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/footer.js +49 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/header.d.ts +4 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/header.js +56 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/index.d.ts +8 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/index.js +61 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/list.d.ts +4 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/list.js +106 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/modal.d.ts +3 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/modal.js +91 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/test-fixtures.d.ts +8 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/test-fixtures.js +156 -0
- package/node_modules/@poe-code/design-system/dist/explorer/runtime.d.ts +2 -0
- package/node_modules/@poe-code/design-system/dist/explorer/runtime.js +282 -0
- package/node_modules/@poe-code/design-system/dist/explorer/runtime.test-helpers.d.ts +50 -0
- package/node_modules/@poe-code/design-system/dist/explorer/runtime.test-helpers.js +101 -0
- package/node_modules/@poe-code/design-system/dist/explorer/state.d.ts +130 -0
- package/node_modules/@poe-code/design-system/dist/explorer/state.js +87 -0
- package/node_modules/@poe-code/design-system/dist/explorer/theme.d.ts +27 -0
- package/node_modules/@poe-code/design-system/dist/explorer/theme.js +97 -0
- package/node_modules/@poe-code/design-system/dist/index.d.ts +3 -0
- package/node_modules/@poe-code/design-system/dist/index.js +3 -0
- package/node_modules/@poe-code/design-system/package.json +1 -0
- 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(
|
|
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("
|
|
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" &&
|
|
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("
|
|
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"
|
|
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
|
|
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
|
|
106
|
+
throw unknownCommandPathError();
|
|
105
107
|
}
|
|
106
108
|
const next = current.children.find((child) => child.name === segment);
|
|
107
109
|
if (next === undefined) {
|
|
108
|
-
throw
|
|
110
|
+
throw unknownCommandPathError();
|
|
109
111
|
}
|
|
110
112
|
current = next;
|
|
111
113
|
}
|
|
112
114
|
if (current.kind !== "command") {
|
|
113
|
-
throw
|
|
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";
|