toolcraft 0.0.136 → 0.0.138

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 (58) hide show
  1. package/composition.json +7 -2
  2. package/dist/cli.js +55 -34
  3. package/dist/composition.json +7 -2
  4. package/dist/human-in-loop/approvals-commands.js +18 -28
  5. package/dist/index.d.ts +3 -2
  6. package/dist/index.js +3 -2
  7. package/dist/renderer.js +4 -1
  8. package/dist/user-error.d.ts +8 -0
  9. package/dist/user-error.js +10 -0
  10. package/node_modules/@poe-code/agent-defs/README.md +14 -1
  11. package/node_modules/@poe-code/agent-defs/dist/agents/claude-code.js +1 -0
  12. package/node_modules/@poe-code/agent-defs/dist/agents/claude-desktop.js +1 -0
  13. package/node_modules/@poe-code/agent-defs/dist/agents/codex.js +1 -0
  14. package/node_modules/@poe-code/agent-defs/dist/agents/cursor.js +1 -0
  15. package/node_modules/@poe-code/agent-defs/dist/agents/gemini-cli.js +1 -0
  16. package/node_modules/@poe-code/agent-defs/dist/agents/goose.js +1 -0
  17. package/node_modules/@poe-code/agent-defs/dist/agents/kimi.js +1 -0
  18. package/node_modules/@poe-code/agent-defs/dist/agents/opencode.js +1 -0
  19. package/node_modules/@poe-code/agent-defs/dist/agents/pi.js +1 -0
  20. package/node_modules/@poe-code/agent-defs/dist/agents/poe-agent.js +1 -0
  21. package/node_modules/@poe-code/agent-defs/dist/capabilities.d.ts +14 -0
  22. package/node_modules/@poe-code/agent-defs/dist/capabilities.js +63 -0
  23. package/node_modules/@poe-code/agent-defs/dist/index.d.ts +2 -1
  24. package/node_modules/@poe-code/agent-defs/dist/index.js +1 -0
  25. package/node_modules/@poe-code/agent-defs/dist/registry.js +3 -0
  26. package/node_modules/@poe-code/agent-defs/dist/types.d.ts +7 -0
  27. package/node_modules/@poe-code/frontmatter/README.md +3 -0
  28. package/node_modules/@poe-code/frontmatter/dist/index.d.ts +1 -1
  29. package/node_modules/@poe-code/frontmatter/dist/index.js +1 -1
  30. package/node_modules/@poe-code/frontmatter/dist/parse.d.ts +9 -0
  31. package/node_modules/@poe-code/frontmatter/dist/parse.js +16 -0
  32. package/node_modules/@poe-code/process-runner/dist/docker/docker-execution-env.js +20 -8
  33. package/node_modules/@poe-code/process-runner/dist/host/host-execution-env.js +1 -0
  34. package/node_modules/@poe-code/process-runner/dist/types.d.ts +2 -0
  35. package/node_modules/@poe-code/task-list/dist/backends/gh-issues-client.js +4 -0
  36. package/node_modules/@poe-code/user-error/README.md +56 -0
  37. package/node_modules/@poe-code/user-error/dist/index.d.ts +17 -0
  38. package/node_modules/@poe-code/user-error/dist/index.js +21 -0
  39. package/node_modules/@poe-code/user-error/package.json +24 -0
  40. package/node_modules/tiny-stdio-mcp-server/dist/composition.json +1 -1
  41. package/node_modules/toolcraft-design/dist/acp/components.d.ts +8 -1
  42. package/node_modules/toolcraft-design/dist/acp/components.js +11 -6
  43. package/node_modules/toolcraft-design/dist/acp/index.d.ts +1 -0
  44. package/node_modules/toolcraft-design/dist/components/command-errors.d.ts +2 -0
  45. package/node_modules/toolcraft-design/dist/components/command-errors.js +8 -2
  46. package/node_modules/toolcraft-design/dist/components/index.d.ts +1 -1
  47. package/node_modules/toolcraft-design/dist/components/index.js +1 -1
  48. package/node_modules/toolcraft-design/dist/components/table.d.ts +1 -0
  49. package/node_modules/toolcraft-design/dist/components/table.js +52 -5
  50. package/node_modules/toolcraft-design/dist/components/template.d.ts +9 -0
  51. package/node_modules/toolcraft-design/dist/components/template.js +27 -2
  52. package/node_modules/toolcraft-design/dist/index.d.ts +2 -2
  53. package/node_modules/toolcraft-design/dist/index.js +2 -2
  54. package/node_modules/toolcraft-design/dist/prompts/interactive/core.d.ts +5 -0
  55. package/node_modules/toolcraft-design/dist/prompts/interactive/core.js +15 -1
  56. package/node_modules/toolcraft-design/dist/prompts/primitives/spinner.js +1 -1
  57. package/node_modules/toolcraft-schema/package.json +1 -1
  58. package/package.json +6 -4
package/composition.json CHANGED
@@ -36,6 +36,11 @@
36
36
  "version": "0.0.1",
37
37
  "license": "MIT"
38
38
  },
39
+ {
40
+ "name": "@poe-code/user-error",
41
+ "version": "0.0.1",
42
+ "license": "MIT"
43
+ },
39
44
  {
40
45
  "name": "auth-store",
41
46
  "version": "0.0.1",
@@ -108,7 +113,7 @@
108
113
  },
109
114
  {
110
115
  "name": "toolcraft",
111
- "version": "0.0.136",
116
+ "version": "0.0.138",
112
117
  "license": "MIT"
113
118
  },
114
119
  {
@@ -118,7 +123,7 @@
118
123
  },
119
124
  {
120
125
  "name": "toolcraft-schema",
121
- "version": "0.0.136",
126
+ "version": "0.0.138",
122
127
  "license": "MIT"
123
128
  },
124
129
  {
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ import path from "node:path";
4
4
  import { Command as CommanderCommand, CommanderError, InvalidArgumentError, Option } from "commander";
5
5
  import { validate as validateSchema } from "toolcraft-schema";
6
6
  import { cancel, configureTheme, confirm, createLogger, formatCommandList, formatOptionList, getTheme, helpFormatterPlain, isCancel, note, promptText, renderHelpTokens, renderTable, resetOutputFormatCache, select, text } from "toolcraft-design";
7
- import { ApprovalDeclinedError, ToolcraftBugError, UserError, assertCommandRequirements, getCommandSourcePath, hasMcpProxyConfig, resolveCommandSecrets } from "./index.js";
7
+ import { ApprovalDeclinedError, ToolcraftBugError, UserError, assertCommandRequirements, getCommandSourcePath, hasMcpProxyConfig, isUserError, resolveCommandSecrets } from "./index.js";
8
8
  import { hasOwnErrorCode } from "./error-codes.js";
9
9
  import { writeErrorReport } from "./error-report.js";
10
10
  import { assertHumanInLoopWired, mergeApprovalsRoot } from "./human-in-loop/wiring.js";
@@ -396,6 +396,13 @@ function formatOptionFlags(field, globalLongOptionFlags) {
396
396
  }
397
397
  return [`-${field.shortFlag}`, field.optionFlag, ...field.longAliases].join(", ");
398
398
  }
399
+ function formatMissingParameterMessage(field) {
400
+ const message = `Missing required parameter "${field.displayPath}".`;
401
+ if (field.schema.kind !== "enum") {
402
+ return message;
403
+ }
404
+ return `${message} Expected one of: ${field.schema.values.map((value) => String(value)).join(", ")}.`;
405
+ }
399
406
  function formatPositionalToken(field) {
400
407
  const optionalPositional = field.optional || field.hasDefault;
401
408
  if (field.variadicPosition === true) {
@@ -1132,6 +1139,8 @@ function formatExampleRows(examples, breadcrumb, rootUsageName) {
1132
1139
  return examples.map((example) => `${example.title}\n ${formatExampleCommand(breadcrumb, rootUsageName, example.params)}`);
1133
1140
  }
1134
1141
  const MAX_INLINE_OPTIONAL_PARAMETER_TOKENS = 8;
1142
+ /** Mirrors the fallback width used by the design system's column layout. */
1143
+ const DEFAULT_HELP_TOTAL_WIDTH = 100;
1135
1144
  function tokenizeHelpFlags(flags) {
1136
1145
  const tokens = [];
1137
1146
  let index = 0;
@@ -1208,9 +1217,13 @@ function formatCommandParameterTokens(command, casing, globalLongOptionFlags) {
1208
1217
  .map((field) => createCommandParameterToken(formatCommandParameterFieldFlags(field, globalLongOptionFlags), field.positionalIndex === undefined && (field.optional || field.hasDefault)))
1209
1218
  .concat(collected.dynamicFields.flatMap((field) => formatCommandDynamicParameterTokens(field, casing)));
1210
1219
  }
1211
- function collapseOptionalParameterTokens(parameterTokens) {
1220
+ function collapseOptionalParameterTokens(parameterTokens, inlineWidthBudget = Number.POSITIVE_INFINITY) {
1212
1221
  const optionalCount = parameterTokens.filter((token) => token.optional).length;
1213
- if (optionalCount <= MAX_INLINE_OPTIONAL_PARAMETER_TOKENS) {
1222
+ if (optionalCount === 0) {
1223
+ return parameterTokens;
1224
+ }
1225
+ const inlineWidth = parameterTokens.reduce((total, token) => total + token.text.length + 1, 0);
1226
+ if (optionalCount <= MAX_INLINE_OPTIONAL_PARAMETER_TOKENS && inlineWidth <= inlineWidthBudget) {
1214
1227
  return parameterTokens;
1215
1228
  }
1216
1229
  const required = parameterTokens.filter((token) => !token.optional);
@@ -1231,8 +1244,11 @@ function collapseOptionalParameterTokens(parameterTokens) {
1231
1244
  function formatCommandRowNameTokens(node, casing, globalLongOptionFlags) {
1232
1245
  const baseName = node.aliases.length === 0 ? node.name : `${node.name} (${node.aliases.join(", ")})`;
1233
1246
  const nameTokens = [{ text: baseName, role: "command" }];
1247
+ // formatCommandList indents rows by two columns, so anything wider than the rest of the
1248
+ // terminal wraps onto continuation lines and makes the command list unreadable.
1249
+ const inlineWidthBudget = Math.max(0, (process.stdout.columns ?? DEFAULT_HELP_TOTAL_WIDTH) - 2 - baseName.length);
1234
1250
  const parameterTokens = node.kind === "command"
1235
- ? collapseOptionalParameterTokens(formatCommandParameterTokens(node, casing, globalLongOptionFlags))
1251
+ ? collapseOptionalParameterTokens(formatCommandParameterTokens(node, casing, globalLongOptionFlags), inlineWidthBudget)
1236
1252
  : [];
1237
1253
  for (const token of parameterTokens) {
1238
1254
  nameTokens.push({ text: " ", role: "literal" }, ...token.tokens);
@@ -1285,10 +1301,10 @@ function formatGlobalOptionsLine(ctx) {
1285
1301
  if (ctx.showVersion) {
1286
1302
  flags.push("--version");
1287
1303
  }
1288
- return flags.length > 0 ? `${text.section("Options:")} ${flags.join(" ")}` : "";
1304
+ return flags.length > 0 ? `${text.section("Global Options:")} ${flags.join(" ")}` : "";
1289
1305
  }
1290
1306
  function formatLeafGlobalOptionsLine(ctx) {
1291
- return ctx.controls.verbose ? `${text.section("Options:")} -v, --verbose` : "";
1307
+ return ctx.controls.verbose ? `${text.section("Global Options:")} -v, --verbose` : "";
1292
1308
  }
1293
1309
  function collectSchemaGlobalFieldRows(group, scope, casing, globalLongOptionFlags) {
1294
1310
  const seen = new Map();
@@ -1331,21 +1347,16 @@ function formatHelpOptionList(rows) {
1331
1347
  : formatOptionList(rows);
1332
1348
  }
1333
1349
  function sortLeafHelpOptionFields(fields) {
1334
- const positionals = [];
1335
1350
  const required = [];
1336
1351
  const optional = [];
1337
1352
  for (const field of fields) {
1338
- if (field.positionalIndex !== undefined) {
1339
- positionals.push(field);
1340
- continue;
1341
- }
1342
1353
  if (!field.optional && !field.hasDefault) {
1343
1354
  required.push(field);
1344
1355
  continue;
1345
1356
  }
1346
1357
  optional.push(field);
1347
1358
  }
1348
- return [...positionals, ...required, ...optional];
1359
+ return [...required, ...optional];
1349
1360
  }
1350
1361
  function buildUsageLine(breadcrumb, rootUsageName, suffix) {
1351
1362
  const visibleBreadcrumb = breadcrumb.filter((segment) => segment.length > 0);
@@ -1388,13 +1399,13 @@ function renderGroupHelp(group, breadcrumb, scope, casing, globalOptions, rootUs
1388
1399
  const globalLongOptionFlags = getGlobalLongOptionFlags(globalOptions.presetsEnabled, globalOptions.showVersion, globalOptions.controls);
1389
1400
  const commandRows = formatCommandRows(group, scope, casing, globalLongOptionFlags, globalOptions.controls.help);
1390
1401
  if (commandRows.length > 0) {
1391
- sections.push(`${text.sectionHeader("Commands")}\n${formatHelpCommandList(commandRows)}`);
1402
+ sections.push(`${text.section("Commands:")}\n${formatHelpCommandList(commandRows)}`);
1392
1403
  }
1393
1404
  if (isRoot) {
1394
1405
  const schemaGlobalRows = collectSchemaGlobalFieldRows(group, scope, casing, globalLongOptionFlags);
1395
1406
  const builtInLine = formatGlobalOptionsLine(globalOptions);
1396
1407
  if (schemaGlobalRows.length > 0) {
1397
- sections.push(`${text.sectionHeader("Options")}\n${formatHelpOptionList(schemaGlobalRows)}\n${builtInLine}`);
1408
+ sections.push(`${text.section("Options:")}\n${formatHelpOptionList(schemaGlobalRows)}\n${builtInLine}`);
1398
1409
  }
1399
1410
  else {
1400
1411
  sections.push(builtInLine);
@@ -1417,11 +1428,19 @@ function renderLeafHelp(command, breadcrumb, casing, globalOptions, rootUsageNam
1417
1428
  const globalLongOptionFlags = getGlobalLongOptionFlags(globalOptions.presetsEnabled, globalOptions.showVersion, globalOptions.controls);
1418
1429
  const collected = collectFields(command.params, casing, globalLongOptionFlags);
1419
1430
  const fields = assignPositionals(collected.fields, command.positional);
1420
- const optionRows = sortLeafHelpOptionFields(fields.filter((field) => field.global !== true))
1421
- .map((field) => createHelpOptionRow(formatHelpFieldFlags(field, globalLongOptionFlags), formatHelpFieldDescription(field)))
1431
+ const localFields = fields.filter((field) => field.global !== true);
1432
+ const toHelpRow = (field) => createHelpOptionRow(formatHelpFieldFlags(field, globalLongOptionFlags), formatHelpFieldDescription(field));
1433
+ const argumentRows = localFields
1434
+ .filter((field) => field.positionalIndex !== undefined)
1435
+ .map(toHelpRow);
1436
+ const optionRows = sortLeafHelpOptionFields(localFields.filter((field) => field.positionalIndex === undefined))
1437
+ .map(toHelpRow)
1422
1438
  .concat(collected.dynamicFields.flatMap((field) => formatDynamicHelpFields(field, casing)));
1439
+ if (argumentRows.length > 0) {
1440
+ sections.push(`${text.section("Arguments:")}\n${formatHelpOptionList(argumentRows)}`);
1441
+ }
1423
1442
  if (optionRows.length > 0) {
1424
- sections.push(`${text.sectionHeader("Options")}\n${formatHelpOptionList(optionRows)}`);
1443
+ sections.push(`${text.section("Options:")}\n${formatHelpOptionList(optionRows)}`);
1425
1444
  }
1426
1445
  const builtInLine = formatLeafGlobalOptionsLine(globalOptions);
1427
1446
  if (builtInLine.length > 0) {
@@ -1429,10 +1448,10 @@ function renderLeafHelp(command, breadcrumb, casing, globalOptions, rootUsageNam
1429
1448
  }
1430
1449
  const secretRows = formatSecretRows(command.secrets);
1431
1450
  if (secretRows.length > 0) {
1432
- sections.push(`${text.sectionHeader("Secrets (environment)")}\n${formatHelpOptionList(secretRows)}`);
1451
+ sections.push(`${text.section("Secrets (environment):")}\n${formatHelpOptionList(secretRows)}`);
1433
1452
  }
1434
1453
  if (command.examples.length > 0) {
1435
- sections.push(`${text.sectionHeader("Examples")}\n${formatExampleRows(command.examples, breadcrumb, rootUsageName).join("\n")}`);
1454
+ sections.push(`${text.section("Examples:")}\n${formatExampleRows(command.examples, breadcrumb, rootUsageName).join("\n")}`);
1436
1455
  }
1437
1456
  const positionalFields = fields.filter((f) => f.positionalIndex !== undefined);
1438
1457
  const usageSuffix = positionalFields.length > 0
@@ -1599,7 +1618,12 @@ function createNodeCommand(node, casing, globalLongOptionFlags, execute, presets
1599
1618
  }
1600
1619
  for (const field of fields) {
1601
1620
  if (field.positionalIndex !== undefined) {
1602
- command.argument(formatPositionalToken(field));
1621
+ // Positionals are declared optional so resolveParams stays the single owner of argument
1622
+ // validation: Commander would otherwise report a missing positional in its own words
1623
+ // before prompts, defaults, and presets have had a chance to supply the value.
1624
+ command.argument(field.variadicPosition === true
1625
+ ? `[${field.displayPath}...]`
1626
+ : `[${field.displayPath}]`);
1603
1627
  continue;
1604
1628
  }
1605
1629
  for (const option of createOption(field, globalLongOptionFlags)) {
@@ -2540,10 +2564,10 @@ function isHumanInLoopPending(result) {
2540
2564
  typeof result.message === "string" &&
2541
2565
  typeof result.enqueuedAt === "string");
2542
2566
  }
2543
- function renderHumanInLoopPending(pending) {
2567
+ function renderHumanInLoopPending(pending, rootUsageName) {
2544
2568
  process.stdout.write(`✓ Queued for human approval (id: ${pending.approvalId})\n` +
2545
2569
  ` Message: ${pending.message}\n` +
2546
- ` Track: toolcraft approvals show ${pending.approvalId}\n`);
2570
+ ` Track: ${rootUsageName} approvals show --approval-id ${pending.approvalId}\n`);
2547
2571
  }
2548
2572
  function renderApprovalDeclined(error) {
2549
2573
  const logger = createLogger();
@@ -2638,7 +2662,7 @@ function parseOptionFieldValue(field, value, errors) {
2638
2662
  return { ok: true, value: parsedValue };
2639
2663
  }
2640
2664
  catch (error) {
2641
- if (error instanceof UserError || error instanceof InvalidArgumentError) {
2665
+ if (isUserError(error) || error instanceof InvalidArgumentError) {
2642
2666
  errors.push({
2643
2667
  path: field.displayPath,
2644
2668
  message: error.message
@@ -3150,7 +3174,7 @@ async function resolveParams(fields, dynamicFields, variants, positionalValues,
3150
3174
  }
3151
3175
  errors.push({
3152
3176
  path: field.displayPath,
3153
- message: `Missing required parameter "${field.displayPath}".`
3177
+ message: formatMissingParameterMessage(field)
3154
3178
  });
3155
3179
  continue;
3156
3180
  }
@@ -3193,7 +3217,7 @@ function getResolvedFlags(command) {
3193
3217
  const flags = command.optsWithGlobals();
3194
3218
  return flags;
3195
3219
  }
3196
- async function executeCommand(state, services, requirementOptions, runtimeFetch, humanInLoop, runtimeEnv, runtimeFs, outputEmitter, outputFormats, promptStreams, diagnosticsOptions, onErrorReportContext) {
3220
+ async function executeCommand(state, rootUsageName, services, requirementOptions, runtimeFetch, humanInLoop, runtimeEnv, runtimeFs, outputEmitter, outputFormats, promptStreams, diagnosticsOptions, onErrorReportContext) {
3197
3221
  const logger = createLogger(outputEmitter);
3198
3222
  const optionValues = state.actionCommand.optsWithGlobals();
3199
3223
  const resolvedFlags = optionValues;
@@ -3334,7 +3358,7 @@ async function executeCommand(state, services, requirementOptions, runtimeFetch,
3334
3358
  writeRichHeader(`${state.command.name} (fixture)`);
3335
3359
  }
3336
3360
  if (isHumanInLoopPending(result)) {
3337
- renderHumanInLoopPending(result);
3361
+ renderHumanInLoopPending(result, rootUsageName);
3338
3362
  return;
3339
3363
  }
3340
3364
  const renderStatus = renderCLIResult(state.command, state.commandPath, result, output, primitives, outputFormats, outputEmitter === undefined
@@ -3571,7 +3595,7 @@ function renderHttpError(error, options) {
3571
3595
  async function handleRunError(error, options) {
3572
3596
  const logger = createLogger(options.outputEmitter);
3573
3597
  await withOutputFormat(options.output, async () => {
3574
- if (error instanceof UserError) {
3598
+ if (isUserError(error)) {
3575
3599
  renderCliErrorPattern(options.userErrorPattern === "definition"
3576
3600
  ? {
3577
3601
  kind: "definition",
@@ -3870,12 +3894,9 @@ function getDefaultCommanderCommandName(command) {
3870
3894
  function configureCommanderSuggestionOutput(command) {
3871
3895
  command.exitOverride();
3872
3896
  command.configureOutput({
3873
- outputError: (message, write) => {
3874
- if (message.includes("unknown command") || message.includes("unknown option")) {
3875
- return;
3876
- }
3877
- write(message);
3878
- }
3897
+ // Every Commander error throws through exitOverride and is rendered once by handleRunError
3898
+ // in the design-system error pattern; writing it here as well double-reports one mistake.
3899
+ outputError: () => { }
3879
3900
  });
3880
3901
  command.commands.forEach((child) => configureCommanderSuggestionOutput(child));
3881
3902
  }
@@ -3941,7 +3962,7 @@ export async function runCLI(roots, options = {}) {
3941
3962
  const execute = async (state) => {
3942
3963
  lastActionCommand = state.actionCommand;
3943
3964
  resolvedCommandPath = formatCliCommandPath(state.commandPath);
3944
- await executeCommand(state, servicesWithBuiltIns, requirementOptions, runtimeFetch, humanInLoop, options.env, options.fs, options.outputEmitter, controls.outputFormats, {
3965
+ await executeCommand(state, rootUsageName, servicesWithBuiltIns, requirementOptions, runtimeFetch, humanInLoop, options.env, options.fs, options.outputEmitter, controls.outputFormats, {
3945
3966
  input: options.promptInput,
3946
3967
  output: options.promptOutput
3947
3968
  }, {
@@ -36,6 +36,11 @@
36
36
  "version": "0.0.1",
37
37
  "license": "MIT"
38
38
  },
39
+ {
40
+ "name": "@poe-code/user-error",
41
+ "version": "0.0.1",
42
+ "license": "MIT"
43
+ },
39
44
  {
40
45
  "name": "auth-store",
41
46
  "version": "0.0.1",
@@ -108,7 +113,7 @@
108
113
  },
109
114
  {
110
115
  "name": "toolcraft",
111
- "version": "0.0.136",
116
+ "version": "0.0.138",
112
117
  "license": "MIT"
113
118
  },
114
119
  {
@@ -118,7 +123,7 @@
118
123
  },
119
124
  {
120
125
  "name": "toolcraft-schema",
121
- "version": "0.0.136",
126
+ "version": "0.0.138",
122
127
  "license": "MIT"
123
128
  },
124
129
  {
@@ -3,12 +3,14 @@ import { S } from "toolcraft-schema";
3
3
  import { hasOwnErrorCode } from "../error-codes.js";
4
4
  import { UserError, defineCommand, defineGroup } from "../index.js";
5
5
  import { ensureApprovalList } from "./approval-tasks.js";
6
+ import { approvalStateMachine } from "./state-machine.js";
6
7
  import { runApproval } from "./runner.js";
7
8
  const approvalsGroupSymbol = Symbol("toolcraft.humanInLoop.approvalsBuiltIn");
8
9
  const listScope = ["cli", "mcp", "sdk"];
9
10
  const runScope = ["cli"];
11
+ const approvalStateValues = approvalStateMachine.states;
10
12
  const listParams = S.Object({
11
- state: S.Optional(S.String())
13
+ state: S.Optional(S.Array(S.Enum(approvalStateValues)))
12
14
  });
13
15
  const showParams = S.Object({
14
16
  approvalId: S.String()
@@ -51,18 +53,12 @@ export const approvalsGroup = markApprovalsBuiltIn(defineGroup({
51
53
  scope: listScope,
52
54
  params: showParams,
53
55
  handler: async ({ params, humanInLoop }) => {
54
- try {
56
+ return withMissingApprovalError(params.approvalId, async () => {
55
57
  const { tasks } = await ensureApprovalList(humanInLoop.runtimeOptions, {
56
58
  create: false
57
59
  });
58
60
  return tasks.get(params.approvalId);
59
- }
60
- catch (error) {
61
- if (isMissingStateError(error)) {
62
- throw new TaskNotFoundError(`Task "approvals/${params.approvalId}" not found.`);
63
- }
64
- throw error;
65
- }
61
+ });
66
62
  },
67
63
  render: {
68
64
  rich: (result, primitives) => renderApprovalDetails(result, primitives),
@@ -76,7 +72,7 @@ export const approvalsGroup = markApprovalsBuiltIn(defineGroup({
76
72
  scope: runScope,
77
73
  params: runParams,
78
74
  handler: async ({ params, humanInLoop, root }) => {
79
- return runApproval(params.approvalId, humanInLoop.runtimeOptions, root);
75
+ return withMissingApprovalError(params.approvalId, async () => runApproval(params.approvalId, humanInLoop.runtimeOptions, root));
80
76
  },
81
77
  render: {
82
78
  rich: (result, primitives) => {
@@ -115,8 +111,7 @@ function isApprovalsBuiltIn(node) {
115
111
  return (node.kind === "group" &&
116
112
  node[approvalsGroupSymbol] === true);
117
113
  }
118
- async function loadApprovals(tasks, stateFilter) {
119
- const states = splitStateFilter(stateFilter);
114
+ async function loadApprovals(tasks, states = []) {
120
115
  if (states.length === 0) {
121
116
  return tasks.all();
122
117
  }
@@ -136,22 +131,6 @@ async function loadApprovals(tasks, stateFilter) {
136
131
  }
137
132
  return approvals;
138
133
  }
139
- function splitStateFilter(stateFilter) {
140
- if (stateFilter === undefined) {
141
- return [];
142
- }
143
- const seen = new Set();
144
- const states = [];
145
- for (const value of stateFilter.split(",")) {
146
- const trimmed = value.trim();
147
- if (trimmed.length === 0 || seen.has(trimmed)) {
148
- continue;
149
- }
150
- seen.add(trimmed);
151
- states.push(trimmed);
152
- }
153
- return states;
154
- }
155
134
  function renderApprovalList(result, { logger, renderTable, getTheme }) {
156
135
  if (result.length === 0) {
157
136
  logger.message("No approvals found.");
@@ -227,6 +206,17 @@ function stringifyValue(value) {
227
206
  function escapeMarkdownCell(value) {
228
207
  return value.replaceAll("|", "\\|");
229
208
  }
209
+ async function withMissingApprovalError(approvalId, run) {
210
+ try {
211
+ return await run();
212
+ }
213
+ catch (error) {
214
+ if (error instanceof TaskNotFoundError || isMissingStateError(error)) {
215
+ throw new UserError(`Approval "${approvalId}" not found. Run approvals list to see queued approvals.`);
216
+ }
217
+ throw error;
218
+ }
219
+ }
230
220
  function isMissingStateError(error) {
231
221
  return hasOwnErrorCode(error, "ENOENT");
232
222
  }
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@ import type { AnySchema, ObjectSchema, Static } from "toolcraft-schema";
3
3
  import type { LoggerOutput, RenderTableOptions, ThemePalette } from "toolcraft-design";
4
4
  import { ApprovalDeclinedError } from "./human-in-loop/types.js";
5
5
  import type { HumanInLoopConfig, HumanInLoopPending, HumanInLoopRuntime } from "./human-in-loop/types.js";
6
- import { ToolcraftBugError, UserError } from "./user-error.js";
6
+ import { ToolcraftBugError, UserError, isUserError } from "./user-error.js";
7
7
  import type { RuntimeLogger } from "./runtime-logging.js";
8
8
  import type { StreamStatusEvent, ToolcraftStream } from "./stream.js";
9
9
  export { createManagedStream } from "./stream.js";
@@ -227,7 +227,8 @@ export declare function hasMcpProxyConfig(group: Group<any>): boolean;
227
227
  export { S, toJsonSchema } from "./schema.js";
228
228
  export { AuthenticationError, BadRequestError, ClientError, ConflictError, HttpError, InternalServerError, NotFoundError, PermissionDeniedError, RateLimitError, ServerError, ServiceUnavailableError, UnprocessableEntityError, createHttpError } from "./http-errors.js";
229
229
  export type { HttpErrorRequest, HttpErrorResponse } from "./http-errors.js";
230
- export { ApprovalDeclinedError, ToolcraftBugError, UserError };
230
+ export { ApprovalDeclinedError, ToolcraftBugError, UserError, isUserError };
231
+ export { suggest } from "./suggest.js";
231
232
  export { createRuntimeLogger, isLogLevel, shouldEmitDiagnostic } from "./runtime-logging.js";
232
233
  export { findPackageMetadata, packageMetadata } from "./package-metadata.js";
233
234
  export type { PackageMetadata } from "./package-metadata.js";
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
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 { ToolcraftBugError, UserError } from "./user-error.js";
4
+ import { ToolcraftBugError, UserError, isUserError } from "./user-error.js";
5
5
  import { suggest } from "./suggest.js";
6
6
  export { createManagedStream } from "./stream.js";
7
7
  const commandConfigSymbol = Symbol("toolcraft.command.config");
@@ -500,6 +500,7 @@ export function hasMcpProxyConfig(group) {
500
500
  }
501
501
  export { S, toJsonSchema } from "./schema.js";
502
502
  export { AuthenticationError, BadRequestError, ClientError, ConflictError, HttpError, InternalServerError, NotFoundError, PermissionDeniedError, RateLimitError, ServerError, ServiceUnavailableError, UnprocessableEntityError, createHttpError } from "./http-errors.js";
503
- export { ApprovalDeclinedError, ToolcraftBugError, UserError };
503
+ export { ApprovalDeclinedError, ToolcraftBugError, UserError, isUserError };
504
+ export { suggest } from "./suggest.js";
504
505
  export { createRuntimeLogger, isLogLevel, shouldEmitDiagnostic } from "./runtime-logging.js";
505
506
  export { findPackageMetadata, packageMetadata } from "./package-metadata.js";
package/dist/renderer.js CHANGED
@@ -142,6 +142,9 @@ function compactUrl(value) {
142
142
  return value;
143
143
  }
144
144
  }
145
+ function stackedList(value) {
146
+ return value.map((entry) => displayScalar(entry)).join("\n") || "—";
147
+ }
145
148
  function displayRowValue(value) {
146
149
  return isUrl(value) ? compactUrl(value) : displayScalar(value);
147
150
  }
@@ -176,7 +179,7 @@ function renderObjectCard(result, primitives, title) {
176
179
  const arrayObjectSections = directArrayObjectSections(result);
177
180
  const listRows = Object.entries(result)
178
181
  .filter(([, value]) => Array.isArray(value) && !isNonEmptyArrayOfObjects(value))
179
- .map(([key, value]) => ({ label: humanizeKey(key), value: displayScalar(value) }));
182
+ .map(([key, value]) => ({ label: humanizeKey(key), value: stackedList(value) }));
180
183
  return renderDetailCard({
181
184
  theme: primitives.getTheme(),
182
185
  title,
@@ -1,6 +1,14 @@
1
1
  export declare class UserError extends Error {
2
2
  constructor(message: string, options?: ErrorOptions);
3
3
  }
4
+ /**
5
+ * Detects user errors by name as well as identity, so an instance created in
6
+ * another bundle (for example `@poe-code/user-error`, thrown by packages that do
7
+ * not depend on toolcraft) is still rendered as guidance where `instanceof`
8
+ * would fail - while a genuine `UserError` stays one even if its name was
9
+ * overwritten.
10
+ */
11
+ export declare function isUserError(error: unknown): error is Error;
4
12
  export declare class ToolcraftBugError extends Error {
5
13
  constructor(message: string);
6
14
  }
@@ -4,6 +4,16 @@ export class UserError extends Error {
4
4
  this.name = "UserError";
5
5
  }
6
6
  }
7
+ /**
8
+ * Detects user errors by name as well as identity, so an instance created in
9
+ * another bundle (for example `@poe-code/user-error`, thrown by packages that do
10
+ * not depend on toolcraft) is still rendered as guidance where `instanceof`
11
+ * would fail - while a genuine `UserError` stays one even if its name was
12
+ * overwritten.
13
+ */
14
+ export function isUserError(error) {
15
+ return error instanceof UserError || (error instanceof Error && error.name === "UserError");
16
+ }
7
17
  export class ToolcraftBugError extends Error {
8
18
  constructor(message) {
9
19
  super(message);
@@ -4,7 +4,8 @@ Shared catalog of supported coding-agent definitions.
4
4
 
5
5
  This package owns the declarative agent metadata used by CLI, SDK, spawn,
6
6
  configuration, and UI surfaces: stable ids, labels, aliases, config paths,
7
- binary names, API-shape support, OTEL capture wiring, and brand colors.
7
+ binary names, API-shape support, OTEL capture wiring, brand colors, and the
8
+ capability matrix that says which commands accept each agent.
8
9
 
9
10
  ## Usage
10
11
 
@@ -24,6 +25,18 @@ const codex = allAgents.find((agent) => agent.id === "codex");
24
25
  - `formatAgentSpecifier(specifier)`: formats an agent specifier.
25
26
  - `normalizeAgentId(input)`: normalizes the agent part through the registry.
26
27
  - Agent definition exports such as `codexAgent`, `claudeCodeAgent`, `geminiCliAgent`, and `piAgent`.
28
+ - `listAgentsWithCapability(capability, options?)`: ids supporting a capability, optionally including aliases.
29
+ - `agentSupportsCapability(input, capability)`: resolves aliases, then checks the matrix.
30
+ - `formatAgentCapabilityError({ agent, capability })`: the shared message for a rejected agent argument.
31
+
32
+ ## Capability Matrix
33
+
34
+ Each `AgentDefinition` declares its `capabilities` (`spawn`, `configure`,
35
+ `install`, `test`, `skill`, `mcp`). This is the single published source for every
36
+ command's allow-list, so `spawn`/`configure`/`install`/`test`/`skill` cannot
37
+ drift apart. `src/cli/commands/agent-capability-matrix.test.ts` pins each
38
+ capability to the registry that implements it, and fails if a registry gains or
39
+ loses an agent without the matrix being updated.
27
40
 
28
41
  ## Config Options
29
42
 
@@ -12,6 +12,7 @@ export const claudeCodeAgent = {
12
12
  }
13
13
  },
14
14
  configPath: "~/.claude.json",
15
+ capabilities: ["spawn", "configure", "install", "test", "skill", "mcp"],
15
16
  branding: {
16
17
  colors: {
17
18
  dark: "#C15F3C",
@@ -9,6 +9,7 @@ export const claudeDesktopAgent = {
9
9
  linux: "~/.config/Claude/claude_desktop_config.json",
10
10
  win32: "~/AppData/Roaming/Claude/claude_desktop_config.json"
11
11
  },
12
+ capabilities: ["mcp"],
12
13
  branding: {
13
14
  colors: {
14
15
  dark: "#D97757",
@@ -16,6 +16,7 @@ export const codexAgent = {
16
16
  ]
17
17
  },
18
18
  configPath: "~/.codex/config.toml",
19
+ capabilities: ["spawn", "configure", "install", "test", "skill", "mcp"],
19
20
  branding: {
20
21
  colors: {
21
22
  dark: "#D5D9DF",
@@ -6,6 +6,7 @@ export const cursorAgent = {
6
6
  summary: "Cursor's CLI coding agent.",
7
7
  binaryName: "cursor-agent",
8
8
  configPath: "~/.cursor/mcp.json",
9
+ capabilities: ["spawn", "configure", "install", "test", "skill", "mcp"],
9
10
  branding: {
10
11
  colors: {
11
12
  dark: "#FFFFFF",
@@ -7,6 +7,7 @@ export const geminiCliAgent = {
7
7
  binaryName: "gemini",
8
8
  configPath: "~/.gemini/settings.json",
9
9
  apiShapes: ["google-generations"],
10
+ capabilities: ["spawn", "configure", "install", "test", "skill"],
10
11
  branding: {
11
12
  colors: {
12
13
  dark: "#8AB4F8",
@@ -7,6 +7,7 @@ export const gooseAgent = {
7
7
  apiShapes: ["openai-chat-completions"],
8
8
  otelCapture: {},
9
9
  configPath: "~/.config/goose/config.yaml",
10
+ capabilities: ["spawn", "configure", "install", "test", "skill", "mcp"],
10
11
  branding: {
11
12
  colors: {
12
13
  dark: "#FF6B35",
@@ -7,6 +7,7 @@ export const kimiAgent = {
7
7
  binaryName: "kimi",
8
8
  apiShapes: ["openai-chat-completions"],
9
9
  configPath: "~/.kimi/mcp.json",
10
+ capabilities: ["spawn", "configure", "install", "test", "mcp"],
10
11
  branding: {
11
12
  colors: {
12
13
  dark: "#7B68EE",
@@ -11,6 +11,7 @@ export const openCodeAgent = {
11
11
  }
12
12
  },
13
13
  configPath: "~/.config/opencode/opencode.json",
14
+ capabilities: ["spawn", "configure", "install", "test", "skill", "mcp"],
14
15
  branding: {
15
16
  colors: {
16
17
  dark: "#4A4F55",
@@ -5,6 +5,7 @@ export const piAgent = {
5
5
  label: "Pi",
6
6
  summary: "Minimal AI coding agent for the terminal.",
7
7
  binaryName: "pi",
8
+ capabilities: ["spawn"],
8
9
  branding: {
9
10
  colors: {
10
11
  dark: "#F2F2F2",
@@ -5,6 +5,7 @@ export const poeAgentAgent = {
5
5
  summary: "Run one-shot prompts with the built-in Poe agent runtime.",
6
6
  apiShapes: ["openai-responses", "openai-chat-completions"],
7
7
  configPath: "~/.poe-code/config.json",
8
+ capabilities: ["configure"],
8
9
  branding: {
9
10
  colors: {
10
11
  dark: "#A465F7",
@@ -0,0 +1,14 @@
1
+ import type { AgentCapability } from "./types.js";
2
+ export declare function listAgentsWithCapability(capability: AgentCapability, options?: {
3
+ includeAliases?: boolean;
4
+ }): readonly string[];
5
+ export declare function agentSupportsCapability(input: string, capability: AgentCapability): boolean;
6
+ /**
7
+ * The single message for every agent argument that misses. It distinguishes a
8
+ * typo (unknown id, plus a did-you-mean) from a real capability gap ("pi
9
+ * supports: spawn"), and always names the agents the command does accept.
10
+ */
11
+ export declare function formatAgentCapabilityError(input: {
12
+ agent: string;
13
+ capability: AgentCapability;
14
+ }): string;