toolcraft 0.0.363 → 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 +84 -0
- package/composition.json +2 -2
- package/dist/cli.d.ts +11 -0
- package/dist/cli.js +111 -34
- package/dist/composition.json +2 -2
- package/dist/index.d.ts +7 -3
- package/dist/runtime/io.js +11 -2
- package/dist/safe-bash-capabilities.compile-check.d.ts +1 -0
- package/dist/safe-bash-capabilities.compile-check.js +11 -0
- package/dist/safe-bash.compile-check.d.ts +1 -0
- package/dist/safe-bash.compile-check.js +16 -0
- package/dist/safe-bash.d.ts +75 -0
- package/dist/safe-bash.js +195 -0
- package/node_modules/tiny-stdio-mcp-server/dist/composition.json +1 -1
- package/node_modules/toolcraft-design/dist/prompts/primitives/note.d.ts +1 -1
- package/node_modules/toolcraft-design/dist/prompts/primitives/note.js +4 -4
- package/node_modules/toolcraft-schema/package.json +1 -1
- package/package.json +8 -3
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.
|
|
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.
|
|
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
|
-
|
|
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) =>
|
|
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 =
|
|
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]) =>
|
|
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}.${
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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,
|
|
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) =>
|
|
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 ??
|
|
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
|
-
|
|
3553
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
package/dist/composition.json
CHANGED
|
@@ -113,7 +113,7 @@
|
|
|
113
113
|
},
|
|
114
114
|
{
|
|
115
115
|
"name": "toolcraft",
|
|
116
|
-
"version": "0.0.
|
|
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.
|
|
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
|
-
|
|
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;
|
package/dist/runtime/io.js
CHANGED
|
@@ -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 =
|
|
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
|
+
}
|
|
@@ -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
|
-
|
|
27
|
+
write(`${heading}${body}\n`);
|
|
28
28
|
return;
|
|
29
29
|
}
|
|
30
30
|
if (format === "json") {
|
|
31
|
-
|
|
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
|
-
|
|
38
|
+
write(`${renderTerminalNote(message, title)}\n`);
|
|
39
39
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolcraft",
|
|
3
|
-
"version": "0.0.
|
|
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.
|
|
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
|
}
|