toolcraft 0.0.93 → 0.0.95

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +74 -4
  2. package/composition.json +1 -1
  3. package/dist/agent-defs.d.ts +1 -0
  4. package/dist/agent-defs.js +1 -0
  5. package/dist/agent-human-in-loop.d.ts +1 -0
  6. package/dist/agent-human-in-loop.js +1 -0
  7. package/dist/agent-mcp-config.d.ts +1 -0
  8. package/dist/agent-mcp-config.js +1 -0
  9. package/dist/auth-store.d.ts +1 -0
  10. package/dist/auth-store.js +1 -0
  11. package/dist/cli.d.ts +4 -1
  12. package/dist/cli.js +24 -65
  13. package/dist/composition.json +1 -1
  14. package/dist/config-mutations.d.ts +1 -0
  15. package/dist/config-mutations.js +1 -0
  16. package/dist/frontmatter.d.ts +1 -0
  17. package/dist/frontmatter.js +1 -0
  18. package/dist/human-in-loop/gate.d.ts +5 -1
  19. package/dist/human-in-loop/gate.js +10 -8
  20. package/dist/human-in-loop/runner.js +1 -28
  21. package/dist/mcp.d.ts +4 -1
  22. package/dist/mcp.js +8 -51
  23. package/dist/process-runner.d.ts +1 -0
  24. package/dist/process-runner.js +1 -0
  25. package/dist/runtime/io.d.ts +5 -0
  26. package/dist/runtime/io.js +50 -0
  27. package/dist/sdk.d.ts +12 -8
  28. package/dist/sdk.js +7 -52
  29. package/dist/task-list.d.ts +1 -0
  30. package/dist/task-list.js +1 -0
  31. package/dist/testing/fakes.d.ts +20 -0
  32. package/dist/testing/fakes.js +83 -0
  33. package/dist/testing/fixtures.d.ts +8 -0
  34. package/dist/testing/fixtures.js +248 -0
  35. package/dist/testing/harness.d.ts +76 -0
  36. package/dist/testing/harness.js +391 -0
  37. package/dist/testing/index.d.ts +4 -0
  38. package/dist/testing/index.js +3 -0
  39. package/dist/testing/memory-fs.d.ts +11 -0
  40. package/dist/testing/memory-fs.js +61 -0
  41. package/dist/testing/parity.d.ts +25 -0
  42. package/dist/testing/parity.js +384 -0
  43. package/dist/testing/render-capture.d.ts +6 -0
  44. package/dist/testing/render-capture.js +54 -0
  45. package/dist/tiny-mcp-client.d.ts +1 -0
  46. package/dist/tiny-mcp-client.js +1 -0
  47. package/node_modules/@poe-code/agent-defs/package.json +2 -0
  48. package/node_modules/@poe-code/agent-human-in-loop/README.md +12 -2
  49. package/node_modules/@poe-code/agent-human-in-loop/package.json +2 -0
  50. package/node_modules/@poe-code/agent-mcp-config/README.md +7 -7
  51. package/node_modules/@poe-code/agent-mcp-config/package.json +2 -0
  52. package/node_modules/@poe-code/config-mutations/README.md +8 -8
  53. package/node_modules/@poe-code/config-mutations/package.json +2 -0
  54. package/node_modules/@poe-code/frontmatter/README.md +2 -2
  55. package/node_modules/@poe-code/frontmatter/package.json +2 -0
  56. package/node_modules/@poe-code/process-runner/README.md +1 -1
  57. package/node_modules/@poe-code/process-runner/package.json +2 -0
  58. package/node_modules/@poe-code/task-list/README.md +15 -3
  59. package/node_modules/@poe-code/task-list/package.json +2 -0
  60. package/node_modules/auth-store/README.md +15 -0
  61. package/node_modules/auth-store/package.json +2 -0
  62. package/node_modules/tiny-mcp-client/README.md +36 -36
  63. package/node_modules/tiny-mcp-client/package.json +2 -0
  64. package/node_modules/toolcraft-design/README.md +8 -4
  65. package/node_modules/toolcraft-design/dist/dashboard/terminal.js +48 -6
  66. package/node_modules/toolcraft-design/dist/explorer/actions.d.ts +4 -0
  67. package/node_modules/toolcraft-design/dist/explorer/actions.js +1 -0
  68. package/node_modules/toolcraft-design/dist/explorer/events.d.ts +4 -0
  69. package/node_modules/toolcraft-design/dist/explorer/reducer.js +52 -4
  70. package/node_modules/toolcraft-design/dist/explorer/render/list.js +47 -20
  71. package/node_modules/toolcraft-design/dist/explorer/render/modal.js +13 -1
  72. package/node_modules/toolcraft-design/dist/explorer/render/test-fixtures.js +1 -1
  73. package/node_modules/toolcraft-design/dist/explorer/runtime.js +3 -0
  74. package/node_modules/toolcraft-design/dist/explorer/state.d.ts +9 -0
  75. package/node_modules/toolcraft-design/package.json +5 -0
  76. package/package.json +42 -2
package/README.md CHANGED
@@ -6,7 +6,7 @@ Define a command once. Get a typed CLI, an MCP server, and a typed SDK from the
6
6
 
7
7
  ## Why
8
8
 
9
- You have a folder of one-off scripts and a couple of MCP servers. Each one re-derives its own argument parsing, env handling, and help text. Running them from a chatbot needs another adapter. Calling them from another script means subprocessing.
9
+ You have one-off scripts and MCP servers. Each one re-derives argument parsing, env handling, and help text. Running them from a chatbot needs another adapter. Calling them from another script means subprocessing.
10
10
 
11
11
  `toolcraft` is the consolidation step. You write each operation as one `defineCommand`, group them, and pick which surfaces to expose:
12
12
 
@@ -30,7 +30,7 @@ Before writing toolcraft code, make a small tool map. For each script or MCP too
30
30
  - **Surfaces** — where it should appear: CLI, MCP, SDK, or all three.
31
31
  - **Safety** — whether it needs approval, auth, or another precondition.
32
32
 
33
- Keep the first migration boring:
33
+ Keep the first migration direct:
34
34
 
35
35
  1. Wrap existing scripts as thin `defineCommand` handlers.
36
36
  2. Proxy existing MCP servers with `defineGroup({ mcp })` when you do not want to rewrite them yet.
@@ -38,7 +38,7 @@ Keep the first migration boring:
38
38
  4. Add MCP scope only to tools that are safe and useful for agents.
39
39
  5. Document exposed env vars and config options in the package README.
40
40
 
41
- Once the tool map exists, the rest is mechanical: add commands to `root`, expose the same tree through CLI, MCP, and SDK, and remove old entrypoints when they are no longer needed.
41
+ After the tool map exists, add commands to `root`, expose the same tree through CLI, MCP, and SDK, and remove old entrypoints when they are no longer needed.
42
42
 
43
43
  ## Install
44
44
 
@@ -255,6 +255,63 @@ defineCommand<Services>({
255
255
 
256
256
  Services are merged into the handler context alongside the built-ins (`fetch`, `fs`, `env`, `progress`).
257
257
 
258
+ ## Testing commands
259
+
260
+ Use `createCommandTestHarness` to exercise the same command pipeline as the SDK without assembling a handler context by hand:
261
+
262
+ ```ts
263
+ import { expect, it } from "vitest";
264
+ import { createCommandTestHarness, fakeService } from "toolcraft/testing";
265
+ import { root } from "./root.js";
266
+
267
+ it("creates a deployment", async () => {
268
+ const deployments = fakeService({
269
+ create: async (name: string) => ({ id: "dep-1", name })
270
+ });
271
+ const harness = createCommandTestHarness(root, {
272
+ services: { deployments },
273
+ env: { HOME: "/test/home" },
274
+ secrets: { apiKey: "test-token" },
275
+ fs: { "/test/input.json": "{}" },
276
+ fetch: [{ method: "POST", url: "https://api.example.com/deployments", json: { ok: true } }],
277
+ confirmations: "approve",
278
+ apiVersion: "1.2.0",
279
+ logLevel: "debug"
280
+ });
281
+
282
+ const result = await harness.run<{ id: string }>(["deploy", "create"], {
283
+ name: "production"
284
+ });
285
+
286
+ expect(result).toMatchObject({ ok: true, value: { id: "dep-1" } });
287
+ });
288
+ ```
289
+
290
+ Harness options provide injected `services`, an explicit `env`, named `secrets`, an initial file map or `HandlerFs`, a fetch implementation or `fakeFetch` routes, confirmation behavior (`"approve"`, `"decline"`, or a callback), `apiVersion`, and `logLevel`. `fakeService`, `fakeFetch`, and `createMemoryFs` are also exported for standalone tests.
291
+
292
+ Each `run()` returns a `RunResult` instead of throwing. Assert `ok`, `value`, or `error`, and use `failedAt` to identify the pipeline stage: `resolve`, `secrets`, `requirements`, `params`, `confirm`, `handler`, or `render`. The result also captures `pending`, diagnostic `logs`, `progress`, `confirmations`, the ordered effect `timeline`, `fsChanges`, and rendered rich, Markdown, and JSON output. Pre-handler validation can therefore be asserted directly:
293
+
294
+ ```ts
295
+ const result = await harness.run(["deploy", "create"], { name: 42 });
296
+
297
+ expect(result.ok).toBe(false);
298
+ expect(result.failedAt).toBe("params");
299
+ expect(result.timeline).toEqual([]);
300
+ expect(deployments.calls).toEqual([]);
301
+ ```
302
+
303
+ The harness is hermetic by default: it never falls back to `process.env`, starts with an in-memory filesystem, blocks unmatched network requests, records injected service and effect calls, and uses deterministic timestamp-free pending approval data. Provide every external input through the harness options.
304
+
305
+ Use `parity()` when a command must behave identically through the real SDK, MCP, and CLI adapters:
306
+
307
+ ```ts
308
+ const parity = await harness.parity(["deploy", "create"], { name: "production" });
309
+
310
+ expect(parity.agree, parity.diff).toBe(true);
311
+ expect(parity.sdk.value).toEqual(parity.mcp.value);
312
+ expect(parity.mcp.value).toEqual(parity.cli.value);
313
+ ```
314
+
258
315
  ## Output rendering
259
316
 
260
317
  Handlers return raw values. Add `result:` when a command returns structured data and may be exposed over MCP; Toolcraft turns that schema into MCP `outputSchema`, validates the returned object, returns it as `structuredContent`, and keeps a JSON text backstop for older MCP clients.
@@ -471,12 +528,16 @@ Import the command definitions from `toolcraft` and the CLI runner from `toolcra
471
528
 
472
529
  esbuild records dependency paths relative to the build layout. A standalone install can produce `node_modules/toolcraft/...`, while a hoisted workspace can produce `../../node_modules/toolcraft/...` in module-label comments and source-map `sources`. For layout-independent artifacts, canonicalize every dependency path by keeping the substring from the first `node_modules/` segment onward. Parse the source map as JSON and apply the same operation to each string in `sources`; do not modify application source paths. Serialize the map, then hash or publish the canonical bundle and map.
473
530
 
474
- ## Environment variables
531
+ ## Environment Variables
475
532
 
476
533
  - `TOOLCRAFT_MCP_REFRESH` — MCP proxy cache refresh (`unset` = use cache, `1`/`true` = refresh all, comma-separated names = refresh those).
477
534
  - `TOOLCRAFT_ERROR_REPORTS=1` — enables structured error report files for CLI, MCP, and SDK surfaces that wire `errorReports`.
478
535
  - Per-command `secrets` declarations name additional env vars. They are read at command run time and passed to the handler.
479
536
 
537
+ ## Configuration Options
538
+
539
+ Toolcraft configuration is code-first. Use `defineCommand(config)` and `defineGroup(config)` for the command tree, then pass runtime options to `runCLI`, `createSDK`, `createMCPServer`, or `runMCP`. MCP proxy schemas are cached under `.toolcraft/mcp`, and optional human-in-loop state is configured with `HumanInLoopRuntimeOptions`.
540
+
480
541
  ## API reference
481
542
 
482
543
  ### `defineCommand(config)`
@@ -573,3 +634,12 @@ Subpath imports:
573
634
  - `toolcraft/mcp` — `runMCP`, `createMCPServer`
574
635
  - `toolcraft/human-in-loop` — provider helpers
575
636
  - `toolcraft/mcp-proxy` — proxy internals
637
+
638
+ ## Manual QA
639
+
640
+ Use these package-local walkthroughs after behavior changes:
641
+
642
+ - [Help output](QA-help-output.md)
643
+ - [Error UX](QA-error-ux.md)
644
+ - [Human-in-loop](QA-human-in-loop.md)
645
+ - [MCP proxy](QA-mcp-proxy.md)
package/composition.json CHANGED
@@ -48,7 +48,7 @@
48
48
  },
49
49
  {
50
50
  "name": "toolcraft",
51
- "version": "0.0.93",
51
+ "version": "0.0.95",
52
52
  "license": "MIT"
53
53
  },
54
54
  {
@@ -0,0 +1 @@
1
+ export * from "@poe-code/agent-defs";
@@ -0,0 +1 @@
1
+ export * from "@poe-code/agent-defs";
@@ -0,0 +1 @@
1
+ export * from "@poe-code/agent-human-in-loop";
@@ -0,0 +1 @@
1
+ export * from "@poe-code/agent-human-in-loop";
@@ -0,0 +1 @@
1
+ export * from "@poe-code/agent-mcp-config";
@@ -0,0 +1 @@
1
+ export * from "@poe-code/agent-mcp-config";
@@ -0,0 +1 @@
1
+ export * from "auth-store";
@@ -0,0 +1 @@
1
+ export * from "auth-store";
package/dist/cli.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import "./node-require-shim.js";
2
2
  import { configureTheme } from "toolcraft-design";
3
- import type { Group, LogLevel, RuntimeLoggerInput } from "./index.js";
3
+ import type { Group, HandlerFs, LogLevel, RuntimeLoggerInput } from "./index.js";
4
4
  import { type ErrorReportsOption } from "./error-report.js";
5
5
  import type { HumanInLoopRuntimeOptions } from "./human-in-loop/types.js";
6
6
  export { renderErrorReport } from "./error-report.js";
@@ -20,10 +20,13 @@ export interface RunCLIOptions<TServices extends object = Record<string, unknown
20
20
  argv?: readonly string[];
21
21
  casing?: Casing;
22
22
  controls?: CLIControls;
23
+ env?: Record<string, string>;
23
24
  fetch?: typeof globalThis.fetch;
25
+ fs?: HandlerFs;
24
26
  humanInLoop?: HumanInLoopRuntimeOptions;
25
27
  logLevel?: LogLevel;
26
28
  logger?: RuntimeLoggerInput;
29
+ outputEmitter?: (entry: string) => void;
27
30
  projectRoot?: string;
28
31
  rootDisplayName?: string;
29
32
  rootUsageName?: string;
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import "./node-require-shim.js";
2
- import { access, lstat, readFile, rename, unlink, writeFile } from "node:fs/promises";
2
+ import { readFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { Command as CommanderCommand, CommanderError, InvalidArgumentError, Option } from "commander";
5
5
  import { cancel, configureTheme, confirm, createLogger, formatCommandList, formatOptionList, getTheme, helpFormatterPlain, isCancel, note, promptText, renderTable, resetOutputFormatCache, select, text } from "toolcraft-design";
@@ -16,21 +16,10 @@ import { renderSourceSnippet } from "./source-snippet.js";
16
16
  import { enableSourceMaps, formatDebugStack } from "./stack-trim.js";
17
17
  import { suggest } from "./suggest.js";
18
18
  import { throwValidationErrors } from "./validation-errors.js";
19
+ import { RESERVED_SERVICE_NAMES, createEnv, createFs, validateServices } from "./runtime/io.js";
19
20
  export { renderErrorReport } from "./error-report.js";
20
21
  configureTheme({ brand: "blue", label: "Toolcraft" });
21
22
  export { configureTheme };
22
- const RESERVED_SERVICE_NAMES = new Set([
23
- "params",
24
- "secrets",
25
- "fetch",
26
- "fs",
27
- "env",
28
- "diagnostics",
29
- "progress",
30
- "runtimeOptions",
31
- "root"
32
- ]);
33
- const RESERVED_SERVICE_NAMES_MESSAGE = "Available reserved names: params, secrets, fetch, fs, env, diagnostics, progress, runtimeOptions, root.";
34
23
  const NULL_OPTION_VALUE = Symbol("toolcraft.cli.null");
35
24
  const optionalModulePaths = {
36
25
  approvals: "./human-in-loop/approvals-commands.js",
@@ -350,7 +339,7 @@ function collectFields(schema, casing, globalLongOptionFlags, path = [], inherit
350
339
  longAliases: [...(childSchema.cliAliases ?? [])].map((alias) => alias.startsWith("--") ? alias : `--${alias}`),
351
340
  shortFlag: childSchema.short,
352
341
  schema: childSchema,
353
- description: childSchema.description,
342
+ description: childSchema.cliDescription ?? childSchema.description,
354
343
  optional: runtimeOptional,
355
344
  hasDefault: childSchema.default !== undefined,
356
345
  defaultValue: childSchema.default,
@@ -1889,33 +1878,6 @@ async function withOutputFormat(output, fn) {
1889
1878
  resetOutputFormatCache();
1890
1879
  }
1891
1880
  }
1892
- function createFs() {
1893
- return {
1894
- readFile: async (path, encoding = "utf8") => readFile(path, { encoding }),
1895
- writeFile: async (path, contents, options) => {
1896
- await writeFile(path, contents, options);
1897
- },
1898
- exists: async (path) => {
1899
- try {
1900
- await access(path);
1901
- return true;
1902
- }
1903
- catch {
1904
- return false;
1905
- }
1906
- },
1907
- lstat: async (path) => lstat(path),
1908
- rename: async (fromPath, toPath) => rename(fromPath, toPath),
1909
- unlink: async (path) => unlink(path)
1910
- };
1911
- }
1912
- function createEnv(values = process.env) {
1913
- return {
1914
- get(key) {
1915
- return values[key];
1916
- }
1917
- };
1918
- }
1919
1881
  function isPlainObject(value) {
1920
1882
  return typeof value === "object" && value !== null && !Array.isArray(value);
1921
1883
  }
@@ -2317,16 +2279,16 @@ function createFixtureEnvValues(command) {
2317
2279
  }
2318
2280
  return values;
2319
2281
  }
2320
- async function resolveFixtureRuntime(command, services, requirementOptions, runtimeFetch) {
2282
+ async function resolveFixtureRuntime(command, services, requirementOptions, runtimeFetch, runtimeEnv, runtimeFs) {
2321
2283
  const selector = process.env.TOOLCRAFT_FIXTURE;
2322
2284
  if (selector === undefined || selector.length === 0) {
2323
2285
  return {
2324
- env: createEnv(),
2286
+ env: createEnv(runtimeEnv),
2325
2287
  fetch: runtimeFetch,
2326
- fs: createFs(),
2288
+ fs: createFs(runtimeFs),
2327
2289
  isFixture: false,
2328
2290
  requirementOptions,
2329
- secrets: resolveCommandSecrets(command),
2291
+ secrets: resolveCommandSecrets(command, runtimeEnv),
2330
2292
  services
2331
2293
  };
2332
2294
  }
@@ -2373,8 +2335,8 @@ function renderApprovalDeclined(error) {
2373
2335
  logger.error(error.message);
2374
2336
  process.exitCode = 1;
2375
2337
  }
2376
- function renderCliErrorPattern(pattern) {
2377
- const logger = createLogger();
2338
+ function renderCliErrorPattern(pattern, outputEmitter) {
2339
+ const logger = createLogger(outputEmitter);
2378
2340
  if (pattern.kind === "usage") {
2379
2341
  logger.error(appendUsagePointer(pattern.message, {
2380
2342
  rootUsageName: pattern.rootUsageName,
@@ -2417,13 +2379,6 @@ function renderCliErrorPattern(pattern) {
2417
2379
  }
2418
2380
  process.exitCode = 1;
2419
2381
  }
2420
- function validateServices(services) {
2421
- for (const name of Object.keys(services)) {
2422
- if (RESERVED_SERVICE_NAMES.has(name)) {
2423
- throw new Error(`Service name "${name}" is reserved. Choose a different name. ${RESERVED_SERVICE_NAMES_MESSAGE}`);
2424
- }
2425
- }
2426
- }
2427
2382
  function getNestedValue(target, path) {
2428
2383
  return path.reduce((current, segment) => current !== null && typeof current === "object"
2429
2384
  ? current[segment]
@@ -2981,8 +2936,8 @@ function getResolvedFlags(command) {
2981
2936
  const flags = command.optsWithGlobals();
2982
2937
  return flags;
2983
2938
  }
2984
- async function executeCommand(state, services, requirementOptions, runtimeFetch, runtimeOptions, diagnosticsOptions, onErrorReportContext) {
2985
- const logger = createLogger();
2939
+ async function executeCommand(state, services, requirementOptions, runtimeFetch, runtimeOptions, runtimeEnv, runtimeFs, outputEmitter, diagnosticsOptions, onErrorReportContext) {
2940
+ const logger = createLogger(outputEmitter);
2986
2941
  const primitives = {
2987
2942
  logger,
2988
2943
  renderTable,
@@ -3000,7 +2955,7 @@ async function executeCommand(state, services, requirementOptions, runtimeFetch,
3000
2955
  logger: diagnosticsOptions.logger ?? writeCLIDiagnosticEvent
3001
2956
  });
3002
2957
  const shouldPrompt = !resolvedFlags.yes && Boolean(process.stdin.isTTY);
3003
- const runtime = await resolveFixtureRuntime(state.command, services, requirementOptions, runtimeFetch);
2958
+ const runtime = await resolveFixtureRuntime(state.command, services, requirementOptions, runtimeFetch, runtimeEnv, runtimeFs);
3004
2959
  const preflightContext = {
3005
2960
  ...runtime.services,
3006
2961
  secrets: runtime.secrets,
@@ -3059,7 +3014,9 @@ async function executeCommand(state, services, requirementOptions, runtimeFetch,
3059
3014
  renderHumanInLoopPending(result);
3060
3015
  return;
3061
3016
  }
3062
- const renderStatus = renderResult(state.command, result, output, primitives);
3017
+ const renderStatus = renderResult(state.command, result, output, primitives, outputEmitter === undefined
3018
+ ? undefined
3019
+ : (chunk) => outputEmitter(chunk.endsWith("\n") ? chunk.slice(0, -1) : chunk));
3063
3020
  if (renderStatus.mcpError) {
3064
3021
  process.exitCode = 1;
3065
3022
  }
@@ -3270,7 +3227,7 @@ function renderHttpError(error, options) {
3270
3227
  }
3271
3228
  }
3272
3229
  async function handleRunError(error, options) {
3273
- const logger = createLogger();
3230
+ const logger = createLogger(options.outputEmitter);
3274
3231
  await withOutputFormat(options.output, async () => {
3275
3232
  if (error instanceof UserError) {
3276
3233
  renderCliErrorPattern(options.userErrorPattern === "definition"
@@ -3289,7 +3246,7 @@ async function handleRunError(error, options) {
3289
3246
  : {
3290
3247
  kind: "runtime-user",
3291
3248
  message: error.message
3292
- });
3249
+ }, options.outputEmitter);
3293
3250
  return;
3294
3251
  }
3295
3252
  if (error instanceof Error && error.name === "ToolcraftBugError") {
@@ -3297,7 +3254,7 @@ async function handleRunError(error, options) {
3297
3254
  kind: "toolcraft-bug",
3298
3255
  error,
3299
3256
  debugStackMode: options.debugStackMode
3300
- });
3257
+ }, options.outputEmitter);
3301
3258
  return;
3302
3259
  }
3303
3260
  if (error instanceof CommanderError) {
@@ -3341,7 +3298,7 @@ async function handleRunError(error, options) {
3341
3298
  message,
3342
3299
  stack: error instanceof Error ? error.stack : undefined,
3343
3300
  debugStackMode: options.debugStackMode
3344
- });
3301
+ }, options.outputEmitter);
3345
3302
  });
3346
3303
  }
3347
3304
  function formatCommanderErrorMessage(error) {
@@ -3560,7 +3517,7 @@ function isCommandNameCharacter(character) {
3560
3517
  const isLowercaseLetter = code >= 97 && code <= 122;
3561
3518
  const isUppercaseLetter = code >= 65 && code <= 90;
3562
3519
  const isDigit = code >= 48 && code <= 57;
3563
- return isLowercaseLetter || isUppercaseLetter || isDigit || character === "-" || character === "_";
3520
+ return (isLowercaseLetter || isUppercaseLetter || isDigit || character === "-" || character === "_");
3564
3521
  }
3565
3522
  function getDefaultCommanderCommandName(command) {
3566
3523
  const candidate = command;
@@ -3612,7 +3569,8 @@ export async function runCLI(roots, options = {}) {
3612
3569
  root
3613
3570
  };
3614
3571
  const requirementOptions = {
3615
- apiVersion: options.apiVersion
3572
+ apiVersion: options.apiVersion,
3573
+ env: options.env
3616
3574
  };
3617
3575
  validateServices(services);
3618
3576
  if (hasHelpFlag(argv)) {
@@ -3642,7 +3600,7 @@ export async function runCLI(roots, options = {}) {
3642
3600
  const execute = async (state) => {
3643
3601
  lastActionCommand = state.actionCommand;
3644
3602
  resolvedCommandPath = formatCliCommandPath(state.commandPath);
3645
- await executeCommand(state, servicesWithBuiltIns, requirementOptions, runtimeFetch, runtimeOptions, {
3603
+ await executeCommand(state, servicesWithBuiltIns, requirementOptions, runtimeFetch, runtimeOptions, options.env, options.fs, options.outputEmitter, {
3646
3604
  logLevel: options.logLevel,
3647
3605
  logger: options.logger,
3648
3606
  verboseControlEnabled: controls.verbose
@@ -3707,6 +3665,7 @@ export async function runCLI(roots, options = {}) {
3707
3665
  argv,
3708
3666
  rootUsageName,
3709
3667
  commandPath: resolvedCommandPath,
3668
+ outputEmitter: options.outputEmitter,
3710
3669
  userErrorPattern: errorReportContext?.params === undefined ? userErrorPattern : "runtime-user"
3711
3670
  });
3712
3671
  }
@@ -48,7 +48,7 @@
48
48
  },
49
49
  {
50
50
  "name": "toolcraft",
51
- "version": "0.0.93",
51
+ "version": "0.0.95",
52
52
  "license": "MIT"
53
53
  },
54
54
  {
@@ -0,0 +1 @@
1
+ export * from "@poe-code/config-mutations";
@@ -0,0 +1 @@
1
+ export * from "@poe-code/config-mutations";
@@ -0,0 +1 @@
1
+ export * from "@poe-code/frontmatter";
@@ -0,0 +1 @@
1
+ export * from "@poe-code/frontmatter";
@@ -1,4 +1,8 @@
1
1
  import type { Command, HandlerContext } from "../index.js";
2
+ import { enqueueApproval } from "./approval-tasks.js";
2
3
  import type { HumanInLoopPending, HumanInLoopProvider, HumanInLoopRuntimeOptions } from "./types.js";
3
4
  export declare function resolveProvider(runtimeOptions: HumanInLoopRuntimeOptions | undefined): HumanInLoopProvider;
4
- export declare function invokeWithHumanInLoop<T>(node: Command<any, any, any, T>, ctx: HandlerContext<any, any, any>, runtimeOptions: HumanInLoopRuntimeOptions | undefined, commandPath: string): Promise<T | HumanInLoopPending>;
5
+ export declare function invokeWithHumanInLoop<T>(node: Command<any, any, any, T>, ctx: HandlerContext<any, any, any>, runtimeOptions: HumanInLoopRuntimeOptions | undefined, commandPath: string, options?: {
6
+ enqueueApproval?: typeof enqueueApproval;
7
+ spawnRunner?: boolean;
8
+ }): Promise<T | HumanInLoopPending>;
@@ -20,37 +20,39 @@ export function resolveProvider(runtimeOptions) {
20
20
  providersByRuntime.set(runtimeOptions, provider);
21
21
  return provider;
22
22
  }
23
- export async function invokeWithHumanInLoop(node, ctx, runtimeOptions, commandPath) {
23
+ export async function invokeWithHumanInLoop(node, ctx, runtimeOptions, commandPath, options = {}) {
24
24
  if (!node.humanInLoop) {
25
25
  return node.handler(ctx);
26
26
  }
27
27
  const message = node.humanInLoop.message({
28
28
  params: ctx.params,
29
- commandPath,
29
+ commandPath
30
30
  });
31
31
  if (node.humanInLoop.mode === "async") {
32
32
  const { tasks } = await ensureApprovalList(runtimeOptions);
33
- const { approvalId, pending } = await enqueueApproval({
33
+ const { approvalId, pending } = await (options.enqueueApproval ?? enqueueApproval)({
34
34
  tasks,
35
35
  payload: {
36
36
  commandPath,
37
37
  params: ctx.params,
38
38
  message,
39
- declineInputPrompt: node.humanInLoop.declineInputPrompt,
40
- },
39
+ declineInputPrompt: node.humanInLoop.declineInputPrompt
40
+ }
41
41
  });
42
- spawnApprovalRunner(approvalId, runtimeOptions);
42
+ if (options.spawnRunner !== false) {
43
+ spawnApprovalRunner(approvalId, runtimeOptions);
44
+ }
43
45
  return pending;
44
46
  }
45
47
  const provider = resolveProvider(runtimeOptions);
46
48
  const result = await provider.requestApproval({
47
49
  message,
48
- declineInputPrompt: node.humanInLoop.declineInputPrompt,
50
+ declineInputPrompt: node.humanInLoop.declineInputPrompt
49
51
  });
50
52
  if (result.outcome === "declined") {
51
53
  throw new ApprovalDeclinedError({
52
54
  reason: result.reason,
53
- commandPath,
55
+ commandPath
54
56
  });
55
57
  }
56
58
  return node.handler(ctx);
@@ -1,6 +1,6 @@
1
- import { access, lstat, readFile, rename, unlink, writeFile } from "node:fs/promises";
2
1
  import { InvalidTransitionError } from "@poe-code/task-list";
3
2
  import { UserError, resolveCommandSecrets } from "../index.js";
3
+ import { createEnv, createFs } from "../runtime/io.js";
4
4
  import { ensureApprovalList } from "./approval-tasks.js";
5
5
  import { resolveProvider } from "./gate.js";
6
6
  import { createRuntimeLogger } from "../runtime-logging.js";
@@ -184,33 +184,6 @@ function createHandlerContext(command, params) {
184
184
  }
185
185
  };
186
186
  }
187
- function createFs() {
188
- return {
189
- readFile: async (path, encoding = "utf8") => readFile(path, { encoding }),
190
- writeFile: async (path, contents, options) => {
191
- await writeFile(path, contents, options);
192
- },
193
- exists: async (path) => {
194
- try {
195
- await access(path);
196
- return true;
197
- }
198
- catch {
199
- return false;
200
- }
201
- },
202
- lstat: async (path) => lstat(path),
203
- rename: async (fromPath, toPath) => rename(fromPath, toPath),
204
- unlink: async (path) => unlink(path)
205
- };
206
- }
207
- function createEnv(values = process.env) {
208
- return {
209
- get(key) {
210
- return values[key];
211
- }
212
- };
213
- }
214
187
  function serializeJsonResult(value) {
215
188
  try {
216
189
  const serialized = JSON.stringify(value);
package/dist/mcp.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type SDKTransport, type Server as TinyServer } from "tiny-stdio-mcp-server";
2
- import type { Group, LogLevel, RuntimeLoggerInput } from "./index.js";
2
+ import type { Group, HandlerFs, LogLevel, RuntimeLoggerInput } from "./index.js";
3
3
  import { type ErrorReportsOption } from "./error-report.js";
4
4
  import { type HumanInLoopRuntimeOptions } from "./human-in-loop/index.js";
5
5
  type Casing = "snake" | "camel";
@@ -7,8 +7,11 @@ type CmdkitServer = Omit<TinyServer, "connect"> & {
7
7
  connect(transport: SDKTransport): Promise<void>;
8
8
  };
9
9
  export interface RunMCPOptions<TServices extends object = Record<string, unknown>> {
10
+ apiVersion?: string;
10
11
  approvals?: boolean;
12
+ env?: Record<string, string>;
11
13
  fetch?: typeof globalThis.fetch;
14
+ fs?: HandlerFs;
12
15
  name: string;
13
16
  version?: string;
14
17
  humanInLoop?: HumanInLoopRuntimeOptions;
package/dist/mcp.js CHANGED
@@ -1,4 +1,3 @@
1
- import { access, lstat, readFile, rename, unlink, writeFile } from "node:fs/promises";
2
1
  import { createServer, JSON_RPC_ERROR_CODES, ToolError } from "tiny-stdio-mcp-server";
3
2
  import { toJsonSchema } from "toolcraft-schema";
4
3
  import { createHttpErrorEnvelope, isHttpErrorLike } from "./api-error-summary.js";
@@ -14,18 +13,7 @@ import { enableSourceMaps } from "./stack-trim.js";
14
13
  import { suggest } from "./suggest.js";
15
14
  import { throwValidationErrors } from "./validation-errors.js";
16
15
  import { createRuntimeLogger } from "./runtime-logging.js";
17
- const RESERVED_SERVICE_NAMES = new Set([
18
- "params",
19
- "secrets",
20
- "fetch",
21
- "fs",
22
- "env",
23
- "diagnostics",
24
- "progress",
25
- "runtimeOptions",
26
- "root"
27
- ]);
28
- const RESERVED_SERVICE_NAMES_MESSAGE = "Available reserved names: params, secrets, fetch, fs, env, diagnostics, progress, runtimeOptions, root.";
16
+ import { createEnv, createFs, validateServices } from "./runtime/io.js";
29
17
  function normalizeRoots(roots) {
30
18
  if (!Array.isArray(roots)) {
31
19
  return roots;
@@ -93,40 +81,6 @@ function isOptional(schema) {
93
81
  function isPlainObject(value) {
94
82
  return typeof value === "object" && value !== null && !Array.isArray(value);
95
83
  }
96
- function createFs() {
97
- return {
98
- readFile: async (path, encoding = "utf8") => readFile(path, { encoding }),
99
- writeFile: async (path, contents, options) => {
100
- await writeFile(path, contents, options);
101
- },
102
- exists: async (path) => {
103
- try {
104
- await access(path);
105
- return true;
106
- }
107
- catch {
108
- return false;
109
- }
110
- },
111
- lstat: async (path) => lstat(path),
112
- rename: async (fromPath, toPath) => rename(fromPath, toPath),
113
- unlink: async (path) => unlink(path)
114
- };
115
- }
116
- function createEnv(values = process.env) {
117
- return {
118
- get(key) {
119
- return values[key];
120
- }
121
- };
122
- }
123
- function validateServices(services) {
124
- for (const name of Object.keys(services)) {
125
- if (RESERVED_SERVICE_NAMES.has(name)) {
126
- throw new Error(`Service name "${name}" is reserved. Choose a different name. ${RESERVED_SERVICE_NAMES_MESSAGE}`);
127
- }
128
- }
129
- }
130
84
  function formatAvailableList(values) {
131
85
  return `Available: ${[...values].sort().join(", ")}.`;
132
86
  }
@@ -779,19 +733,22 @@ function createResolvedMCPServer(root, options) {
779
733
  let params;
780
734
  let secrets;
781
735
  try {
782
- secrets = resolveCommandSecrets(tool.command);
736
+ secrets = resolveCommandSecrets(tool.command, options.env);
783
737
  const baseContext = {
784
738
  ...servicesWithBuiltIns,
785
739
  secrets,
786
740
  fetch: runtimeFetch,
787
- fs: createFs(),
788
- env: createEnv(),
741
+ fs: createFs(options.fs),
742
+ env: createEnv(options.env),
789
743
  diagnostics,
790
744
  progress(message) {
791
745
  diagnostics.emit({ level: "info", message, category: "progress" });
792
746
  }
793
747
  };
794
- await assertCommandRequirements(tool.command, { ...baseContext, params: undefined });
748
+ await assertCommandRequirements(tool.command, { ...baseContext, params: undefined }, {
749
+ apiVersion: options.apiVersion,
750
+ env: options.env
751
+ });
795
752
  params = validateToolArguments(tool.command.params, argumentsValue, casing);
796
753
  const result = await invokeWithHumanInLoop(tool.command, {
797
754
  ...baseContext,
@@ -0,0 +1 @@
1
+ export * from "@poe-code/process-runner";
@@ -0,0 +1 @@
1
+ export * from "@poe-code/process-runner";
@@ -0,0 +1,5 @@
1
+ import type { HandlerEnv, HandlerFs } from "../index.js";
2
+ export declare const RESERVED_SERVICE_NAMES: Set<string>;
3
+ export declare function createFs(fs?: HandlerFs): HandlerFs;
4
+ export declare function createEnv(values?: Record<string, string | undefined>): HandlerEnv;
5
+ export declare function validateServices(services: object): void;