toolcraft 0.0.17 → 0.0.19

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 (109) 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/config-mutations/dist/execution/apply-mutation.js +3 -3
  30. package/node_modules/@poe-code/config-mutations/dist/mutations/template-mutation.d.ts +3 -3
  31. package/node_modules/@poe-code/config-mutations/dist/template/render.d.ts +0 -1
  32. package/node_modules/@poe-code/config-mutations/dist/template/render.js +2 -22
  33. package/node_modules/@poe-code/config-mutations/package.json +1 -4
  34. package/node_modules/@poe-code/design-system/dist/acp/components.js +15 -13
  35. package/node_modules/@poe-code/design-system/dist/components/color.d.ts +31 -0
  36. package/node_modules/@poe-code/design-system/dist/components/color.js +101 -0
  37. package/node_modules/@poe-code/design-system/dist/components/help-formatter-plain.d.ts +1 -0
  38. package/node_modules/@poe-code/design-system/dist/components/help-formatter-plain.js +1 -1
  39. package/node_modules/@poe-code/design-system/dist/components/index.d.ts +4 -0
  40. package/node_modules/@poe-code/design-system/dist/components/index.js +2 -0
  41. package/node_modules/@poe-code/design-system/dist/components/logger.js +2 -2
  42. package/node_modules/@poe-code/design-system/dist/components/symbols.js +3 -3
  43. package/node_modules/@poe-code/design-system/dist/components/table.js +191 -40
  44. package/node_modules/@poe-code/design-system/dist/components/template.d.ts +6 -0
  45. package/node_modules/@poe-code/design-system/dist/components/template.js +271 -0
  46. package/node_modules/@poe-code/design-system/dist/components/text.d.ts +1 -0
  47. package/node_modules/@poe-code/design-system/dist/components/text.js +11 -3
  48. package/node_modules/@poe-code/design-system/dist/dashboard/buffer.js +20 -13
  49. package/node_modules/@poe-code/design-system/dist/dashboard/keymap.d.ts +5 -0
  50. package/node_modules/@poe-code/design-system/dist/dashboard/keymap.js +146 -12
  51. package/node_modules/@poe-code/design-system/dist/dashboard/terminal.js +31 -0
  52. package/node_modules/@poe-code/design-system/dist/dashboard/types.d.ts +1 -0
  53. package/node_modules/@poe-code/design-system/dist/explorer/actions.d.ts +16 -0
  54. package/node_modules/@poe-code/design-system/dist/explorer/actions.js +39 -0
  55. package/node_modules/@poe-code/design-system/dist/explorer/demo.d.ts +13 -0
  56. package/node_modules/@poe-code/design-system/dist/explorer/demo.js +297 -0
  57. package/node_modules/@poe-code/design-system/dist/explorer/events.d.ts +61 -0
  58. package/node_modules/@poe-code/design-system/dist/explorer/events.js +1 -0
  59. package/node_modules/@poe-code/design-system/dist/explorer/filter.d.ts +10 -0
  60. package/node_modules/@poe-code/design-system/dist/explorer/filter.js +95 -0
  61. package/node_modules/@poe-code/design-system/dist/explorer/index.d.ts +8 -0
  62. package/node_modules/@poe-code/design-system/dist/explorer/index.js +8 -0
  63. package/node_modules/@poe-code/design-system/dist/explorer/jobs.d.ts +7 -0
  64. package/node_modules/@poe-code/design-system/dist/explorer/jobs.js +59 -0
  65. package/node_modules/@poe-code/design-system/dist/explorer/keymap.d.ts +21 -0
  66. package/node_modules/@poe-code/design-system/dist/explorer/keymap.js +363 -0
  67. package/node_modules/@poe-code/design-system/dist/explorer/layout.d.ts +20 -0
  68. package/node_modules/@poe-code/design-system/dist/explorer/layout.js +73 -0
  69. package/node_modules/@poe-code/design-system/dist/explorer/reducer.d.ts +9 -0
  70. package/node_modules/@poe-code/design-system/dist/explorer/reducer.js +704 -0
  71. package/node_modules/@poe-code/design-system/dist/explorer/render/detail.d.ts +4 -0
  72. package/node_modules/@poe-code/design-system/dist/explorer/render/detail.js +96 -0
  73. package/node_modules/@poe-code/design-system/dist/explorer/render/footer.d.ts +4 -0
  74. package/node_modules/@poe-code/design-system/dist/explorer/render/footer.js +49 -0
  75. package/node_modules/@poe-code/design-system/dist/explorer/render/header.d.ts +4 -0
  76. package/node_modules/@poe-code/design-system/dist/explorer/render/header.js +56 -0
  77. package/node_modules/@poe-code/design-system/dist/explorer/render/index.d.ts +8 -0
  78. package/node_modules/@poe-code/design-system/dist/explorer/render/index.js +61 -0
  79. package/node_modules/@poe-code/design-system/dist/explorer/render/list.d.ts +4 -0
  80. package/node_modules/@poe-code/design-system/dist/explorer/render/list.js +106 -0
  81. package/node_modules/@poe-code/design-system/dist/explorer/render/modal.d.ts +3 -0
  82. package/node_modules/@poe-code/design-system/dist/explorer/render/modal.js +91 -0
  83. package/node_modules/@poe-code/design-system/dist/explorer/render/test-fixtures.d.ts +8 -0
  84. package/node_modules/@poe-code/design-system/dist/explorer/render/test-fixtures.js +156 -0
  85. package/node_modules/@poe-code/design-system/dist/explorer/runtime.d.ts +2 -0
  86. package/node_modules/@poe-code/design-system/dist/explorer/runtime.js +282 -0
  87. package/node_modules/@poe-code/design-system/dist/explorer/runtime.test-helpers.d.ts +50 -0
  88. package/node_modules/@poe-code/design-system/dist/explorer/runtime.test-helpers.js +101 -0
  89. package/node_modules/@poe-code/design-system/dist/explorer/state.d.ts +130 -0
  90. package/node_modules/@poe-code/design-system/dist/explorer/state.js +87 -0
  91. package/node_modules/@poe-code/design-system/dist/explorer/theme.d.ts +27 -0
  92. package/node_modules/@poe-code/design-system/dist/explorer/theme.js +97 -0
  93. package/node_modules/@poe-code/design-system/dist/index.d.ts +7 -0
  94. package/node_modules/@poe-code/design-system/dist/index.js +5 -0
  95. package/node_modules/@poe-code/design-system/dist/internal/color-support.d.ts +9 -0
  96. package/node_modules/@poe-code/design-system/dist/internal/color-support.js +12 -0
  97. package/node_modules/@poe-code/design-system/dist/prompts/index.js +2 -2
  98. package/node_modules/@poe-code/design-system/dist/prompts/primitives/cancel.js +2 -2
  99. package/node_modules/@poe-code/design-system/dist/prompts/primitives/intro.js +2 -2
  100. package/node_modules/@poe-code/design-system/dist/prompts/primitives/log.js +4 -4
  101. package/node_modules/@poe-code/design-system/dist/prompts/primitives/note.js +5 -5
  102. package/node_modules/@poe-code/design-system/dist/prompts/primitives/outro.js +2 -2
  103. package/node_modules/@poe-code/design-system/dist/prompts/primitives/spinner.js +3 -3
  104. package/node_modules/@poe-code/design-system/dist/static/menu.js +5 -5
  105. package/node_modules/@poe-code/design-system/dist/static/spinner.js +8 -8
  106. package/node_modules/@poe-code/design-system/dist/tokens/colors.js +29 -29
  107. package/node_modules/@poe-code/design-system/dist/tokens/typography.js +6 -6
  108. package/node_modules/@poe-code/design-system/package.json +6 -3
  109. package/package.json +6 -5
package/dist/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { fileURLToPath } from "node:url";
2
2
  import { ApprovalDeclinedError } from "./human-in-loop/types.js";
3
3
  import { mergeHumanInLoopFromGroup, validateHumanInLoopOnDefine } from "./human-in-loop/config.js";
4
- import { UserError } from "./user-error.js";
4
+ import { ToolcraftBugError, UserError } from "./user-error.js";
5
+ import { suggest } from "./suggest.js";
5
6
  const commandConfigSymbol = Symbol("toolcraft.command.config");
6
7
  const groupConfigSymbol = Symbol("toolcraft.group.config");
7
8
  const commandSourcePathSymbol = Symbol("toolcraft.command.sourcePath");
@@ -12,7 +13,7 @@ function cloneSecretDefinition(secret) {
12
13
  return {
13
14
  env: secret.env,
14
15
  description: secret.description,
15
- optional: secret.optional,
16
+ optional: secret.optional
16
17
  };
17
18
  }
18
19
  function cloneSecrets(secrets) {
@@ -28,7 +29,7 @@ function cloneRequires(requires) {
28
29
  return {
29
30
  auth: requires.auth,
30
31
  apiVersion: requires.apiVersion,
31
- check: requires.check,
32
+ check: requires.check
32
33
  };
33
34
  }
34
35
  function cloneStringArray(values) {
@@ -46,13 +47,13 @@ function cloneMcpServerConfig(config) {
46
47
  transport: "stdio",
47
48
  command: config.command,
48
49
  args: cloneStringArray(config.args),
49
- env: cloneStringRecord(config.env),
50
+ env: cloneStringRecord(config.env)
50
51
  };
51
52
  }
52
53
  return {
53
54
  transport: "http",
54
55
  url: config.url,
55
- headers: cloneStringRecord(config.headers),
56
+ headers: cloneStringRecord(config.headers)
56
57
  };
57
58
  }
58
59
  function cloneRenameMap(rename) {
@@ -156,11 +157,9 @@ function mergeRequires(parent, child) {
156
157
  const merged = {
157
158
  auth: child?.auth ?? parent?.auth,
158
159
  apiVersion: child?.apiVersion ?? parent?.apiVersion,
159
- check: composeChecks(parent?.check, child?.check),
160
+ check: composeChecks(parent?.check, child?.check)
160
161
  };
161
- if (merged.auth === undefined &&
162
- merged.apiVersion === undefined &&
163
- merged.check === undefined) {
162
+ if (merged.auth === undefined && merged.apiVersion === undefined && merged.check === undefined) {
164
163
  return undefined;
165
164
  }
166
165
  return merged;
@@ -207,12 +206,33 @@ export function resolveCommandSecrets(command, env = process.env) {
207
206
  const value = env[secret.env];
208
207
  if (value === undefined && secret.optional !== true) {
209
208
  const details = secret.description ? `\n ${secret.description}` : "";
210
- throw new UserError(`Error: Missing required secret ${secret.env}${details}`);
209
+ const candidates = Object.keys(env).filter((candidate) => candidate !== secret.env && env[candidate] !== undefined);
210
+ const suggestions = suggestSecretEnv(secret.env, candidates);
211
+ const suggestionLine = suggestions.length > 0 ? `\nDid you mean: ${suggestions.join(", ")}?` : "";
212
+ throw new UserError(`Missing required secret ${secret.env}${details}${suggestionLine}`);
211
213
  }
212
214
  secrets[name] = value;
213
215
  }
214
216
  return secrets;
215
217
  }
218
+ function suggestSecretEnv(input, candidates) {
219
+ const directSuggestions = suggest(input, candidates);
220
+ if (!input.includes("_")) {
221
+ return directSuggestions;
222
+ }
223
+ const inputParts = input.split("_");
224
+ const firstPart = inputParts[0];
225
+ const lastPart = inputParts[inputParts.length - 1];
226
+ const relatedCandidates = candidates.filter((candidate) => {
227
+ const candidateParts = candidate.split("_");
228
+ return (candidateParts[0] === firstPart &&
229
+ candidateParts[candidateParts.length - 1] === lastPart);
230
+ });
231
+ const expandedSuggestions = suggest(input, relatedCandidates, {
232
+ threshold: Math.max(4, Math.floor(input.length / 4))
233
+ });
234
+ return [...new Set([...directSuggestions, ...expandedSuggestions])].slice(0, 3);
235
+ }
216
236
  export async function assertCommandRequirements(command, context, options = {}) {
217
237
  const requires = command.requires;
218
238
  if (requires === undefined) {
@@ -221,22 +241,22 @@ export async function assertCommandRequirements(command, context, options = {})
221
241
  const env = options.env ?? process.env;
222
242
  const authEnvVar = options.authEnvVar ?? "POE_API_KEY";
223
243
  if (requires.auth === true && env[authEnvVar] === undefined) {
224
- throw new UserError(`Error: Command "${command.name}" requires authentication.\n Run 'poe-code login' first.`);
244
+ throw new UserError(`Command "${command.name}" requires authentication.\n Run 'poe-code login' first.`);
225
245
  }
226
246
  if (requires.apiVersion !== undefined) {
227
247
  const minimumVersion = parseMinimumApiVersion(requires.apiVersion);
228
248
  if (minimumVersion === undefined) {
229
- throw new UserError(`Error: Command "${command.name}" has invalid apiVersion requirement "${requires.apiVersion}". Expected format ">=X.Y.Z".`);
249
+ throw new UserError(`Command "${command.name}" has invalid apiVersion requirement "${requires.apiVersion}". Expected format ">=X.Y.Z".`);
230
250
  }
231
251
  if (options.apiVersion === undefined) {
232
- throw new UserError(`Error: Command "${command.name}" requires API version ${requires.apiVersion}, but no runner API version was provided.`);
252
+ throw new UserError(`Command "${command.name}" requires API version ${requires.apiVersion}, but no runner API version was provided.`);
233
253
  }
234
254
  const runnerVersion = parseSimpleSemver(options.apiVersion);
235
255
  if (runnerVersion === undefined) {
236
- throw new UserError(`Error: Command "${command.name}" requires API version ${requires.apiVersion}, but runner API version "${options.apiVersion}" is not valid semver.`);
256
+ throw new UserError(`Command "${command.name}" requires API version ${requires.apiVersion}, but runner API version "${options.apiVersion}" is not valid semver.`);
237
257
  }
238
258
  if (compareSemver(runnerVersion, minimumVersion) < 0) {
239
- throw new UserError(`Error: Command "${command.name}" requires API version ${requires.apiVersion}, but runner API version is ${options.apiVersion}.`);
259
+ throw new UserError(`Command "${command.name}" requires API version ${requires.apiVersion}, but runner API version is ${options.apiVersion}.`);
240
260
  }
241
261
  }
242
262
  const checkResult = await requires.check?.(context);
@@ -247,7 +267,7 @@ export async function assertCommandRequirements(command, context, options = {})
247
267
  function mergeSecrets(parent, child) {
248
268
  return cloneSecrets({
249
269
  ...parent,
250
- ...child,
270
+ ...child
251
271
  });
252
272
  }
253
273
  function resolveCommandScope(ownScope, inheritedScope) {
@@ -270,7 +290,7 @@ function createBaseCommand(config) {
270
290
  humanInLoop: config.humanInLoop,
271
291
  requires: cloneRequires(config.requires),
272
292
  handler: config.handler,
273
- render: config.render,
293
+ render: config.render
274
294
  };
275
295
  Object.defineProperty(command, commandConfigSymbol, {
276
296
  value: {
@@ -278,8 +298,8 @@ function createBaseCommand(config) {
278
298
  humanInLoop: config.humanInLoop,
279
299
  secrets: cloneSecrets(config.secrets),
280
300
  requires: cloneRequires(config.requires),
281
- sourcePath: inferCommandSourcePath(),
282
- },
301
+ sourcePath: inferCommandSourcePath()
302
+ }
283
303
  });
284
304
  return command;
285
305
  }
@@ -294,7 +314,7 @@ function createBaseGroup(config) {
294
314
  secrets: cloneSecrets(config.secrets),
295
315
  requires: cloneRequires(config.requires),
296
316
  children: [],
297
- default: undefined,
317
+ default: undefined
298
318
  };
299
319
  Object.defineProperty(group, groupConfigSymbol, {
300
320
  value: {
@@ -306,8 +326,8 @@ function createBaseGroup(config) {
306
326
  rename: cloneRenameMap(config.rename),
307
327
  requires: cloneRequires(config.requires),
308
328
  children: [...config.children],
309
- default: config.default,
310
- },
329
+ default: config.default
330
+ }
311
331
  });
312
332
  return group;
313
333
  }
@@ -332,7 +352,7 @@ function materializeCommand(command, inherited) {
332
352
  humanInLoop: mergeHumanInLoopFromGroup(inherited.humanInLoop, internal.humanInLoop),
333
353
  requires: mergeRequires(inherited.requires, internal.requires),
334
354
  handler: command.handler,
335
- render: command.render,
355
+ render: command.render
336
356
  };
337
357
  Object.defineProperty(materialized, commandConfigSymbol, {
338
358
  value: {
@@ -340,11 +360,11 @@ function materializeCommand(command, inherited) {
340
360
  humanInLoop: internal.humanInLoop,
341
361
  secrets: cloneSecrets(internal.secrets),
342
362
  requires: cloneRequires(internal.requires),
343
- sourcePath: internal.sourcePath,
344
- },
363
+ sourcePath: internal.sourcePath
364
+ }
345
365
  });
346
366
  Object.defineProperty(materialized, commandSourcePathSymbol, {
347
- value: internal.sourcePath,
367
+ value: internal.sourcePath
348
368
  });
349
369
  return materialized;
350
370
  }
@@ -353,7 +373,7 @@ function mergeInheritedMetadata(group, inherited) {
353
373
  scope: resolveGroupScope(group.scope, inherited.scope),
354
374
  humanInLoop: mergeHumanInLoopFromGroup(inherited.humanInLoop, group.humanInLoop),
355
375
  secrets: mergeSecrets(inherited.secrets, group.secrets),
356
- requires: mergeRequires(inherited.requires, group.requires),
376
+ requires: mergeRequires(inherited.requires, group.requires)
357
377
  };
358
378
  }
359
379
  function materializeGroup(group, inherited) {
@@ -364,11 +384,11 @@ function materializeGroup(group, inherited) {
364
384
  if (internal.default !== undefined) {
365
385
  const defaultIndex = internal.children.indexOf(internal.default);
366
386
  if (defaultIndex === -1) {
367
- throw new UserError(`Default command "${internal.default.name}" must be listed in children.`);
387
+ throw new ToolcraftBugError(`Default command "${internal.default.name}" must be listed in children.`);
368
388
  }
369
389
  const resolvedDefault = materializedChildren[defaultIndex];
370
390
  if (resolvedDefault?.kind !== "command") {
371
- throw new UserError(`Default child "${internal.default.name}" must be a command.`);
391
+ throw new ToolcraftBugError(`Default child "${internal.default.name}" must be a command.`);
372
392
  }
373
393
  defaultChild = resolvedDefault;
374
394
  }
@@ -382,7 +402,7 @@ function materializeGroup(group, inherited) {
382
402
  secrets: mergedInherited.secrets,
383
403
  requires: mergedInherited.requires,
384
404
  children: materializedChildren,
385
- default: defaultChild,
405
+ default: defaultChild
386
406
  };
387
407
  Object.defineProperty(materialized, groupConfigSymbol, {
388
408
  value: {
@@ -394,8 +414,8 @@ function materializeGroup(group, inherited) {
394
414
  rename: cloneRenameMap(internal.rename),
395
415
  requires: cloneRequires(internal.requires),
396
416
  children: [...internal.children],
397
- default: internal.default,
398
- },
417
+ default: internal.default
418
+ }
399
419
  });
400
420
  return materialized;
401
421
  }
@@ -411,7 +431,7 @@ export function defineCommand(config) {
411
431
  scope: undefined,
412
432
  humanInLoop: undefined,
413
433
  secrets: {},
414
- requires: undefined,
434
+ requires: undefined
415
435
  });
416
436
  }
417
437
  export function defineGroup(config) {
@@ -421,12 +441,12 @@ export function defineGroup(config) {
421
441
  scope: undefined,
422
442
  humanInLoop: undefined,
423
443
  secrets: {},
424
- requires: undefined,
444
+ requires: undefined
425
445
  });
426
446
  }
427
447
  export function getCommandSourcePath(command) {
428
448
  return command[commandSourcePathSymbol];
429
449
  }
430
450
  export { S, toJsonSchema } from "toolcraft-schema";
431
- export { ApprovalDeclinedError, UserError };
451
+ export { ApprovalDeclinedError, ToolcraftBugError, UserError };
432
452
  export { findPackageMetadata, packageMetadata } from "./package-metadata.js";
@@ -4,6 +4,7 @@ export interface JsonSchema {
4
4
  $defs?: Record<string, JsonSchema>;
5
5
  $ref?: string;
6
6
  additionalProperties?: boolean | JsonSchema;
7
+ allOf?: readonly JsonSchema[];
7
8
  anyOf?: readonly JsonSchema[];
8
9
  const?: unknown;
9
10
  default?: unknown;
@@ -2,17 +2,17 @@ import { S } from "toolcraft-schema";
2
2
  export function convertJsonSchema(schema) {
3
3
  if (hasSelfReferencingRef(schema, schema)) {
4
4
  return applyMetadata(S.Json(), schema, {
5
- nullable: schema.nullable === true,
5
+ nullable: schema.nullable === true
6
6
  });
7
7
  }
8
- return convertSchema(schema, schema);
8
+ return convertSchema(schema, schema, []);
9
9
  }
10
- function convertSchema(schema, root) {
11
- const resolvedSchema = resolveReferencedSchema(schema, root);
10
+ function convertSchema(schema, root, path) {
11
+ const resolvedSchema = resolveReferencedSchema(schema, root, path);
12
12
  const normalizedSchema = normalizeNullability(resolvedSchema);
13
- const composition = normalizedSchema.schema.oneOf ?? normalizedSchema.schema.anyOf;
13
+ const composition = getComposition(normalizedSchema.schema);
14
14
  if (Array.isArray(normalizedSchema.schema.type)) {
15
- throw new Error(`Unsupported JSON Schema type array: ${JSON.stringify(normalizedSchema.schema.type)}.`);
15
+ throw new Error(`JSON Schema "${formatJsonSchemaPath(path)}" has an unsupported type "${formatJsonSchemaType(normalizedSchema.schema.type)}". Supported: string, number, integer, boolean, array, object.`);
16
16
  }
17
17
  if (resolvedSchema.const !== undefined) {
18
18
  return convertConstSchema(resolvedSchema, normalizedSchema.nullable);
@@ -21,11 +21,14 @@ function convertSchema(schema, root) {
21
21
  return convertEnumSchema(resolvedSchema, normalizedSchema.nullable);
22
22
  }
23
23
  if (composition !== undefined) {
24
- return convertCompositionSchema(normalizedSchema.schema, root, normalizedSchema.nullable);
24
+ return convertCompositionSchema(normalizedSchema.schema, root, normalizedSchema.nullable, path);
25
25
  }
26
26
  if (isRecordSchema(normalizedSchema.schema)) {
27
- return applyMetadata(S.Record(convertSchema(normalizedSchema.schema.additionalProperties, root)), normalizedSchema.schema, {
28
- nullable: normalizedSchema.nullable,
27
+ return applyMetadata(S.Record(convertSchema(normalizedSchema.schema.additionalProperties, root, [
28
+ ...path,
29
+ "additionalProperties"
30
+ ])), normalizedSchema.schema, {
31
+ nullable: normalizedSchema.nullable
29
32
  });
30
33
  }
31
34
  switch (normalizedSchema.schema.type) {
@@ -34,40 +37,41 @@ function convertSchema(schema, root) {
34
37
  ...createCommonOptions(normalizedSchema.schema, normalizedSchema.nullable, getStringDefault(normalizedSchema.schema.default)),
35
38
  ...(normalizedSchema.schema.pattern === undefined
36
39
  ? {}
37
- : { pattern: normalizedSchema.schema.pattern }),
40
+ : { pattern: normalizedSchema.schema.pattern })
38
41
  });
39
42
  case "number":
40
43
  return S.Number(createCommonOptions(normalizedSchema.schema, normalizedSchema.nullable, getNumberDefault(normalizedSchema.schema.default)));
41
44
  case "integer":
42
45
  return S.Number({
43
46
  ...createCommonOptions(normalizedSchema.schema, normalizedSchema.nullable, getIntegerDefault(normalizedSchema.schema.default)),
44
- jsonType: "integer",
47
+ jsonType: "integer"
45
48
  });
46
49
  case "boolean":
47
50
  return S.Boolean(createCommonOptions(normalizedSchema.schema, normalizedSchema.nullable, getBooleanDefault(normalizedSchema.schema.default)));
48
51
  case "array":
49
52
  if (normalizedSchema.schema.items === undefined) {
50
- throw new Error('JSON Schema arrays must define "items".');
53
+ throw new Error(`JSON Schema "${formatJsonSchemaPath(path)}" is an array but is missing the "items" field. Add "items": { ... } to declare the element type.`);
51
54
  }
52
- return S.Array(convertSchema(normalizedSchema.schema.items, root), createCommonOptions(normalizedSchema.schema, normalizedSchema.nullable, getArrayDefault(normalizedSchema.schema.default)));
55
+ return S.Array(convertSchema(normalizedSchema.schema.items, root, [...path, "items"]), createCommonOptions(normalizedSchema.schema, normalizedSchema.nullable, getArrayDefault(normalizedSchema.schema.default)));
53
56
  case "object":
54
57
  return convertObjectSchema(normalizedSchema.schema, root, {
55
58
  nullable: normalizedSchema.nullable,
59
+ path
56
60
  });
57
61
  case "null":
58
62
  return applyMetadata(S.Json(), normalizedSchema.schema, {
59
63
  default: getJsonDefault(normalizedSchema.schema.default) ?? null,
60
- nullable: true,
64
+ nullable: true
61
65
  });
62
66
  case undefined:
63
67
  if (normalizedSchema.nullable) {
64
68
  return applyMetadata(S.Json(), normalizedSchema.schema, {
65
- nullable: true,
69
+ nullable: true
66
70
  });
67
71
  }
68
- throw new Error("Unsupported JSON Schema: missing type, enum, const, or composition.");
72
+ throw new Error(`JSON Schema "${formatJsonSchemaPath(path)}" must declare one of: "type", "enum", "const", "oneOf", "anyOf", or "allOf".`);
69
73
  }
70
- throw new Error(`Unsupported JSON Schema type: ${JSON.stringify(normalizedSchema.schema.type)}.`);
74
+ throw new Error(`JSON Schema "${formatJsonSchemaPath(path)}" has an unsupported type "${formatJsonSchemaType(normalizedSchema.schema.type)}". Supported: string, number, integer, boolean, array, object.`);
71
75
  }
72
76
  function convertConstSchema(schema, nullable) {
73
77
  if (isPrimitiveEnumValue(schema.const)) {
@@ -76,13 +80,13 @@ function convertConstSchema(schema, nullable) {
76
80
  default: schema.const,
77
81
  ...(schema.type === "integer" && typeof schema.const === "number"
78
82
  ? { jsonType: "integer" }
79
- : {}),
83
+ : {})
80
84
  });
81
85
  }
82
86
  return applyMetadata(S.Json(), schema, {
83
87
  default: schema.const,
84
88
  nullable: nullable || schema.const === null,
85
- description: appendDescription(schema.description, `Constant JSON value: ${JSON.stringify(schema.const)}.`),
89
+ description: appendDescription(schema.description, `Constant JSON value: ${JSON.stringify(schema.const)}.`)
86
90
  });
87
91
  }
88
92
  function convertEnumSchema(schema, nullable) {
@@ -94,64 +98,75 @@ function convertEnumSchema(schema, nullable) {
94
98
  ...createCommonOptions(schema, nullable || hasNull, getPrimitiveEnumDefault(schema.default, nonNullValues)),
95
99
  ...(schema.type === "integer" && nonNullValues.every((value) => Number.isInteger(value))
96
100
  ? { jsonType: "integer" }
97
- : {}),
101
+ : {})
98
102
  });
99
103
  }
100
104
  return applyMetadata(S.Json(), schema, {
101
105
  nullable: nullable || hasNull,
102
- description: appendDescription(schema.description, `Allowed JSON values: ${values.map((value) => JSON.stringify(value)).join(", ")}.`),
106
+ description: appendDescription(schema.description, `Allowed JSON values: ${values.map((value) => JSON.stringify(value)).join(", ")}.`)
103
107
  });
104
108
  }
105
- function convertCompositionSchema(schema, root, nullable) {
106
- const branches = [...(schema.oneOf ?? schema.anyOf ?? [])].map((branch) => resolveReferencedSchema(branch, root));
107
- const discriminator = findDiscriminator(branches, root);
109
+ function convertCompositionSchema(schema, root, nullable, path) {
110
+ const composition = getComposition(schema);
111
+ const branchSchemas = composition?.branches ?? [];
112
+ const keyword = composition?.keyword ?? "oneOf";
113
+ const branches = branchSchemas.map((branch, index) => resolveReferencedSchema(branch, root, [...path, keyword, String(index)]));
114
+ const discriminator = findDiscriminator(branches, root, path);
108
115
  if (discriminator !== undefined) {
109
- const convertedBranches = Object.fromEntries(branches.map((branch) => [
116
+ const convertedBranches = Object.fromEntries(branches.map((branch, index) => [
110
117
  getDiscriminatorLiteral(branch, discriminator, root),
111
118
  convertObjectSchema(branch, root, {
112
119
  omitProperty: discriminator,
113
- }),
120
+ path: [...path, keyword, String(index)]
121
+ })
114
122
  ]));
115
123
  return applyMetadata(S.OneOf({
116
124
  discriminator,
117
- branches: convertedBranches,
125
+ branches: convertedBranches
118
126
  }), schema, {
119
- nullable,
127
+ nullable
120
128
  });
121
129
  }
122
- return applyMetadata(S.Union(branches.map((branch) => convertObjectSchema(branch, root, {}))), schema, {
123
- nullable,
130
+ return applyMetadata(S.Union(branches.map((branch, index) => convertObjectSchema(branch, root, {
131
+ path: [...path, keyword, String(index)]
132
+ }))), schema, {
133
+ nullable
124
134
  });
125
135
  }
126
136
  function convertObjectSchema(schema, root, options) {
127
- const resolvedSchema = resolveReferencedSchema(schema, root);
137
+ const resolvedSchema = resolveReferencedSchema(schema, root, options.path);
128
138
  const normalizedSchema = normalizeNullability(resolvedSchema);
129
139
  const properties = normalizedSchema.schema.properties ?? {};
130
140
  const requiredKeys = new Set(normalizedSchema.schema.required ?? []);
131
141
  const shape = {};
132
- if (normalizedSchema.schema.type !== "object" && normalizedSchema.schema.properties === undefined) {
133
- throw new Error("Expected an object schema branch.");
142
+ if (normalizedSchema.schema.type !== "object" &&
143
+ normalizedSchema.schema.properties === undefined) {
144
+ throw new Error(`Expected "${formatJsonSchemaPath(options.path)}" to be an object schema (got "${describeObjectSchemaKind(normalizedSchema.schema)}").`);
134
145
  }
135
146
  for (const [key, propertySchema] of Object.entries(properties)) {
136
147
  if (key === options.omitProperty) {
137
148
  continue;
138
149
  }
139
- const convertedProperty = convertSchema(propertySchema, root);
150
+ const convertedProperty = convertSchema(propertySchema, root, [
151
+ ...options.path,
152
+ "properties",
153
+ key
154
+ ]);
140
155
  shape[key] = requiredKeys.has(key) ? convertedProperty : S.Optional(convertedProperty);
141
156
  }
142
157
  return applyMetadata(S.Object(shape, {
143
158
  ...(typeof normalizedSchema.schema.additionalProperties === "boolean"
144
159
  ? { additionalProperties: normalizedSchema.schema.additionalProperties }
145
- : {}),
160
+ : {})
146
161
  }), normalizedSchema.schema, {
147
- nullable: options.nullable ?? normalizedSchema.nullable,
162
+ nullable: options.nullable ?? normalizedSchema.nullable
148
163
  });
149
164
  }
150
165
  function createCommonOptions(schema, nullable, defaultValue) {
151
166
  return {
152
167
  ...(schema.description === undefined ? {} : { description: schema.description }),
153
168
  ...(defaultValue === undefined ? {} : { default: defaultValue }),
154
- ...(nullable ? { nullable: true } : {}),
169
+ ...(nullable ? { nullable: true } : {})
155
170
  };
156
171
  }
157
172
  function applyMetadata(schema, source, overrides) {
@@ -174,25 +189,53 @@ function normalizeNullability(schema) {
174
189
  if (!Array.isArray(schema.type)) {
175
190
  return {
176
191
  schema,
177
- nullable: schema.nullable === true,
192
+ nullable: schema.nullable === true
178
193
  };
179
194
  }
180
195
  const nextTypes = schema.type.filter((value) => value !== "null");
181
196
  if (nextTypes.length === schema.type.length) {
182
197
  return {
183
198
  schema,
184
- nullable: schema.nullable === true,
199
+ nullable: schema.nullable === true
185
200
  };
186
201
  }
187
202
  return {
188
203
  schema: {
189
204
  ...schema,
190
205
  type: nextTypes.length === 0 ? undefined : nextTypes.length === 1 ? nextTypes[0] : nextTypes,
191
- nullable: undefined,
206
+ nullable: undefined
192
207
  },
193
- nullable: true,
208
+ nullable: true
194
209
  };
195
210
  }
211
+ function getComposition(schema) {
212
+ if (schema.oneOf !== undefined) {
213
+ return { keyword: "oneOf", branches: schema.oneOf };
214
+ }
215
+ if (schema.anyOf !== undefined) {
216
+ return { keyword: "anyOf", branches: schema.anyOf };
217
+ }
218
+ if (schema.allOf !== undefined) {
219
+ return { keyword: "allOf", branches: schema.allOf };
220
+ }
221
+ return undefined;
222
+ }
223
+ function formatJsonSchemaPath(path) {
224
+ if (path.length === 0) {
225
+ return "#";
226
+ }
227
+ return `#/${path.map(escapeJsonPointerSegment).join("/")}`;
228
+ }
229
+ function formatJsonSchemaType(type) {
230
+ return Array.isArray(type) ? JSON.stringify(type) : String(type);
231
+ }
232
+ function describeObjectSchemaKind(schema) {
233
+ const type = schema.type;
234
+ if (typeof type === "string") {
235
+ return type;
236
+ }
237
+ return type === undefined ? "unknown" : JSON.stringify(type);
238
+ }
196
239
  function isRecordSchema(schema) {
197
240
  const propertyKeys = Object.keys(schema.properties ?? {});
198
241
  return (schema.type === "object" &&
@@ -200,10 +243,10 @@ function isRecordSchema(schema) {
200
243
  typeof schema.additionalProperties === "object" &&
201
244
  schema.additionalProperties !== null);
202
245
  }
203
- function findDiscriminator(branches, root) {
246
+ function findDiscriminator(branches, root, path) {
204
247
  const [firstBranch] = branches;
205
248
  if (firstBranch === undefined) {
206
- throw new Error("JSON Schema composition requires at least one branch.");
249
+ throw new Error(`JSON Schema "${formatJsonSchemaPath(path)}" uses oneOf/anyOf/allOf but has no branches.`);
207
250
  }
208
251
  const candidateKeys = Object.keys(firstBranch.properties ?? {});
209
252
  for (const candidate of candidateKeys) {
@@ -233,7 +276,7 @@ function getDiscriminatorLiteral(branch, key, root) {
233
276
  if (propertySchema === undefined) {
234
277
  return undefined;
235
278
  }
236
- const resolvedProperty = resolveReferencedSchema(propertySchema, root);
279
+ const resolvedProperty = resolveReferencedSchema(propertySchema, root, []);
237
280
  if (typeof resolvedProperty.const === "string") {
238
281
  return resolvedProperty.const;
239
282
  }
@@ -244,17 +287,17 @@ function getDiscriminatorLiteral(branch, key, root) {
244
287
  }
245
288
  return undefined;
246
289
  }
247
- function resolveReferencedSchema(schema, root) {
290
+ function resolveReferencedSchema(schema, root, path) {
248
291
  if (schema.$ref === undefined) {
249
292
  return schema;
250
293
  }
251
294
  const resolvedTarget = resolveLocalRef(root, schema.$ref);
252
295
  if (resolvedTarget === undefined) {
253
- throw new Error(`Unsupported JSON Schema $ref: ${JSON.stringify(schema.$ref)}.`);
296
+ throw new Error(`JSON Schema "${formatJsonSchemaPath(path)}" uses "$ref": ${schema.$ref}. toolcraft only supports internal refs like "#/components/schemas/Foo".`);
254
297
  }
255
298
  const { $ref: ignoredRef, ...siblingKeywords } = schema;
256
299
  void ignoredRef;
257
- const resolvedSchema = resolveReferencedSchema(resolvedTarget, root);
300
+ const resolvedSchema = resolveReferencedSchema(resolvedTarget, root, path);
258
301
  if (Object.keys(siblingKeywords).length === 0) {
259
302
  return resolvedSchema;
260
303
  }
@@ -265,13 +308,13 @@ function mergeJsonSchemas(base, overlay) {
265
308
  ? undefined
266
309
  : {
267
310
  ...(base.properties ?? {}),
268
- ...(overlay.properties ?? {}),
311
+ ...(overlay.properties ?? {})
269
312
  };
270
313
  const mergedDefs = base.$defs === undefined && overlay.$defs === undefined
271
314
  ? undefined
272
315
  : {
273
316
  ...(base.$defs ?? {}),
274
- ...(overlay.$defs ?? {}),
317
+ ...(overlay.$defs ?? {})
275
318
  };
276
319
  const mergedRequired = base.required === undefined && overlay.required === undefined
277
320
  ? undefined
@@ -281,7 +324,7 @@ function mergeJsonSchemas(base, overlay) {
281
324
  ...overlay,
282
325
  ...(mergedDefs === undefined ? {} : { $defs: mergedDefs }),
283
326
  ...(mergedProperties === undefined ? {} : { properties: mergedProperties }),
284
- ...(mergedRequired === undefined ? {} : { required: mergedRequired }),
327
+ ...(mergedRequired === undefined ? {} : { required: mergedRequired })
285
328
  };
286
329
  }
287
330
  function hasSelfReferencingRef(schema, root, path = "#", activePaths = new Set()) {
@@ -293,11 +336,13 @@ function hasSelfReferencingRef(schema, root, path = "#", activePaths = new Set()
293
336
  return true;
294
337
  }
295
338
  const target = resolveLocalRef(root, localRefPath);
296
- if (target !== undefined && hasSelfReferencingRef(target, root, localRefPath, nextActivePaths)) {
339
+ if (target !== undefined &&
340
+ hasSelfReferencingRef(target, root, localRefPath, nextActivePaths)) {
297
341
  return true;
298
342
  }
299
343
  }
300
- if (schema.items !== undefined && hasSelfReferencingRef(schema.items, root, `${path}/items`, nextActivePaths)) {
344
+ if (schema.items !== undefined &&
345
+ hasSelfReferencingRef(schema.items, root, `${path}/items`, nextActivePaths)) {
301
346
  return true;
302
347
  }
303
348
  if (typeof schema.additionalProperties === "object" &&
@@ -325,6 +370,11 @@ function hasSelfReferencingRef(schema, root, path = "#", activePaths = new Set()
325
370
  return true;
326
371
  }
327
372
  }
373
+ for (const [index, childSchema] of (schema.allOf ?? []).entries()) {
374
+ if (hasSelfReferencingRef(childSchema, root, `${path}/allOf/${index}`, nextActivePaths)) {
375
+ return true;
376
+ }
377
+ }
328
378
  return false;
329
379
  }
330
380
  function getLocalRefPath(ref) {
@@ -5,6 +5,7 @@ export interface ResolveMcpProxyOptions {
5
5
  projectRoot?: string;
6
6
  }
7
7
  export declare function hasMcpProxyGroups(root: Group<any>): boolean;
8
+ export declare function findProjectRoot(from?: string): string | undefined;
8
9
  export declare function resolveCachePath(name: string, projectRoot?: string): string;
9
10
  export declare function parseRefreshEnv(value: string | undefined): "all" | Set<string> | undefined;
10
11
  export declare function dialUpstream(name: string, config: McpServerConfig): Promise<McpClient>;
package/dist/mcp-proxy.js CHANGED
@@ -327,22 +327,29 @@ function collectProxyGroups(root) {
327
327
  export function hasMcpProxyGroups(root) {
328
328
  return collectProxyGroups(root).length > 0;
329
329
  }
330
- export function resolveCachePath(name, projectRoot) {
331
- if (projectRoot !== undefined) {
332
- return path.join(projectRoot, ".toolcraft", "mcp", `${name}.json`);
333
- }
330
+ export function findProjectRoot(from = process.cwd()) {
334
331
  let current = process.cwd();
332
+ if (from !== current) {
333
+ current = path.resolve(from);
334
+ }
335
335
  while (true) {
336
336
  if (existsSync(path.join(current, "package.json"))) {
337
- return path.join(current, ".toolcraft", "mcp", `${name}.json`);
337
+ return current;
338
338
  }
339
339
  const parent = path.dirname(current);
340
340
  if (parent === current) {
341
- throw new Error(`Could not find package.json above "${process.cwd()}" while resolving MCP cache path.`);
341
+ return undefined;
342
342
  }
343
343
  current = parent;
344
344
  }
345
345
  }
346
+ export function resolveCachePath(name, projectRoot) {
347
+ const resolvedProjectRoot = projectRoot ?? findProjectRoot();
348
+ if (resolvedProjectRoot === undefined) {
349
+ throw new Error(`Could not find package.json above "${process.cwd()}" while resolving MCP cache path.`);
350
+ }
351
+ return path.join(resolvedProjectRoot, ".toolcraft", "mcp", `${name}.json`);
352
+ }
346
353
  export function parseRefreshEnv(value) {
347
354
  const trimmed = value?.trim();
348
355
  if (trimmed === undefined || trimmed.length === 0) {