toolcraft 0.0.362 → 0.0.364

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/README.md CHANGED
@@ -13,11 +13,95 @@ You have one-off scripts and MCP servers. Each one re-derives argument parsing,
13
13
  - `runCLI` — argv parsing, `--help`, kebab/snake flags, exit codes.
14
14
  - `createMCPServer` / `runMCP` — JSON-RPC over stdio with auto-generated tool schemas.
15
15
  - `createSDK` — typed in-process function calls.
16
+ - `toolcraftCommands` — register a whole library in Safe Bash with captured help, local status and streaming output.
16
17
 
17
18
  Same handler runs everywhere. Schema, secrets, preconditions, and human-in-loop gating are declared once.
18
19
 
19
20
  Building from an OpenAPI spec? Use [`toolcraft-openapi`](../toolcraft-openapi/) to generate toolcraft commands from the API contract.
20
21
 
22
+ ## Use a library inside Safe Bash
23
+
24
+ Install `toolcraft` and `@poe-platform/safe-bash` in the application that owns the shell.
25
+
26
+ ```ts
27
+ import { Shell, createMemoryFileSystem } from "@poe-platform/safe-bash";
28
+ import { toolcraftCommands, toolcraftDefaults } from "toolcraft/safe-bash";
29
+
30
+ const shell = new Shell({ fs: createMemoryFileSystem(), env: {} });
31
+ shell.use(toolcraftCommands(myToolcraftLibrary));
32
+ const result = await shell.exec("tools list-users --limit 10 --output json");
33
+ await shell.dispose();
34
+ ```
35
+
36
+ Each root name and alias becomes a virtual command. Nested groups, command aliases,
37
+ default commands, positional arguments, schema defaults, scalar/enum/array/object/
38
+ record/union inputs and CLI scope filtering reuse Toolcraft's CLI parser. Hidden
39
+ commands are excluded from this surface. Standalone CLI behavior remains unchanged.
40
+ No CLI subprocess or per-tool adapter is needed.
41
+
42
+ Configure services or declared parameter defaults once for the library:
43
+
44
+ ```ts
45
+ shell.use(toolcraftCommands(myToolcraftLibrary, {
46
+ services: { baseUrl: "https://api.example.com", api: existingApiClient },
47
+ defaults: toolcraftDefaults(myToolcraftLibrary, {
48
+ "users/list_users": { limit: 25 },
49
+ }),
50
+ version: "1.0.0",
51
+ }));
52
+ ```
53
+
54
+ The `toolcraftDefaults` helper infers declaration paths and parameter types from
55
+ `defineGroup`/`defineCommand` metadata; keep child arrays as tuples (`as const`) for
56
+ precise inference. Runtime defaults also accept a record. Paths use original
57
+ names, separated by `/`, relative to a single root; arrays of roots require the
58
+ root name prefix. Aliases share the same defaults. Unknown paths/parameters and
59
+ invalid values fail at registration. Schema defaults come first, then plugin
60
+ defaults, then explicit argv (including false, zero, empty strings and null).
61
+ Arrays and objects replace the lower-priority value as a whole. Defaults are
62
+ snapshotted, and resolved parameters are validated before requirements and approvals.
63
+ Service objects retain their client identities and cannot override runtime fields.
64
+
65
+ `createToolcraftCommandExecutor(library, options).execute(argv, invocation)` is the
66
+ same public execution contract without plugin registration. A single-root executor
67
+ accepts arguments after the root command; a multiple-root executor expects the root
68
+ name/alias as its first token. `ToolcraftInvocation` supplies async byte sinks,
69
+ stdin, cancellation, virtual filesystem/cwd, explicit environment, services and
70
+ optional authorized fetch/human-in-loop capabilities. The result is `{ exitCode }`.
71
+ Help/version/results use stdout; errors and diagnostics use stderr. Both sinks are
72
+ awaited, and streaming commands wait for writes before consuming the next event.
73
+ Cancellation preserves the abort reason and closes the active iterator. A sink
74
+ failure propagates to the shell, including its output budget failures. The plugin
75
+ never sets host exit status, installs host signal listeners or prompts host stdin.
76
+ `confirm` commands require explicit `--yes`; wire approval policy with the optional
77
+ `humanInLoop` runtime. `controls` configures existing output formats and log controls.
78
+
79
+ Handlers are trusted application code; this adapter does not sandbox JavaScript.
80
+ The filesystem bridge uses the invocation's virtual cwd and original filesystem,
81
+ including middleware and budget state. Text reads/writes support Node encodings;
82
+ writes support `w`, `wx` and `a`. Other flags fail explicitly. Missing fetch is
83
+ denied; there is no native network fallback. Callers own supplied capabilities;
84
+ plugin disposal does not close them. Synchronous rendering is bounded to 1 MiB of
85
+ pending output; use `defineStreamCommand` or the invocation's async sinks for larger
86
+ outputs. Binary/artifact results have no automatic binary renderer; existing MCP
87
+ structured/text rendering is retained.
88
+
89
+ Native plugins currently exclude MCP proxy discovery, host fixture loading,
90
+ file presets, interactive parameter resolvers and detached approval runners.
91
+ Use an explicitly supplied in-process approval provider; providers that spawn a
92
+ runner remain application code. Shell `capabilities` supplies borrowed `services`,
93
+ authorized `fetch`, and `humanInLoop` to each library automatically. Per-execution
94
+ capabilities override shell defaults; explicit `undefined` revokes a capability.
95
+ `agentCommands({ regexExecutor, regex })` also supplies its exact bounded regex
96
+ provider and effective limits as `ctx.regex.executor` and `ctx.regex.limits`.
97
+ `BoundedRegexProvider`, `RegexExecutionOptions`, and `RegexExecutor` are public
98
+ safe-bash exports; missing regex never gains a native JavaScript fallback. Native
99
+ handlers can use `signal`, byte `stdin`/`stdout`/`stderr`, `cwd`, `inputBudget`,
100
+ `invoke` and `registerCleanup`. Dry-run readback uses an explicitly supplied mock
101
+ or overlay filesystem. Library services combine with invocation services; runtime
102
+ capability names are reserved and cannot be replaced through services.
103
+ Schema pattern validation currently follows Toolcraft's existing validator.
104
+
21
105
  ## What the owner decides
22
106
 
23
107
  Before writing toolcraft code, make a small tool map. For each script or MCP tool, write down:
package/composition.json CHANGED
@@ -113,7 +113,7 @@
113
113
  },
114
114
  {
115
115
  "name": "toolcraft",
116
- "version": "0.0.362",
116
+ "version": "0.0.364",
117
117
  "license": "MIT"
118
118
  },
119
119
  {
@@ -123,7 +123,7 @@
123
123
  },
124
124
  {
125
125
  "name": "toolcraft-schema",
126
- "version": "0.0.362",
126
+ "version": "0.0.364",
127
127
  "license": "MIT"
128
128
  },
129
129
  {
package/dist/cli.d.ts CHANGED
@@ -54,6 +54,15 @@ export interface RunCLIOptions<TServices extends object = Record<string, unknown
54
54
  presets?: boolean;
55
55
  errorReports?: ErrorReportsOption;
56
56
  }
57
+ /** Invocation-local transport used by the native command executor. */
58
+ export interface CLIInvocationRuntime {
59
+ signal: AbortSignal;
60
+ write(chunk: string, stream?: "stdout" | "stderr"): void;
61
+ flush(): Promise<void>;
62
+ exitCode: number;
63
+ defaults?: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
64
+ capabilities?: Readonly<Record<string, unknown>>;
65
+ }
57
66
  export interface CLICommandTreeSnapshotOption {
58
67
  name: string;
59
68
  flags: string[];
@@ -112,3 +121,5 @@ export interface CLICommandTreeSnapshotOptions {
112
121
  */
113
122
  export declare function createCLICommandTreeSnapshot<TServices extends object>(roots: Group<TServices> | Group<TServices>[], options?: CLICommandTreeSnapshotOptions): Promise<CLICommandTreeSnapshot>;
114
123
  export declare function runCLI<TServices extends object = Record<string, unknown>>(roots: Group<TServices> | Group<TServices>[], options?: RunCLIOptions<TServices>): Promise<void>;
124
+ /** Shared parser, validation and dispatch; native callers supply a local runtime. */
125
+ export declare function executeCLICommand<TServices extends object>(roots: Group<TServices> | Group<TServices>[], options: RunCLIOptions<TServices>, invocation?: CLIInvocationRuntime): Promise<void>;
package/dist/cli.js CHANGED
@@ -112,17 +112,18 @@ function splitWords(value) {
112
112
  }
113
113
  return words;
114
114
  }
115
- function formatSegment(segment, casing) {
115
+ /** @internal */
116
+ export function formatCLIName(segment, casing) {
116
117
  const separator = casing === "snake" ? "_" : "-";
117
118
  return splitWords(segment).join(separator);
118
119
  }
119
120
  function toOptionFlag(path, casing) {
120
- return `--${path.map((segment) => formatSegment(segment, casing)).join(".")}`;
121
+ return `--${path.map((segment) => formatCLIName(segment, casing)).join(".")}`;
121
122
  }
122
123
  function toOptionAttribute(path, casing) {
123
124
  return path
124
125
  .map((segment) => {
125
- const formatted = formatSegment(segment, casing);
126
+ const formatted = formatCLIName(segment, casing);
126
127
  if (casing === "snake") {
127
128
  return formatted;
128
129
  }
@@ -164,7 +165,7 @@ function createSyntheticEnumSchema(values) {
164
165
  function getRequiredBranchFingerprint(branch, casing) {
165
166
  const requiredKeys = Object.entries(branch.shape)
166
167
  .filter(([, schema]) => schema.kind !== "optional")
167
- .map(([key]) => formatSegment(key, casing))
168
+ .map(([key]) => formatCLIName(key, casing))
168
169
  .sort();
169
170
  return requiredKeys.join("+");
170
171
  }
@@ -1207,7 +1208,7 @@ function collectDynamicObjectHelpRows(schema, casing, optionPrefix, displayPrefi
1207
1208
  const rows = [];
1208
1209
  for (const [key, rawChildSchema] of Object.entries(schema.shape)) {
1209
1210
  const childSchema = unwrapOptional(rawChildSchema);
1210
- const optionFlag = `${optionPrefix}.${formatSegment(key, casing)}`;
1211
+ const optionFlag = `${optionPrefix}.${formatCLIName(key, casing)}`;
1211
1212
  const displayPath = `${displayPrefix}.${key}`;
1212
1213
  const rawDescription = childSchema.description ?? displayPath;
1213
1214
  const description = suppressEchoHelpDescription(rawDescription, displayPath);
@@ -1740,14 +1741,15 @@ function renderHelpDocument(input) {
1740
1741
  lines.push(renderHelpSections(input.sections));
1741
1742
  return `${lines.join("\n").trimEnd()}\n`;
1742
1743
  }
1743
- async function renderGeneratedHelp(root, argv, options) {
1744
+ async function renderGeneratedHelp(root, argv, options, invocation) {
1745
+ const write = invocation?.write ?? ((chunk) => { process.stdout.write(chunk); });
1744
1746
  const output = resolveHelpOutput(argv);
1745
1747
  const casing = options.casing ?? "kebab";
1746
1748
  const rootUsageName = options.rootUsageName ?? inferProgramName(argv);
1747
1749
  const target = resolveHelpTarget(root, argv, "cli", rootUsageName, options.rootDisplayName);
1748
1750
  const controls = resolveCLIControls(options.controls);
1749
1751
  if (output === "json") {
1750
- process.stdout.write(renderJsonHelp(target, root, casing, {
1752
+ write(renderJsonHelp(target, root, casing, {
1751
1753
  controls,
1752
1754
  showVersion: options.version !== undefined,
1753
1755
  presetsEnabled: options.presets === true
@@ -1766,7 +1768,7 @@ async function renderGeneratedHelp(root, argv, options) {
1766
1768
  showVersion: options.version !== undefined,
1767
1769
  presetsEnabled: options.presets === true
1768
1770
  }, rootUsageName);
1769
- process.stdout.write(rendered);
1771
+ write(rendered);
1770
1772
  });
1771
1773
  }
1772
1774
  function createNodeCommand(node, casing, globalLongOptionFlags, execute, presetsEnabled, controls, fieldLoaders, pathSegments = []) {
@@ -1832,6 +1834,7 @@ function createNodeCommand(node, casing, globalLongOptionFlags, execute, presets
1832
1834
  await execute({
1833
1835
  command: node,
1834
1836
  commandPath: nextPathSegments.join("."),
1837
+ declarationPath: nextPathSegments,
1835
1838
  casing,
1836
1839
  dynamicFields: collected.dynamicFields,
1837
1840
  fields,
@@ -2723,8 +2726,8 @@ function createFixtureEnvValues(command) {
2723
2726
  }
2724
2727
  return values;
2725
2728
  }
2726
- async function resolveFixtureRuntime(command, services, requirementOptions, runtimeFetch, runtimeEnv, runtimeFs) {
2727
- const selector = process.env.TOOLCRAFT_FIXTURE;
2729
+ async function resolveFixtureRuntime(command, services, requirementOptions, runtimeFetch, runtimeEnv, runtimeFs, embedded = false) {
2730
+ const selector = embedded ? undefined : process.env.TOOLCRAFT_FIXTURE;
2728
2731
  if (selector === undefined || selector.length === 0) {
2729
2732
  return {
2730
2733
  env: createEnv(runtimeEnv),
@@ -2966,12 +2969,12 @@ function resolveDynamicLeaf(schema, rawSegments, casing, outputPath = [], displa
2966
2969
  case "object": {
2967
2970
  const [head, ...rest] = rawSegments;
2968
2971
  for (const [key, childSchema] of Object.entries(unwrappedSchema.shape)) {
2969
- if (formatSegment(key, casing) !== head) {
2972
+ if (formatCLIName(key, casing) !== head) {
2970
2973
  continue;
2971
2974
  }
2972
2975
  return resolveDynamicLeaf(childSchema, rest, casing, [...outputPath, key], [...displayPath, key], displayPathPrefix);
2973
2976
  }
2974
- throw new UserError(`Unknown parameter "${qualifyDisplayPath(displayPathPrefix, [...displayPath, head].join("."))}". ${formatAvailableList(Object.keys(unwrappedSchema.shape).map((key) => qualifyDisplayPath(displayPathPrefix, toDisplayPath([...displayPath, formatSegment(key, casing)]))))}`);
2977
+ throw new UserError(`Unknown parameter "${qualifyDisplayPath(displayPathPrefix, [...displayPath, head].join("."))}". ${formatAvailableList(Object.keys(unwrappedSchema.shape).map((key) => qualifyDisplayPath(displayPathPrefix, toDisplayPath([...displayPath, formatCLIName(key, casing)]))))}`);
2975
2978
  }
2976
2979
  case "record": {
2977
2980
  const [head, ...rest] = rawSegments;
@@ -3123,7 +3126,7 @@ function resolveDynamicOption(dynamicFields, flagName, casing) {
3123
3126
  const match = [...dynamicFields]
3124
3127
  .sort((left, right) => right.optionPath.length - left.optionPath.length)
3125
3128
  .find((field) => {
3126
- const optionPath = field.optionPath.map((segment) => formatSegment(segment, casing));
3129
+ const optionPath = field.optionPath.map((segment) => formatCLIName(segment, casing));
3127
3130
  return flagPath.length > optionPath.length &&
3128
3131
  optionPath.every((segment, index) => flagPath[index] === segment);
3129
3132
  });
@@ -3317,11 +3320,28 @@ async function enforceVariantConstraints(params, fields, dynamicFields, variants
3317
3320
  }
3318
3321
  }
3319
3322
  }
3320
- async function resolveParams(fields, dynamicFields, variants, positionalValues, optionValues, rawArgv, casing, presetPath, shouldPrompt, missingParameterContext, promptStreams) {
3323
+ async function resolveParams(fields, dynamicFields, variants, positionalValues, optionValues, rawArgv, casing, presetPath, shouldPrompt, missingParameterContext, promptStreams, parameterDefaults = {}) {
3321
3324
  const params = {};
3322
3325
  const errors = [];
3323
3326
  const dynamicResults = parseDynamicValues(dynamicFields, rawArgv, casing, errors);
3324
3327
  const positionalTokens = [...positionalValues, ...dynamicResults.positionals];
3328
+ const fieldRoot = (field) => field.synthetic
3329
+ ? (field.path.length === 1 ? field.path[0].slice(0, -"Kind".length) : field.path[0])
3330
+ : field.path[0];
3331
+ const explicitRoots = new Set();
3332
+ for (const field of fields) {
3333
+ if (hasFieldValue(optionValues[field.commanderOptionAttribute]) ||
3334
+ (field.positionalIndex !== undefined && positionalTokens[field.positionalIndex] !== undefined)) {
3335
+ explicitRoots.add(fieldRoot(field));
3336
+ }
3337
+ }
3338
+ for (const field of dynamicFields) {
3339
+ if (dynamicResults.values.has(field.id))
3340
+ explicitRoots.add(field.path[0]);
3341
+ }
3342
+ const defaultRoots = new Set(Object.keys(parameterDefaults).filter(key => !explicitRoots.has(key)));
3343
+ for (const key of defaultRoots)
3344
+ params[key] = cloneDefaultValue(parameterDefaults[key]);
3325
3345
  const positionalFields = fields.filter((field) => field.positionalIndex !== undefined);
3326
3346
  if (!positionalFields.some((field) => field.variadicPosition === true) &&
3327
3347
  positionalTokens.length > positionalFields.length) {
@@ -3333,6 +3353,8 @@ async function resolveParams(fields, dynamicFields, variants, positionalValues,
3333
3353
  const providedFieldIds = new Set();
3334
3354
  const resolvedFieldValues = new Map();
3335
3355
  for (const field of fields) {
3356
+ if (defaultRoots.has(fieldRoot(field)))
3357
+ continue;
3336
3358
  let value;
3337
3359
  let resolvedMissing = false;
3338
3360
  let source;
@@ -3463,6 +3485,8 @@ async function resolveParams(fields, dynamicFields, variants, positionalValues,
3463
3485
  }
3464
3486
  }
3465
3487
  for (const field of dynamicFields) {
3488
+ if (defaultRoots.has(field.path[0]))
3489
+ continue;
3466
3490
  let value = dynamicResults.values.get(field.id);
3467
3491
  if (presetValues.dynamic.has(field.id)) {
3468
3492
  if (value === undefined)
@@ -3484,7 +3508,7 @@ async function resolveParams(fields, dynamicFields, variants, positionalValues,
3484
3508
  }
3485
3509
  setNestedValue(params, field.path, value);
3486
3510
  }
3487
- await enforceVariantConstraints(params, fields, dynamicFields, variants, resolvedFieldValues, dynamicResults.providedFieldIds, providedFieldIds, shouldPrompt, errors, promptStreams);
3511
+ await enforceVariantConstraints(params, fields, dynamicFields, variants.filter(variant => !defaultRoots.has(fieldRoot(fields.find(field => field.id === variant.controlFieldId)))), resolvedFieldValues, dynamicResults.providedFieldIds, providedFieldIds, shouldPrompt, errors, promptStreams);
3488
3512
  throwValidationErrors(errors);
3489
3513
  return params;
3490
3514
  }
@@ -3499,7 +3523,7 @@ function getResolvedFlags(command) {
3499
3523
  }
3500
3524
  return flags;
3501
3525
  }
3502
- async function executeCommand(state, rootUsageName, services, requirementOptions, runtimeFetch, humanInLoop, runtimeEnv, runtimeFs, outputEmitter, outputFormats, promptStreams, diagnosticsOptions, onErrorReportContext) {
3526
+ async function executeCommand(state, rootUsageName, services, requirementOptions, runtimeFetch, humanInLoop, runtimeEnv, runtimeFs, outputEmitter, outputFormats, promptStreams, diagnosticsOptions, onErrorReportContext, invocation) {
3503
3527
  const logger = createLogger(outputEmitter);
3504
3528
  const optionValues = getResolvedFlags(state.actionCommand);
3505
3529
  const resolvedFlags = optionValues;
@@ -3508,7 +3532,7 @@ async function executeCommand(state, rootUsageName, services, requirementOptions
3508
3532
  logger,
3509
3533
  renderTable,
3510
3534
  getTheme,
3511
- note,
3535
+ note: invocation ? (message, title) => note(message, title, invocation.write) : note,
3512
3536
  outputFormat: output
3513
3537
  };
3514
3538
  const diagnostics = createRuntimeLogger({
@@ -3516,10 +3540,12 @@ async function executeCommand(state, rootUsageName, services, requirementOptions
3516
3540
  (diagnosticsOptions.verboseControlEnabled && resolvedFlags.verbose
3517
3541
  ? "trace"
3518
3542
  : diagnosticsOptions.logLevel),
3519
- logger: diagnosticsOptions.logger ?? writeCLIDiagnosticEvent
3543
+ logger: diagnosticsOptions.logger ?? (invocation
3544
+ ? (event) => invocation.write(`${event.message}\n`, "stderr")
3545
+ : writeCLIDiagnosticEvent)
3520
3546
  });
3521
- const promptInput = promptStreams.input ?? process.stdin;
3522
- const promptOutput = promptStreams.output ?? process.stdout;
3547
+ const promptInput = promptStreams.input ?? (invocation ? { isTTY: false } : process.stdin);
3548
+ const promptOutput = promptStreams.output ?? (invocation ? { isTTY: false } : process.stdout);
3523
3549
  const stdinTTY = Boolean(promptInput.isTTY);
3524
3550
  const stdoutTTY = Boolean(promptOutput.isTTY);
3525
3551
  const shouldPrompt = !resolvedFlags.yes && stdinTTY;
@@ -3532,9 +3558,11 @@ async function executeCommand(state, rootUsageName, services, requirementOptions
3532
3558
  stdoutTTY
3533
3559
  }
3534
3560
  : undefined;
3535
- const runtime = await resolveFixtureRuntime(state.command, services, requirementOptions, runtimeFetch, runtimeEnv, runtimeFs);
3561
+ const runtime = await resolveFixtureRuntime(state.command, services, requirementOptions, runtimeFetch, runtimeEnv, runtimeFs, invocation !== undefined);
3536
3562
  const preflightContext = {
3537
3563
  ...runtime.services,
3564
+ ...invocation?.capabilities,
3565
+ ...(invocation ? { signal: invocation.signal } : {}),
3538
3566
  secrets: runtime.secrets,
3539
3567
  fetch: runtime.fetch,
3540
3568
  fs: runtime.fs,
@@ -3549,14 +3577,28 @@ async function executeCommand(state, rootUsageName, services, requirementOptions
3549
3577
  let resolvedParams;
3550
3578
  try {
3551
3579
  await withOutputFormat(toDesignSystemOutput(output), async () => {
3552
- await assertCommandRequirements(state.command, preflightContext, runtime.requirementOptions);
3553
- const params = await resolveParams(state.fields, state.dynamicFields, state.variants, state.positionalValues, optionValues, state.rawArgv, state.casing, state.presetsEnabled ? resolvedFlags.preset : undefined, shouldPrompt, missingParameterContext, promptStreams);
3580
+ if (!invocation)
3581
+ await assertCommandRequirements(state.command, preflightContext, runtime.requirementOptions);
3582
+ const params = await resolveParams(state.fields, state.dynamicFields, state.variants, state.positionalValues, optionValues, state.rawArgv, state.casing, state.presetsEnabled ? resolvedFlags.preset : undefined, shouldPrompt, missingParameterContext, promptStreams, invocation?.defaults?.[state.declarationPath.join("/")]);
3554
3583
  const paramsSchema = state.command.params;
3555
3584
  if (paramsSchema !== undefined && paramsSchema[nativeJsonSchema] !== undefined) {
3556
3585
  const validation = validateSchema(paramsSchema, params);
3557
3586
  if (!validation.ok)
3558
3587
  throwValidationErrors(validation.issues.map((issue) => formatFieldValidationIssue(issue, "")));
3559
3588
  }
3589
+ if (invocation) {
3590
+ const validation = validateSchema(state.command.params, params);
3591
+ if (!validation.ok) {
3592
+ throwValidationErrors(validation.issues.map((issue) => formatFieldValidationIssue(issue, "")));
3593
+ return;
3594
+ }
3595
+ Object.assign(params, validation.value);
3596
+ invocation.signal.throwIfAborted();
3597
+ await assertCommandRequirements(state.command, { ...preflightContext, params }, runtime.requirementOptions);
3598
+ if (state.command.confirm && !state.command.humanInLoop && !resolvedFlags.yes) {
3599
+ throw new UserError("Confirmation required; supply --yes to authorize this command.");
3600
+ }
3601
+ }
3560
3602
  resolvedParams = params;
3561
3603
  runtimeSecrets = runtime.secrets;
3562
3604
  const context = {
@@ -3566,6 +3608,7 @@ async function executeCommand(state, rootUsageName, services, requirementOptions
3566
3608
  if (state.command.stream !== undefined) {
3567
3609
  const stream = createManagedStream({
3568
3610
  eventSchema: state.command.stream.event,
3611
+ signal: invocation?.signal,
3569
3612
  onStatus(event) {
3570
3613
  diagnostics.emit({
3571
3614
  level: "info",
@@ -3590,9 +3633,11 @@ async function executeCommand(state, rootUsageName, services, requirementOptions
3590
3633
  const interrupt = () => {
3591
3634
  void stream.cancel(new UserError("Operation cancelled.")).catch(() => undefined);
3592
3635
  };
3593
- process.once("SIGINT", interrupt);
3636
+ if (!invocation)
3637
+ process.once("SIGINT", interrupt);
3594
3638
  try {
3595
3639
  for await (const event of stream) {
3640
+ invocation?.signal.throwIfAborted();
3596
3641
  if (output === "json") {
3597
3642
  const line = JSON.stringify(event);
3598
3643
  if (outputEmitter === undefined) {
@@ -3603,14 +3648,17 @@ async function executeCommand(state, rootUsageName, services, requirementOptions
3603
3648
  }
3604
3649
  }
3605
3650
  else {
3606
- renderCLIResult(state.command, state.commandPath, event, output, primitives, outputFormats, outputEmitter === undefined
3651
+ renderCLIResult(state.command, state.commandPath, event, output, primitives, outputFormats, invocation?.write ?? (outputEmitter === undefined
3607
3652
  ? undefined
3608
- : (chunk) => outputEmitter(chunk.endsWith("\n") ? chunk.slice(0, -1) : chunk), outputEmitter);
3653
+ : (chunk) => outputEmitter(chunk.endsWith("\n") ? chunk.slice(0, -1) : chunk)), outputEmitter);
3609
3654
  }
3655
+ await invocation?.flush();
3610
3656
  }
3657
+ invocation?.signal.throwIfAborted();
3611
3658
  }
3612
3659
  finally {
3613
- process.removeListener("SIGINT", interrupt);
3660
+ if (!invocation)
3661
+ process.removeListener("SIGINT", interrupt);
3614
3662
  await stream.cancel();
3615
3663
  }
3616
3664
  return;
@@ -3644,16 +3692,20 @@ async function executeCommand(state, rootUsageName, services, requirementOptions
3644
3692
  if (output === "rich" && runtime.isFixture) {
3645
3693
  writeRichHeader(`${state.command.name} (fixture)`);
3646
3694
  }
3695
+ invocation?.signal.throwIfAborted();
3647
3696
  const pendingApproval = isHumanInLoopPending(result);
3648
3697
  if (pendingApproval && output === "rich") {
3649
3698
  renderHumanInLoopPending(result, rootUsageName, outputEmitter);
3650
3699
  return;
3651
3700
  }
3652
- const renderStatus = renderCLIResult(pendingApproval ? { ...state.command, render: undefined } : state.command, state.commandPath, result, output, primitives, outputFormats, outputEmitter === undefined
3701
+ const renderStatus = renderCLIResult(pendingApproval ? { ...state.command, render: undefined } : state.command, state.commandPath, result, output, primitives, outputFormats, invocation?.write ?? (outputEmitter === undefined
3653
3702
  ? undefined
3654
- : (chunk) => outputEmitter(chunk.endsWith("\n") ? chunk.slice(0, -1) : chunk), outputEmitter);
3703
+ : (chunk) => outputEmitter(chunk.endsWith("\n") ? chunk.slice(0, -1) : chunk)), outputEmitter);
3655
3704
  if (renderStatus.mcpError) {
3656
- process.exitCode = 1;
3705
+ if (invocation)
3706
+ invocation.exitCode = 1;
3707
+ else
3708
+ process.exitCode = 1;
3657
3709
  }
3658
3710
  });
3659
3711
  }
@@ -4200,6 +4252,10 @@ function configureCommanderSuggestionOutput(command, version) {
4200
4252
  }
4201
4253
  export async function runCLI(roots, options = {}) {
4202
4254
  enableSourceMaps();
4255
+ await executeCLICommand(roots, options);
4256
+ }
4257
+ /** Shared parser, validation and dispatch; native callers supply a local runtime. */
4258
+ export async function executeCLICommand(roots, options, invocation) {
4203
4259
  const controls = resolveCLIControls(options.controls);
4204
4260
  let argv = [...(options.argv ?? process.argv)];
4205
4261
  const rootUsageName = options.rootUsageName ?? inferProgramName(argv);
@@ -4215,6 +4271,8 @@ export async function runCLI(roots, options = {}) {
4215
4271
  const root = mergeApprovalsRoot(normalizedRoot, options);
4216
4272
  assertHumanInLoopWired(root, options.humanInLoop);
4217
4273
  if (hasMcpProxyConfig(root)) {
4274
+ if (invocation)
4275
+ throw new UserError("MCP proxy discovery requires the standalone CLI; native library plugins support in-process handlers only.");
4218
4276
  const proxyRuntime = await importOptionalModule(optionalModulePaths.mcpProxy);
4219
4277
  await proxyRuntime.resolveMcpProxies(root, { projectRoot: options.projectRoot });
4220
4278
  proxyCleanup = { dispose: proxyRuntime.disposeMcpProxies, root };
@@ -4223,7 +4281,7 @@ export async function runCLI(roots, options = {}) {
4223
4281
  const services = (options.services ?? {});
4224
4282
  const humanInLoop = options.humanInLoop;
4225
4283
  const runtimeFetch = options.fetch ?? globalThis.fetch;
4226
- version = options.version ?? findEntrypointPackageMetadata(argv[1])?.version;
4284
+ version = options.version ?? (invocation ? undefined : findEntrypointPackageMetadata(argv[1])?.version);
4227
4285
  const servicesWithBuiltIns = {
4228
4286
  ...services,
4229
4287
  humanInLoop,
@@ -4236,7 +4294,7 @@ export async function runCLI(roots, options = {}) {
4236
4294
  validateServices(services);
4237
4295
  if (argv.length <= 2 && root.default?.scope.includes("cli") !== true) {
4238
4296
  userErrorPattern = "usage";
4239
- await renderGeneratedHelp(root, argv, { ...options, version });
4297
+ await renderGeneratedHelp(root, argv, { ...options, version }, invocation);
4240
4298
  return;
4241
4299
  }
4242
4300
  program = new CommanderCommand();
@@ -4266,7 +4324,7 @@ export async function runCLI(roots, options = {}) {
4266
4324
  verboseControlEnabled: controls.verbose
4267
4325
  }, (context) => {
4268
4326
  errorReportContext = context;
4269
- });
4327
+ }, invocation);
4270
4328
  };
4271
4329
  const rootChildNames = new Set(root.children
4272
4330
  .filter((candidate) => isNodeVisibleInScope(candidate, "cli"))
@@ -4282,11 +4340,18 @@ export async function runCLI(roots, options = {}) {
4282
4340
  addCommanderChild(program, command, isDefaultChild, rootChildNames);
4283
4341
  }
4284
4342
  configureCommanderSuggestionOutput(program, version);
4343
+ if (invocation) {
4344
+ const configure = (command) => {
4345
+ command.configureOutput({ writeOut: chunk => invocation.write(chunk), writeErr: chunk => invocation.write(chunk, "stderr") });
4346
+ command.commands.forEach(configure);
4347
+ };
4348
+ configure(program);
4349
+ }
4285
4350
  const prepared = prepareCliArguments(program, argv, fieldLoaders, casing, controls);
4286
4351
  argv = prepared.argv;
4287
4352
  if (prepared.helpArgv !== undefined) {
4288
4353
  userErrorPattern = "usage";
4289
- await renderGeneratedHelp(root, prepared.helpArgv, { ...options, version });
4354
+ await renderGeneratedHelp(root, prepared.helpArgv, { ...options, version }, invocation);
4290
4355
  return;
4291
4356
  }
4292
4357
  for (const loadFields of fieldLoaders.values()) {
@@ -4294,6 +4359,8 @@ export async function runCLI(roots, options = {}) {
4294
4359
  }
4295
4360
  const unknownCommand = findUnknownCommanderCommand(program, argv);
4296
4361
  if (unknownCommand !== undefined) {
4362
+ if (invocation)
4363
+ throw new UserError(formatUnknownCommandMessage(unknownCommand.input, unknownCommand.currentCommand));
4297
4364
  await withOutputFormat(toDesignSystemOutput(resolveOutputFromArgv(argv, controls.outputFormats)), async () => {
4298
4365
  renderCliErrorPattern({
4299
4366
  kind: "usage",
@@ -4308,6 +4375,15 @@ export async function runCLI(roots, options = {}) {
4308
4375
  await program.parseAsync(argv);
4309
4376
  }
4310
4377
  catch (error) {
4378
+ if (invocation) {
4379
+ if (invocation.signal.aborted)
4380
+ throw invocation.signal.reason;
4381
+ invocation.exitCode = error instanceof CommanderError ? error.exitCode : 1;
4382
+ if (!(error instanceof CommanderError && error.exitCode === 0)) {
4383
+ invocation.write(`${error instanceof Error ? error.message : String(error)}\n`, "stderr");
4384
+ }
4385
+ return;
4386
+ }
4311
4387
  const resolvedFlags = lastActionCommand ? getResolvedFlags(lastActionCommand) : undefined;
4312
4388
  if (error instanceof ApprovalDeclinedError) {
4313
4389
  await withOutputFormat(toDesignSystemOutput(resolvedFlags !== undefined
@@ -4353,6 +4429,7 @@ export async function runCLI(roots, options = {}) {
4353
4429
  });
4354
4430
  }
4355
4431
  finally {
4432
+ await invocation?.flush();
4356
4433
  if (proxyCleanup !== undefined) {
4357
4434
  try {
4358
4435
  await proxyCleanup.dispose(proxyCleanup.root);
@@ -113,7 +113,7 @@
113
113
  },
114
114
  {
115
115
  "name": "toolcraft",
116
- "version": "0.0.362",
116
+ "version": "0.0.364",
117
117
  "license": "MIT"
118
118
  },
119
119
  {
@@ -123,7 +123,7 @@
123
123
  },
124
124
  {
125
125
  "name": "toolcraft-schema",
126
- "version": "0.0.362",
126
+ "version": "0.0.364",
127
127
  "license": "MIT"
128
128
  },
129
129
  {
package/dist/index.d.ts CHANGED
@@ -68,7 +68,7 @@ export interface CommandExample {
68
68
  title: string;
69
69
  params: Record<string, unknown>;
70
70
  }
71
- export type GroupCheckContext<TServices extends object = EmptyServices> = TServices & {
71
+ export type GroupCheckContext<TServices extends object = EmptyServices> = TServices & HandlerInvocationCapabilities & {
72
72
  params?: unknown;
73
73
  secrets?: Record<string, string | undefined>;
74
74
  fetch: typeof globalThis.fetch;
@@ -78,7 +78,7 @@ export type GroupCheckContext<TServices extends object = EmptyServices> = TServi
78
78
  signal?: AbortSignal;
79
79
  progress(message: string): void;
80
80
  };
81
- export type CommandCheckContext<TParamsSchema extends ObjectSchema<any> = AnyObjectSchema, TSecrets extends SecretDeclarations | undefined = undefined, TServices extends object = EmptyServices> = TServices & {
81
+ export type CommandCheckContext<TParamsSchema extends ObjectSchema<any> = AnyObjectSchema, TSecrets extends SecretDeclarations | undefined = undefined, TServices extends object = EmptyServices> = TServices & HandlerInvocationCapabilities & {
82
82
  params?: Static<TParamsSchema>;
83
83
  secrets?: InferSecrets<TSecrets>;
84
84
  fetch: typeof globalThis.fetch;
@@ -105,7 +105,11 @@ export interface ToolAnnotations {
105
105
  idempotentHint?: boolean;
106
106
  openWorldHint?: boolean;
107
107
  }
108
- export type HandlerContext<TParamsSchema extends ObjectSchema<any> = AnyObjectSchema, TSecrets extends SecretDeclarations | undefined = undefined, TServices extends object = EmptyServices> = TServices & {
108
+ /** Additional capabilities supplied by native invocation hosts. */
109
+ export interface HandlerInvocationCapabilities {
110
+ readonly signal?: AbortSignal;
111
+ }
112
+ export type HandlerContext<TParamsSchema extends ObjectSchema<any> = AnyObjectSchema, TSecrets extends SecretDeclarations | undefined = undefined, TServices extends object = EmptyServices> = TServices & HandlerInvocationCapabilities & {
109
113
  params: Static<TParamsSchema>;
110
114
  secrets: InferSecrets<TSecrets>;
111
115
  fetch: typeof globalThis.fetch;
@@ -8,9 +8,18 @@ export const RESERVED_SERVICE_NAMES = new Set([
8
8
  "diagnostics",
9
9
  "progress",
10
10
  "runtimeOptions",
11
- "root"
11
+ "root",
12
+ "signal",
13
+ "stdin",
14
+ "stdout",
15
+ "stderr",
16
+ "cwd",
17
+ "regex",
18
+ "registerCleanup",
19
+ "invoke",
20
+ "inputBudget"
12
21
  ]);
13
- const RESERVED_SERVICE_NAMES_MESSAGE = "Available reserved names: params, secrets, fetch, fs, env, diagnostics, progress, runtimeOptions, root.";
22
+ const RESERVED_SERVICE_NAMES_MESSAGE = `Available reserved names: ${[...RESERVED_SERVICE_NAMES].join(", ")}.`;
14
23
  export function createFs(fs) {
15
24
  if (fs !== undefined) {
16
25
  return fs;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,11 @@
1
+ const revoked = { capabilities: { fetch: undefined, regex: undefined, services: undefined } };
2
+ const provider = { createWorker() { throw new Error("synthetic provider"); } };
3
+ const supplied = {
4
+ services: { client: {} },
5
+ regex: { executor: provider, limits: { requestTimeoutMs: 17 } },
6
+ fetch: undefined,
7
+ humanInLoop: undefined
8
+ };
9
+ void revoked;
10
+ void supplied;
11
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,16 @@
1
+ import { S, defineCommand, defineGroup } from "./index.js";
2
+ import { toolcraftCommands, toolcraftDefaults } from "./safe-bash.js";
3
+ const users = defineGroup({ name: "users", children: [
4
+ defineCommand({ name: "list_users", params: S.Object({ limit: S.Number(), enabled: S.Boolean() }), handler: ({ params }) => params })
5
+ ] });
6
+ const library = defineGroup({ name: "tools", children: [users] });
7
+ toolcraftCommands(library, { defaults: toolcraftDefaults(library, { "users/list_users": { limit: 25, enabled: false } }) });
8
+ // @ts-expect-error unknown declaration path
9
+ toolcraftDefaults(library, { "users/missing": { limit: 25 } });
10
+ // @ts-expect-error unknown parameter
11
+ toolcraftDefaults(library, { "users/list_users": { typo: 25 } });
12
+ // @ts-expect-error wrong schema value type
13
+ toolcraftDefaults(library, { "users/list_users": { limit: "25" } });
14
+ toolcraftDefaults([library], { "tools/users/list_users": { limit: 25 } });
15
+ // @ts-expect-error multiple roots require the root prefix
16
+ toolcraftDefaults([library], { "users/list_users": { limit: 25 } });
@@ -0,0 +1,75 @@
1
+ import type { ByteSink, ByteSource, CommandContext, FileSystem, VirtualShellPlugin, ShellCapabilities } from "@poe-platform/safe-bash";
2
+ import { type AnySchema, type Static } from "toolcraft-schema";
3
+ import type { Group } from "./index.js";
4
+ import type { HumanInLoopRuntime } from "./human-in-loop/types.js";
5
+ import { type CLIControls } from "./cli.js";
6
+ export interface ToolcraftInvocation<TServices extends object = Record<string, never>> {
7
+ regex?: ShellCapabilities["regex"];
8
+ registerCleanup?: CommandContext["registerCleanup"];
9
+ invoke?: CommandContext["invoke"];
10
+ inputBudget?: CommandContext["inputBudget"];
11
+ cwd: string;
12
+ env: Readonly<Record<string, string>>;
13
+ fs: FileSystem;
14
+ stdin: ByteSource;
15
+ stdout: ByteSink;
16
+ stderr: ByteSink;
17
+ signal: AbortSignal;
18
+ services?: TServices;
19
+ fetch?: typeof globalThis.fetch;
20
+ humanInLoop?: HumanInLoopRuntime;
21
+ }
22
+ declare module "./index.js" {
23
+ interface HandlerInvocationCapabilities {
24
+ readonly cwd?: string;
25
+ readonly stdin?: ByteSource;
26
+ readonly stdout?: ByteSink;
27
+ readonly stderr?: ByteSink;
28
+ readonly regex?: ShellCapabilities["regex"];
29
+ readonly registerCleanup?: CommandContext["registerCleanup"];
30
+ readonly invoke?: CommandContext["invoke"];
31
+ readonly inputBudget?: CommandContext["inputBudget"];
32
+ }
33
+ }
34
+ export interface ToolcraftCapabilities<TServices extends object = Record<string, never>> extends ShellCapabilities {
35
+ readonly services?: TServices | undefined;
36
+ readonly humanInLoop?: HumanInLoopRuntime | undefined;
37
+ }
38
+ export interface ToolcraftCommandsOptions<TServices extends object = Record<string, never>> {
39
+ services?: TServices | ((invocation: ToolcraftInvocation<TServices>) => TServices);
40
+ defaults?: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
41
+ apiVersion?: string;
42
+ version?: string;
43
+ controls?: CLIControls;
44
+ humanInLoop?: HumanInLoopRuntime;
45
+ }
46
+ type UnionToIntersection<T> = (T extends unknown ? (value: T) => void : never) extends (value: infer U) => void ? U : never;
47
+ type NodeDefaults<T, TPrefix extends string = ""> = T extends {
48
+ readonly __agentKitCommandTypeInfo: {
49
+ name: infer N extends string;
50
+ params: infer P;
51
+ };
52
+ } ? P extends AnySchema ? {
53
+ [K in `${TPrefix}${N}`]?: Partial<Static<P>>;
54
+ } : never : T extends {
55
+ readonly __agentKitGroupTypeInfo: {
56
+ name: infer N extends string;
57
+ children: infer C extends readonly unknown[];
58
+ };
59
+ } ? UnionToIntersection<NodeDefaults<C[number], `${TPrefix}${N}/`>> : never;
60
+ export type ToolcraftParameterDefaults<TLibrary> = TLibrary extends readonly unknown[] ? UnionToIntersection<NodeDefaults<TLibrary[number]>> : TLibrary extends {
61
+ readonly __agentKitGroupTypeInfo: {
62
+ children: infer C extends readonly unknown[];
63
+ };
64
+ } ? UnionToIntersection<NodeDefaults<C[number]>> : ToolcraftCommandsOptions["defaults"];
65
+ /** Checks paths, parameter names and values against a library's inferred definitions. */
66
+ export declare function toolcraftDefaults<TLibrary>(_library: TLibrary, defaults: ToolcraftParameterDefaults<TLibrary>): ToolcraftParameterDefaults<TLibrary>;
67
+ /** Executes already-tokenized argv in process, using the same parser and dispatch as runCLI. */
68
+ export declare function createToolcraftCommandExecutor<TServices extends object>(library: Group<TServices> | readonly Group<TServices>[], options?: ToolcraftCommandsOptions<TServices>): {
69
+ execute(argv: readonly string[], invocation: ToolcraftInvocation<TServices>): Promise<{
70
+ exitCode: number;
71
+ }>;
72
+ };
73
+ /** Registers every CLI-visible root and alias; nested commands use shared Toolcraft discovery. */
74
+ export declare function toolcraftCommands<TServices extends object>(library: Group<TServices> | readonly Group<TServices>[], options?: ToolcraftCommandsOptions<TServices>): VirtualShellPlugin;
75
+ export {};
@@ -0,0 +1,195 @@
1
+ import path from "node:path";
2
+ import { cloneDefaultValue, validate } from "toolcraft-schema";
3
+ import { executeCLICommand, formatCLIName } from "./cli.js";
4
+ import { validateServices } from "./runtime/io.js";
5
+ /** Checks paths, parameter names and values against a library's inferred definitions. */
6
+ export function toolcraftDefaults(_library, defaults) {
7
+ // Snapshot configuration so changing the caller's defaults cannot change registered commands.
8
+ return cloneDefaultValue(defaults);
9
+ }
10
+ function discoverCommands(root, prefix = "") {
11
+ const commands = new Map();
12
+ const visit = (node, commandPath) => {
13
+ if (node.kind === "command") {
14
+ if (node.scope.includes("cli") && !node.hidden)
15
+ commands.set(commandPath, node);
16
+ return;
17
+ }
18
+ if (node.scope && !node.scope.includes("cli"))
19
+ return;
20
+ for (const child of node.children)
21
+ visit(child, `${commandPath}${commandPath ? "/" : ""}${child.name}`);
22
+ };
23
+ if (!root.scope || root.scope.includes("cli")) {
24
+ for (const child of root.children)
25
+ visit(child, `${prefix}${child.name}`);
26
+ }
27
+ return commands;
28
+ }
29
+ function visibleLibrary(root) {
30
+ const aliases = (node) => {
31
+ const name = formatCLIName(node.name, "kebab");
32
+ return name === node.name || node.aliases.includes(name) ? [...node.aliases] : [...node.aliases, name];
33
+ };
34
+ const children = root.children.filter(child => child.kind === "command"
35
+ ? child.scope.includes("cli") && !child.hidden
36
+ : !child.scope || child.scope.includes("cli"))
37
+ .map(child => child.kind === "group" ? visibleLibrary(child) : { ...child, aliases: aliases(child) });
38
+ const defaultCommand = children.find(child => child.kind === "command" && child.name === root.default?.name);
39
+ return { ...root, aliases: aliases(root), children, default: defaultCommand };
40
+ }
41
+ function handlerFileSystem(invocation) {
42
+ const { fs, signal, cwd } = invocation;
43
+ const resolve = (value) => path.posix.resolve(cwd, value);
44
+ return {
45
+ async readFile(value, encoding = "utf8") {
46
+ return Buffer.from(await fs.readFile(resolve(value), { signal })).toString(encoding);
47
+ },
48
+ async writeFile(value, contents, options = {}) {
49
+ const bytes = Buffer.from(contents, options.encoding ?? "utf8");
50
+ const target = resolve(value);
51
+ const flag = options.flag ?? "w";
52
+ if (flag === "a")
53
+ await fs.appendFile(target, bytes, { signal, mode: options.mode });
54
+ else if (flag === "w" || flag === "wx")
55
+ await fs.writeFile(target, bytes, { signal, mode: options.mode, flag });
56
+ else
57
+ throw new Error(`Unsupported virtual write flag: ${flag}`);
58
+ },
59
+ async exists(value) {
60
+ try {
61
+ await fs.stat(resolve(value), { signal });
62
+ return true;
63
+ }
64
+ catch (error) {
65
+ if (error !== null && typeof error === "object" && "code" in error && error.code === "ENOENT")
66
+ return false;
67
+ throw error;
68
+ }
69
+ },
70
+ async lstat(value) {
71
+ const stat = await fs.lstat(resolve(value), { signal });
72
+ return { isSymbolicLink: () => stat.type === "symlink" };
73
+ },
74
+ async rename(from, to) { await fs.rename(resolve(from), resolve(to), { signal }); },
75
+ async unlink(value) {
76
+ if (!fs.unlink)
77
+ throw new Error("Virtual filesystem does not support unlink");
78
+ await fs.unlink(resolve(value), { signal });
79
+ }
80
+ };
81
+ }
82
+ /** Executes already-tokenized argv in process, using the same parser and dispatch as runCLI. */
83
+ export function createToolcraftCommandExecutor(library, options = {}) {
84
+ const roots = Array.isArray(library) ? library : [library];
85
+ const multiple = Array.isArray(library);
86
+ const commands = new Map(roots.flatMap(root => [...discoverCommands(root, multiple ? `${root.name}/` : "")]));
87
+ const defaults = cloneDefaultValue(options.defaults ?? {});
88
+ for (const [commandPath, values] of Object.entries(defaults)) {
89
+ const command = commands.get(commandPath);
90
+ if (!command)
91
+ throw new TypeError(`Unknown default command path: ${commandPath}`);
92
+ for (const [key, value] of Object.entries(values)) {
93
+ if (!Object.prototype.hasOwnProperty.call(command.params.shape, key))
94
+ throw new TypeError(`Unknown default parameter: ${commandPath}/${key}`);
95
+ const result = validate(command.params.shape[key], value);
96
+ if (!result.ok)
97
+ throw new TypeError(`Invalid default parameter ${commandPath}/${key}: ${result.issues.map(issue => issue.message).join("; ")}`);
98
+ }
99
+ }
100
+ if (options.services && typeof options.services !== "function")
101
+ validateServices(options.services);
102
+ return {
103
+ async execute(argv, invocation) {
104
+ invocation.signal.throwIfAborted();
105
+ let services;
106
+ let root;
107
+ try {
108
+ const configuredServices = typeof options.services === "function" ? options.services(invocation) : options.services;
109
+ services = { ...invocation.services, ...configuredServices };
110
+ const rootName = multiple ? argv[0] : roots[0]?.name;
111
+ const selectedRoot = multiple ? roots.find(candidate => candidate.name === rootName || candidate.aliases.includes(rootName ?? "")) : roots[0];
112
+ if (!selectedRoot)
113
+ throw new TypeError(`Unknown toolcraft root: ${rootName}`);
114
+ root = selectedRoot;
115
+ }
116
+ catch (error) {
117
+ invocation.signal.throwIfAborted();
118
+ await invocation.stderr.write(new TextEncoder().encode(`${error instanceof Error ? error.message : String(error)}\n`));
119
+ return { exitCode: 1 };
120
+ }
121
+ const args = multiple ? argv.slice(1) : argv;
122
+ let pending = Promise.resolve();
123
+ let queuedBytes = 0;
124
+ const runtime = {
125
+ signal: invocation.signal,
126
+ exitCode: 0,
127
+ defaults: multiple ? Object.fromEntries(Object.entries(defaults).filter(([key]) => key.startsWith(`${root.name}/`)).map(([key, value]) => [key.slice(root.name.length + 1), value])) : defaults,
128
+ capabilities: { signal: invocation.signal, stdin: invocation.stdin, stdout: invocation.stdout, stderr: invocation.stderr, cwd: invocation.cwd, regex: invocation.regex, registerCleanup: invocation.registerCleanup, invoke: invocation.invoke, inputBudget: invocation.inputBudget },
129
+ write(chunk, stream = "stdout") {
130
+ invocation.signal.throwIfAborted();
131
+ const bytes = new TextEncoder().encode(chunk);
132
+ queuedBytes += bytes.byteLength;
133
+ if (queuedBytes > 1024 * 1024)
134
+ throw new Error("Toolcraft synchronous output exceeded 1 MiB; use a streaming command for larger output");
135
+ pending = pending.then(async () => {
136
+ invocation.signal.throwIfAborted();
137
+ await invocation[stream].write(bytes);
138
+ queuedBytes -= bytes.byteLength;
139
+ });
140
+ // The awaited flush propagates sink failures; observe immediately to avoid unhandled rejections.
141
+ void pending.catch(() => { });
142
+ },
143
+ async flush() { await pending; }
144
+ };
145
+ const deniedFetch = async () => { throw new Error("Network capability is unavailable for this invocation"); };
146
+ await executeCLICommand(visibleLibrary(root), {
147
+ argv: ["toolcraft", root.name, ...args],
148
+ rootUsageName: root.name,
149
+ version: options.version,
150
+ apiVersion: options.apiVersion,
151
+ services: services,
152
+ env: { ...invocation.env },
153
+ fs: handlerFileSystem(invocation),
154
+ fetch: invocation.fetch ?? deniedFetch,
155
+ humanInLoop: invocation.humanInLoop ?? options.humanInLoop,
156
+ controls: { output: true, yes: true, ...options.controls },
157
+ errorReports: false,
158
+ outputEmitter: entry => runtime.write(`${entry}\n`)
159
+ }, runtime);
160
+ return { exitCode: runtime.exitCode };
161
+ }
162
+ };
163
+ }
164
+ /** Registers every CLI-visible root and alias; nested commands use shared Toolcraft discovery. */
165
+ export function toolcraftCommands(library, options = {}) {
166
+ const executor = createToolcraftCommandExecutor(library, options);
167
+ const roots = Array.isArray(library) ? library : [library];
168
+ return {
169
+ name: "toolcraft",
170
+ setup(host) {
171
+ if (typeof host.provideCapabilities !== "function")
172
+ throw new Error("Toolcraft requires a safe-bash runtime with invocation capabilities");
173
+ for (const root of roots) {
174
+ if (root.scope && !root.scope.includes("cli"))
175
+ continue;
176
+ for (const name of [root.name, ...root.aliases]) {
177
+ host.commands.register({
178
+ name,
179
+ description: root.description,
180
+ execute(context) {
181
+ const capabilities = context.capabilities;
182
+ return executor.execute(Array.isArray(library) ? [root.name, ...context.args] : context.args, {
183
+ ...context,
184
+ services: capabilities?.services,
185
+ fetch: capabilities?.fetch,
186
+ regex: capabilities?.regex,
187
+ humanInLoop: capabilities?.humanInLoop
188
+ });
189
+ }
190
+ });
191
+ }
192
+ }
193
+ }
194
+ };
195
+ }
@@ -8,7 +8,7 @@
8
8
  },
9
9
  {
10
10
  "name": "toolcraft-schema",
11
- "version": "0.0.362",
11
+ "version": "0.0.364",
12
12
  "license": "MIT"
13
13
  }
14
14
  ]
@@ -1 +1 @@
1
- export declare function note(message: string, title?: string): void;
1
+ export declare function note(message: string, title?: string, write?: (chunk: string) => void): void;
@@ -16,7 +16,7 @@ function renderTerminalNote(message, title) {
16
16
  const bottom = color.gray(`├${"─".repeat(contentWidth + 2)}╯`);
17
17
  return [color.gray("│"), titleLine, ...content, bottom].join("\n");
18
18
  }
19
- export function note(message, title) {
19
+ export function note(message, title, write = process.stdout.write.bind(process.stdout)) {
20
20
  const format = resolveOutputFormat();
21
21
  const strippedMessage = stripAnsi(message);
22
22
  const strippedTitle = stripAnsi(title ?? "").replaceAll("\r\n", " ").replaceAll("\n", " ").replaceAll("\r", " ");
@@ -24,16 +24,16 @@ export function note(message, title) {
24
24
  const lines = strippedMessage.split("\n");
25
25
  const heading = strippedTitle ? `> **${strippedTitle}**\n` : "";
26
26
  const body = lines.map((line) => `> ${line}`).join("\n");
27
- process.stdout.write(`${heading}${body}\n`);
27
+ write(`${heading}${body}\n`);
28
28
  return;
29
29
  }
30
30
  if (format === "json") {
31
- process.stdout.write(`${JSON.stringify({
31
+ write(`${JSON.stringify({
32
32
  type: "note",
33
33
  title: strippedTitle,
34
34
  message: strippedMessage
35
35
  })}\n`);
36
36
  return;
37
37
  }
38
- process.stdout.write(`${renderTerminalNote(message, title)}\n`);
38
+ write(`${renderTerminalNote(message, title)}\n`);
39
39
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolcraft-schema",
3
- "version": "0.0.362",
3
+ "version": "0.0.364",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolcraft",
3
- "version": "0.0.362",
3
+ "version": "0.0.364",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -97,6 +97,10 @@
97
97
  "./tiny-mcp-client": {
98
98
  "types": "./dist/tiny-mcp-client.d.ts",
99
99
  "import": "./dist/tiny-mcp-client.js"
100
+ },
101
+ "./safe-bash": {
102
+ "types": "./dist/safe-bash.d.ts",
103
+ "import": "./dist/safe-bash.js"
100
104
  }
101
105
  },
102
106
  "scripts": {
@@ -158,7 +162,7 @@
158
162
  "yaml"
159
163
  ],
160
164
  "optionalDependencies": {
161
- "toolcraft-schema": "0.0.362",
165
+ "toolcraft-schema": "0.0.364",
162
166
  "toolcraft-design": "*",
163
167
  "@poe-code/frontmatter": "*",
164
168
  "@poe-code/agent-mcp-config": "*",
@@ -175,6 +179,7 @@
175
179
  "auth-store": "*"
176
180
  },
177
181
  "devDependencies": {
178
- "tiny-stdio-mcp-test-server": "*"
182
+ "tiny-stdio-mcp-test-server": "*",
183
+ "@poe-platform/safe-bash": "*"
179
184
  }
180
185
  }