toolcraft 0.0.2 → 0.0.4

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 (85) hide show
  1. package/README.md +461 -58
  2. package/dist/cli.compile-check.js +1 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +768 -40
  5. package/dist/human-in-loop/approval-tasks.d.ts +31 -0
  6. package/dist/human-in-loop/approval-tasks.js +201 -0
  7. package/dist/human-in-loop/approvals-commands.d.ts +11 -0
  8. package/dist/human-in-loop/approvals-commands.js +191 -0
  9. package/dist/human-in-loop/config.d.ts +11 -0
  10. package/dist/human-in-loop/config.js +21 -0
  11. package/dist/human-in-loop/default-provider.d.ts +2 -0
  12. package/dist/human-in-loop/default-provider.js +26 -0
  13. package/dist/human-in-loop/gate.d.ts +4 -0
  14. package/dist/human-in-loop/gate.js +57 -0
  15. package/dist/human-in-loop/index.d.ts +7 -0
  16. package/dist/human-in-loop/index.js +4 -0
  17. package/dist/human-in-loop/runner.d.ts +3 -0
  18. package/dist/human-in-loop/runner.js +196 -0
  19. package/dist/human-in-loop/spawn.d.ts +3 -0
  20. package/dist/human-in-loop/spawn.js +16 -0
  21. package/dist/human-in-loop/state-machine.d.ts +4 -0
  22. package/dist/human-in-loop/state-machine.js +10 -0
  23. package/dist/human-in-loop/types.d.ts +41 -0
  24. package/dist/human-in-loop/types.js +13 -0
  25. package/dist/index.compile-check.js +24 -0
  26. package/dist/index.d.ts +32 -13
  27. package/dist/index.js +82 -17
  28. package/dist/json-schema-converter.d.ts +21 -0
  29. package/dist/json-schema-converter.js +432 -0
  30. package/dist/mcp-proxy.d.ts +8 -0
  31. package/dist/mcp-proxy.js +383 -0
  32. package/dist/mcp.compile-check.js +1 -0
  33. package/dist/mcp.d.ts +2 -0
  34. package/dist/mcp.js +103 -11
  35. package/dist/sdk.compile-check.js +77 -0
  36. package/dist/sdk.d.ts +14 -5
  37. package/dist/sdk.js +57 -6
  38. package/dist/user-error.d.ts +3 -0
  39. package/dist/user-error.js +6 -0
  40. package/node_modules/@poe-code/agent-human-in-loop/README.md +42 -0
  41. package/node_modules/@poe-code/agent-human-in-loop/dist/index.d.ts +5 -0
  42. package/node_modules/@poe-code/agent-human-in-loop/dist/index.js +3 -0
  43. package/node_modules/@poe-code/agent-human-in-loop/dist/providers/mock.d.ts +2 -0
  44. package/node_modules/@poe-code/agent-human-in-loop/dist/providers/mock.js +11 -0
  45. package/node_modules/@poe-code/agent-human-in-loop/dist/providers/osascript-script.d.ts +4 -0
  46. package/node_modules/@poe-code/agent-human-in-loop/dist/providers/osascript-script.js +40 -0
  47. package/node_modules/@poe-code/agent-human-in-loop/dist/providers/osascript.d.ts +6 -0
  48. package/node_modules/@poe-code/agent-human-in-loop/dist/providers/osascript.js +33 -0
  49. package/node_modules/@poe-code/agent-human-in-loop/dist/request-approval.d.ts +4 -0
  50. package/node_modules/@poe-code/agent-human-in-loop/dist/request-approval.js +4 -0
  51. package/node_modules/@poe-code/agent-human-in-loop/dist/types.d.ts +14 -0
  52. package/node_modules/@poe-code/agent-human-in-loop/dist/types.js +1 -0
  53. package/node_modules/@poe-code/agent-human-in-loop/package.json +25 -0
  54. package/node_modules/@poe-code/agent-mcp-config/dist/apply.d.ts +6 -0
  55. package/node_modules/@poe-code/agent-mcp-config/dist/apply.js +175 -0
  56. package/node_modules/@poe-code/agent-mcp-config/dist/configs.d.ts +22 -0
  57. package/node_modules/@poe-code/agent-mcp-config/dist/configs.js +74 -0
  58. package/node_modules/@poe-code/agent-mcp-config/dist/index.d.ts +3 -0
  59. package/node_modules/@poe-code/agent-mcp-config/dist/index.js +2 -0
  60. package/node_modules/@poe-code/agent-mcp-config/dist/shapes.d.ts +31 -0
  61. package/node_modules/@poe-code/agent-mcp-config/dist/shapes.js +87 -0
  62. package/node_modules/@poe-code/agent-mcp-config/dist/types.d.ts +25 -0
  63. package/node_modules/@poe-code/agent-mcp-config/dist/types.js +1 -0
  64. package/node_modules/@poe-code/agent-mcp-config/package.json +25 -0
  65. package/node_modules/@poe-code/task-list/README.md +114 -0
  66. package/node_modules/@poe-code/task-list/dist/backends/markdown-dir.d.ts +2 -0
  67. package/node_modules/@poe-code/task-list/dist/backends/markdown-dir.js +466 -0
  68. package/node_modules/@poe-code/task-list/dist/backends/utils.d.ts +8 -0
  69. package/node_modules/@poe-code/task-list/dist/backends/utils.js +58 -0
  70. package/node_modules/@poe-code/task-list/dist/backends/yaml-file.d.ts +2 -0
  71. package/node_modules/@poe-code/task-list/dist/backends/yaml-file.js +444 -0
  72. package/node_modules/@poe-code/task-list/dist/index.d.ts +4 -0
  73. package/node_modules/@poe-code/task-list/dist/index.js +4 -0
  74. package/node_modules/@poe-code/task-list/dist/open.d.ts +3 -0
  75. package/node_modules/@poe-code/task-list/dist/open.js +34 -0
  76. package/node_modules/@poe-code/task-list/dist/schema/store.schema.json +32 -0
  77. package/node_modules/@poe-code/task-list/dist/schema/task.schema.json +33 -0
  78. package/node_modules/@poe-code/task-list/dist/state-machine.d.ts +16 -0
  79. package/node_modules/@poe-code/task-list/dist/state-machine.js +67 -0
  80. package/node_modules/@poe-code/task-list/dist/state.d.ts +29 -0
  81. package/node_modules/@poe-code/task-list/dist/state.js +61 -0
  82. package/node_modules/@poe-code/task-list/dist/types.d.ts +116 -0
  83. package/node_modules/@poe-code/task-list/dist/types.js +37 -0
  84. package/node_modules/@poe-code/task-list/package.json +26 -0
  85. package/package.json +22 -7
package/README.md CHANGED
@@ -1,91 +1,494 @@
1
1
  # toolcraft
2
2
 
3
- tools for agents and humans
3
+ Create tools for both agents and humans.
4
4
 
5
- Typed command and group definitions built on top of `toolcraft-schema`.
5
+ Define a command once. Get a typed CLI, an MCP server, and a typed SDK from the same source. Built on `toolcraft-schema`.
6
6
 
7
- ## Usage
7
+ ## Why
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.
10
+
11
+ `toolcraft` is the consolidation step. You write each operation as one `defineCommand`, group them, and pick which surfaces to expose:
12
+
13
+ - `runCLI` — argv parsing, `--help`, kebab/snake flags, exit codes.
14
+ - `createMCPServer` / `runMCP` — JSON-RPC over stdio with auto-generated tool schemas.
15
+ - `createSDK` — typed in-process function calls.
16
+
17
+ Same handler runs everywhere. Schema, secrets, preconditions, and human-in-loop gating are declared once.
18
+
19
+ Building from an OpenAPI spec? Use [`toolcraft-openapi`](../toolcraft-openapi/) to generate toolcraft commands from the API contract.
20
+
21
+ ## What the owner decides
22
+
23
+ Before writing toolcraft code, make a small tool map. For each script or MCP tool, write down:
24
+
25
+ - **Name** — the generic command path, like `issues.list` or `messages.send`.
26
+ - **Inputs** — params, defaults, and which ones should be positional CLI args.
27
+ - **Output** — the structured value the handler returns.
28
+ - **Secrets** — env vars or credentials the command needs.
29
+ - **Side effects** — files, APIs, databases, money, or user-visible changes.
30
+ - **Surfaces** — where it should appear: CLI, MCP, SDK, or all three.
31
+ - **Safety** — whether it needs approval, auth, or another precondition.
32
+
33
+ Keep the first migration boring:
34
+
35
+ 1. Wrap existing scripts as thin `defineCommand` handlers.
36
+ 2. Proxy existing MCP servers with `defineGroup({ mcp })` when you do not want to rewrite them yet.
37
+ 3. Group commands by domain and put shared secrets or approvals on the group.
38
+ 4. Add MCP scope only to tools that are safe and useful for agents.
39
+ 5. Document exposed env vars and config options in the package README.
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.
42
+
43
+ ## Install
44
+
45
+ ```sh
46
+ npm install toolcraft toolcraft-schema
47
+ ```
48
+
49
+ Requires Node 20+.
50
+
51
+ ## Hello world
8
52
 
9
53
  ```ts
10
- import { defineCommand, defineGroup } from "toolcraft";
11
- import { S } from "toolcraft-schema";
54
+ // src/commands/greet.ts
55
+ import { defineCommand, S } from "toolcraft";
12
56
 
13
- const deploy = defineCommand({
14
- name: "deploy",
57
+ export const greet = defineCommand({
58
+ name: "greet",
59
+ description: "Say hello",
15
60
  params: S.Object({
16
- service: S.String(),
61
+ name: S.String({ description: "Who to greet" }),
62
+ loud: S.Optional(S.Boolean({ default: false })),
17
63
  }),
18
- secrets: {
19
- apiKey: {
20
- env: "API_KEY",
21
- },
64
+ handler: async ({ params }) => {
65
+ const message = `Hello, ${params.name}`;
66
+ return { message: params.loud ? message.toUpperCase() : message };
22
67
  },
23
- handler: async ({ params, secrets }) => ({
24
- service: params.service,
25
- authenticated: Boolean(secrets.apiKey),
26
- }),
27
68
  });
69
+ ```
70
+
71
+ ```ts
72
+ // src/root.ts
73
+ import { defineGroup } from "toolcraft";
74
+ import { greet } from "./commands/greet.js";
28
75
 
29
76
  export const root = defineGroup({
30
- name: "root",
31
- children: [deploy],
77
+ name: "mytool",
78
+ children: [greet],
79
+ });
80
+ ```
81
+
82
+ ```ts
83
+ // src/bin.ts
84
+ #!/usr/bin/env node
85
+ import { runCLI } from "toolcraft/cli";
86
+ import { root } from "./root.js";
87
+
88
+ await runCLI(root, { version: "0.1.0" });
89
+ ```
90
+
91
+ ```sh
92
+ mytool greet --name world
93
+ mytool greet --name world --loud
94
+ mytool greet --help
95
+ ```
96
+
97
+ ## Project layout
98
+
99
+ A typical toolcraft project:
100
+
101
+ ```
102
+ package.json
103
+ src/
104
+ bin.ts # one entrypoint, dispatches by argv (see below)
105
+ root.ts # defineGroup({ children: [...] })
106
+ commands/
107
+ greet.ts # one defineCommand per file
108
+ deploy.ts
109
+ ...
110
+ groups/
111
+ issues/
112
+ index.ts # defineGroup, exports a sub-tree
113
+ list.ts
114
+ create.ts
115
+ ```
116
+
117
+ `package.json`:
118
+
119
+ ```json
120
+ {
121
+ "name": "mytool",
122
+ "type": "module",
123
+ "bin": { "mytool": "./dist/bin.js" },
124
+ "scripts": { "build": "tsc" },
125
+ "dependencies": {
126
+ "toolcraft": "^0.0.1",
127
+ "toolcraft-schema": "^0.0.1"
128
+ }
129
+ }
130
+ ```
131
+
132
+ `tsconfig.json` needs `"module": "NodeNext"` (or `"ESNext"`) and `"moduleResolution": "NodeNext"`.
133
+
134
+ ## One binary, three runtimes
135
+
136
+ Most consumers ship one bin and dispatch on the first argv:
137
+
138
+ ```ts
139
+ // src/bin.ts
140
+ #!/usr/bin/env node
141
+ import { runCLI } from "toolcraft/cli";
142
+ import { runMCP } from "toolcraft/mcp";
143
+ import { root } from "./root.js";
144
+
145
+ const mode = process.argv[2];
146
+
147
+ if (mode === "mcp") {
148
+ await runMCP(root, { name: "mytool", version: "0.1.0" });
149
+ } else {
150
+ await runCLI(root, { version: "0.1.0" });
151
+ }
152
+ ```
153
+
154
+ ```sh
155
+ mytool greet --name world # CLI
156
+ mytool mcp # MCP stdio server (Claude Desktop, etc.)
157
+ ```
158
+
159
+ The SDK is a separate import for in-process callers — your library code, tests, other packages:
160
+
161
+ ```ts
162
+ import { createSDK } from "toolcraft/sdk";
163
+ import { root } from "mytool/root";
164
+
165
+ const sdk = createSDK(root);
166
+ const { message } = await sdk.greet({ name: "world", loud: true });
167
+ ```
168
+
169
+ The same `root` flows into all three. No duplication.
170
+
171
+ ## Mental model
172
+
173
+ **Command**: one operation. Has a name, a `params` schema, optional `secrets`, a `handler`. The handler receives `{ params, secrets, fetch, fs, env, progress, ...services }` and returns a value.
174
+
175
+ **Group**: a folder. Has a name and `children`. Inheritable fields (`secrets`, `requires`, `scope`, `humanInLoop`) cascade to descendants. A group can also proxy an upstream MCP server (see below).
176
+
177
+ **Scope**: which runtimes a node is exposed on. Per-command default is `["cli", "sdk"]`. Set `scope: ["cli", "mcp", "sdk"]` to also surface as an MCP tool. Inherited from parent group when not set on the child.
178
+
179
+ **Tree**: the `root` group is a `defineGroup` whose children are commands and sub-groups. Any depth. CLI flags, MCP tool names, and SDK methods are derived from the path.
180
+
181
+ ## Secrets
182
+
183
+ Declare env-backed secrets on a command or group. Toolcraft reads `process.env` at command-run time and passes the values to the handler:
184
+
185
+ ```ts
186
+ const deploy = defineCommand({
187
+ name: "deploy",
188
+ params: S.Object({ service: S.String() }),
189
+ secrets: {
190
+ apiKey: { env: "DEPLOY_API_KEY", description: "Required for /deploy endpoint" },
191
+ debugToken: { env: "DEPLOY_DEBUG", optional: true },
192
+ },
193
+ handler: async ({ params, secrets }) => {
194
+ // secrets.apiKey: string
195
+ // secrets.debugToken: string | undefined
196
+ },
197
+ });
198
+ ```
199
+
200
+ Required secrets that aren't set produce a `UserError` with the env var name and description before the handler runs. Declaring `secrets` on a group cascades them down — a group-level `apiKey` is visible inside every descendant handler with full type inference.
201
+
202
+ ## Preconditions (`requires`)
203
+
204
+ ```ts
205
+ defineCommand({
206
+ // ...
207
+ requires: {
208
+ auth: true, // fails if POE_API_KEY (or runner-specified env) is missing
209
+ apiVersion: ">=1.2.0", // fails if runner reports older apiVersion
210
+ check: async (ctx) => ({ // arbitrary async gate
211
+ ok: ctx.fs.exists(".lock") === false,
212
+ message: ".lock present, refusing to run",
213
+ }),
214
+ },
215
+ });
216
+ ```
217
+
218
+ Runners pass `{ apiVersion }` to `runCLI` / `runMCP` / `createSDK` to populate the `apiVersion` check. Group-level `requires.check` runs before the child's; both must pass.
219
+
220
+ ## Services (dependency injection)
221
+
222
+ Inject shared services (DB clients, loggers, fetch wrappers) once at the runtime boundary, get them in every handler:
223
+
224
+ ```ts
225
+ type Services = { db: DbClient; logger: Logger };
226
+
227
+ const root = defineGroup<Services>({ ... });
228
+
229
+ await runCLI(root, {
230
+ services: { db, logger },
231
+ });
232
+ ```
233
+
234
+ Inside a handler:
235
+
236
+ ```ts
237
+ defineCommand<Services>({
238
+ // ...
239
+ handler: async ({ params, db, logger }) => {
240
+ logger.info("running");
241
+ return db.query(params.id);
242
+ },
243
+ });
244
+ ```
245
+
246
+ Services are merged into the handler context alongside the built-ins (`fetch`, `fs`, `env`, `progress`).
247
+
248
+ ## Output rendering
249
+
250
+ Handlers return raw values. Add per-format renderers when you want richer CLI output:
251
+
252
+ ```ts
253
+ defineCommand({
254
+ // ...
255
+ handler: async () => ({ rows: [{ id: 1 }, { id: 2 }] }),
256
+ render: {
257
+ rich: (result, { renderTable }) =>
258
+ console.log(renderTable({ rows: result.rows, columns: ["id"] })),
259
+ markdown: (result) => `Found ${result.rows.length} rows`,
260
+ json: (result) => result,
261
+ },
262
+ });
263
+ ```
264
+
265
+ CLI picks `rich` by default, `--json` switches to `json`. SDK and MCP always return the raw handler value.
266
+
267
+ ## MCP proxy: adopt an existing MCP server
268
+
269
+ If you already run an upstream MCP (e.g. `github-mcp-server`) and you want a subset under your tree:
270
+
271
+ ```ts
272
+ defineGroup({
273
+ name: "github",
274
+ mcp: {
275
+ transport: "stdio",
276
+ command: "github-mcp-server",
277
+ },
278
+ tools: ["create_issue", "list_issues"],
279
+ rename: {
280
+ create_issue: "issues.create",
281
+ },
282
+ children: [],
283
+ });
284
+ ```
285
+
286
+ - `tools` filters by upstream tool name.
287
+ - `rename` remaps to dotted toolcraft paths; missing intermediate groups are created.
288
+ - Proxy discovery is eager for `runCLI` and `runMCP`: they resolve every `defineGroup({ mcp })` proxy in the root tree before routing, command execution, or CLI help rendering. SDK proxies resolve when the deferred SDK is awaited or first used.
289
+ - On a first run without cached schemas, even `my-cli --help` or `my-cli some-group --help` may connect to every configured upstream MCP server.
290
+ - Discovery is cached at `<projectRoot>/.toolcraft/mcp/<group>.json` (project root = nearest ancestor with `package.json`), so successful discovery avoids repeated upstream connects unless refreshed.
291
+ - `TOOLCRAFT_MCP_REFRESH=1` refreshes all proxies; `TOOLCRAFT_MCP_REFRESH=github,linear` refreshes specific ones.
292
+ - Selective or lazy discovery for only the requested command path is not currently supported. CLIs that wrap many MCP servers should expect first-run help to touch all of them.
293
+ - Discovery output goes to stderr only.
294
+
295
+ ## Human-in-loop approvals
296
+
297
+ Gate destructive commands on a human approval. Configure on the command (or inherit from a group):
298
+
299
+ ```ts
300
+ defineGroup({
301
+ name: "deploy",
302
+ humanInLoop: {
303
+ mode: "async",
304
+ message: ({ commandPath, params }) => `Run ${commandPath} for ${params.target}?`,
305
+ },
306
+ children: [
307
+ defineCommand({
308
+ name: "prod",
309
+ params: S.Object({ target: S.String() }),
310
+ handler: async ({ params }) => ({ target: params.target }),
311
+ }),
312
+ defineCommand({
313
+ name: "preview",
314
+ params: S.Object({ target: S.String() }),
315
+ humanInLoop: null, // opt out
316
+ handler: async ({ params }) => ({ target: params.target }),
317
+ }),
318
+ ],
32
319
  });
33
320
  ```
34
321
 
35
- ## API
322
+ Modes:
323
+
324
+ - `sync` — handler waits for approval before running.
325
+ - `async` — toolcraft enqueues the command, returns a pending marker, and runs it in a fresh process when an operator approves via the reserved `approvals` group.
326
+
327
+ Wire the same `humanInLoop` options into every entrypoint:
328
+
329
+ ```ts
330
+ const humanInLoop = {
331
+ provider: slackApprovalProvider({ channel: "#deploys", client }),
332
+ taskList: { dir: ".toolcraft/approvals.yaml", format: "yaml-file" as const },
333
+ };
334
+
335
+ await runCLI(root, { humanInLoop });
336
+ createMCPServer(root, { name: "mytool", version: "0.1.0", humanInLoop });
337
+ const sdk = createSDK(root, { humanInLoop });
338
+ ```
339
+
340
+ If `provider` is omitted, toolcraft picks a default lazily on first use: `osascriptProvider` on macOS; otherwise a stub that throws `UserError("no human-in-loop provider configured for this platform")`.
341
+
342
+ A built-in `approvals` group is auto-merged into every root:
343
+
344
+ - `approvals list` — list pending tasks (CLI, MCP, SDK).
345
+ - `approvals show --approval-id <id>` — show one task.
346
+ - `approvals run --approval-id <id>` — execute one queued task. CLI-only; used by the detached runner.
36
347
 
37
- - `defineCommand(config)` creates a typed command definition with inferred `params` and `secrets`.
38
- - `defineGroup(config)` creates a command group and inherits `secrets`, `requires`, and `scope` through descendants.
39
- - `UserError` marks expected user-facing failures.
40
- - `createMCPServer(root, options)` exposes `mcp`-scoped commands as MCP tools.
41
- - `runMCP(root, options)` starts the stdio MCP server for the given command tree.
348
+ The name `approvals` is reserved. Defining your own `approvals` group fails at startup.
349
+
350
+ The async runner re-execs your binary (`process.execPath` + `process.argv[1]` by default; override via `humanInLoop.binPath`). Re-exec calls the same toolcraft entrypoint with the same `humanInLoop` options — do not branch on `argv` before calling `runCLI`/`runMCP`/`createSDK`.
351
+
352
+ Async results must be JSON-serializable; non-serializable returns mark the approval as failed instead of being persisted.
353
+
354
+ A minimal Slack-style provider:
355
+
356
+ ```ts
357
+ import type { ApprovalRequest, ApprovalResult, HumanInLoopProvider } from "@poe-code/agent-human-in-loop";
358
+
359
+ export function slackApprovalProvider(opts: {
360
+ channel: string;
361
+ client: {
362
+ postApprovalMessage(channel: string, message: string): Promise<string>;
363
+ waitForButtonClick(ts: string): Promise<{ action: "approve" | "decline"; userId: string }>;
364
+ openModal(userId: string, prompt: string): Promise<string | undefined>;
365
+ };
366
+ }): HumanInLoopProvider {
367
+ return {
368
+ id: "slack-approval",
369
+ async requestApproval(request: ApprovalRequest): Promise<ApprovalResult> {
370
+ const ts = await opts.client.postApprovalMessage(opts.channel, request.message);
371
+ const click = await opts.client.waitForButtonClick(ts);
372
+
373
+ if (click.action === "approve") {
374
+ return { outcome: "approved" };
375
+ }
376
+
377
+ if (request.declineInputPrompt) {
378
+ const reason = await opts.client.openModal(click.userId, request.declineInputPrompt);
379
+ return reason ? { outcome: "declined", reason } : { outcome: "declined" };
380
+ }
381
+
382
+ return { outcome: "declined" };
383
+ },
384
+ };
385
+ }
386
+ ```
387
+
388
+ ## Errors
389
+
390
+ Throw `UserError` for expected, user-facing failures. The CLI prints the message without a stack trace and sets exit code 1; MCP and SDK surface the message as the error body. Any other thrown error is treated as unexpected and shows a stack with `--verbose`.
391
+
392
+ ## Migrating from a folder of scripts
393
+
394
+ Pattern for adopting toolcraft incrementally:
395
+
396
+ 1. Pick one script. Wrap its logic in `defineCommand`. Keep the existing imports, `fetch` calls, file I/O — they all work inside a handler.
397
+ 2. Move env-var reads to `secrets`. Replace `process.env.X` access in the script body with `secrets.x` from the handler context.
398
+ 3. Add the command to a `defineGroup`. Repeat. The tree grows file by file.
399
+ 4. When you're ready, point your `bin` at `runCLI` and delete the per-script entry points. The script files become handler implementations imported by `defineCommand`s.
400
+ 5. To expose to an MCP client, set `scope: ["cli", "mcp", "sdk"]` on the command and add the `runMCP` branch to the bin. No code changes needed inside handlers.
401
+ 6. To expose to other JS code, `import { createSDK }` from your package and call methods directly.
402
+
403
+ If you have an existing MCP server you want to keep running, use the MCP proxy: a `defineGroup` with an `mcp` field pulls its tools into your tree without rewriting them.
42
404
 
43
405
  ## Environment variables
44
406
 
45
- - This package does not read environment variables directly.
46
- - Commands can declare required or optional secret environment variable names via `secrets`.
407
+ - `TOOLCRAFT_MCP_REFRESH` MCP proxy cache refresh (`unset` = use cache, `1`/`true` = refresh all, comma-separated names = refresh those).
408
+ - Per-command `secrets` declarations name additional env vars. They are read at command run time and passed to the handler.
47
409
 
48
- ## Configuration
410
+ ## API reference
49
411
 
50
412
  ### `defineCommand(config)`
51
413
 
52
- - `name: string`: command name.
53
- - `description?: string`: help text for the command.
54
- - `aliases?: string[]`: alternate command names.
55
- - `positional?: string[]`: positional parameter names mapped from CLI argv order.
56
- - `params: S.Object(...)`: command parameter schema.
57
- - `secrets?: Record<string, { env: string; description?: string; optional?: boolean }>`: environment-backed secrets available in the handler context.
58
- - `scope?: Array<"cli" | "mcp" | "sdk">`: runner visibility. Defaults to `["cli", "sdk"]`.
59
- - `confirm?: boolean`: whether the command requires confirmation before execution. Defaults to `false`.
60
- - `requires?: { auth?: boolean; apiVersion?: string; check?: (ctx) => Promise<{ ok: boolean; message?: string }> }`: command preconditions.
61
- - `handler: (ctx) => Promise<unknown>`: async command implementation.
62
- - `render?: { rich?: (result, primitives) => void; markdown?: (result) => string; json?: (result) => unknown }`: optional output renderers.
414
+ - `name: string`
415
+ - `description?: string`
416
+ - `aliases?: string[]`
417
+ - `positional?: string[]` parameter names mapped from CLI argv order.
418
+ - `params: S.Object(...)` input schema from `toolcraft-schema`.
419
+ - `secrets?: Record<string, { env: string; description?: string; optional?: boolean }>`
420
+ - `scope?: Array<"cli" | "mcp" | "sdk">` defaults to `["cli", "sdk"]`.
421
+ - `confirm?: boolean` deprecated CLI-only TTY confirmation; use `humanInLoop` instead. Cannot be combined with `humanInLoop`.
422
+ - `humanInLoop?: { mode: "sync" | "async"; message: ({ params, commandPath }) => string; declineInputPrompt?: string } | null`
423
+ - `requires?: { auth?: boolean; apiVersion?: string; check?: (ctx) => Promise<{ ok: boolean; message?: string }> }`
424
+ - `handler: (ctx) => Promise<unknown>`
425
+ - `render?: { rich?, markdown?, json? }` — per-format output renderers.
63
426
 
64
427
  ### `defineGroup(config)`
65
428
 
66
- - `name: string`: group name.
67
- - `description?: string`: help text for the group.
68
- - `aliases?: string[]`: alternate group names.
69
- - `scope?: Array<"cli" | "mcp" | "sdk">`: inherited by descendants that do not override it.
70
- - `secrets?: Record<string, { env: string; description?: string; optional?: boolean }>`: inherited secret declarations.
71
- - `requires?: { auth?: boolean; apiVersion?: string; check?: (ctx) => Promise<{ ok: boolean; message?: string }> }`: inherited preconditions.
72
- - `children: Array<Command | Group>`: nested commands and groups.
73
- - `default?: Command`: default child command used by runners when no child token matches.
429
+ - `name: string`
430
+ - `description?: string`
431
+ - `aliases?: string[]`
432
+ - `mcp?: McpServerConfig` proxy an upstream MCP server; uses the standard `@poe-code/agent-mcp-config` shape.
433
+ - `tools?: string[]` proxy allowlist by upstream tool name.
434
+ - `rename?: Record<string, string>` proxy upstream dotted toolcraft path.
435
+ - `scope?` / `humanInLoop?` / `secrets?` / `requires?` — inherited by descendants that don't override. Set `humanInLoop: null` on a child to opt out.
436
+ - `children: Array<Command | Group>`
437
+ - `default?: Command` — invoked when no child token matches.
438
+
439
+ ### `runCLI(root, options)`
440
+
441
+ - `casing?: "kebab" | "snake"` — generated CLI flag style.
442
+ - `services?: TServices` — merged into every handler context.
443
+ - `version?: string` — surfaced via `--version`.
444
+ - `apiVersion?: string` — for `requires.apiVersion`.
445
+ - `humanInLoop?: HumanInLoopRuntimeOptions`
446
+
447
+ ### `createSDK(root, options)`
448
+
449
+ - `casing?: "camel"` — generated SDK member style.
450
+ - `services?` / `humanInLoop?` / `apiVersion?`
74
451
 
75
452
  ### `createMCPServer(root, options)` / `runMCP(root, options)`
76
453
 
77
- - `name: string`: MCP server name.
78
- - `version: string`: MCP server version.
79
- - `services?: TServices`: extra services merged into the handler context.
80
- - `tools?: string[]`: optional allowlist of MCP tool names or group prefixes. Tool names use `__`-joined snake_case path segments like `root__bot__create`; passing `root__bot` includes every descendant tool in that subtree.
81
- - `casing?: "snake" | "camel"`: changes MCP input-schema property names and accepted argument keys only. It does **not** change MCP tool names, which always stay `__`-joined snake_case.
454
+ - `name: string`
455
+ - `version: string`
456
+ - `services?` / `humanInLoop?` / `apiVersion?`
457
+ - `tools?: string[]` allowlist of MCP tool names or group prefixes. Tool names are `__`-joined snake_case path segments (`root__bot__create`); a prefix like `root__bot` includes every descendant tool.
458
+ - `casing?: "snake" | "camel"` affects MCP **input-schema property names** only. Tool names always stay `__`-joined snake_case.
459
+
460
+ ### `HumanInLoopRuntimeOptions`
461
+
462
+ ```ts
463
+ type HumanInLoopRuntimeOptions = {
464
+ provider?: HumanInLoopProvider;
465
+ taskList?: TaskList | { dir: string; format: "markdown-dir" | "yaml-file" };
466
+ listName?: string; // defaults to "approvals"
467
+ binPath?: { execPath: string; entryArgs: readonly string[] };
468
+ };
469
+ ```
82
470
 
83
471
  ### Handler context
84
472
 
85
- - `params`: inferred from the command `params` schema.
86
- - `secrets`: inferred from the command `secrets` declaration.
87
- - `fetch`: `typeof globalThis.fetch`.
88
- - `fs`: `{ readFile, writeFile, exists }`.
89
- - `env`: `{ get(key: string): string | undefined }`.
90
- - `progress(message: string): void`.
91
- - Custom runner services are merged into the same context object.
473
+ - `params` inferred from the command `params` schema.
474
+ - `secrets` inferred from the command `secrets` declaration.
475
+ - `fetch: typeof globalThis.fetch`
476
+ - `fs: { readFile, writeFile, exists }`
477
+ - `env: { get(key: string): string | undefined }`
478
+ - `progress(message: string): void`
479
+ - All `services` keys merged in.
480
+
481
+ ### Exports
482
+
483
+ - `defineCommand`, `defineGroup`
484
+ - `S`, `toJsonSchema`, type helpers — re-exported from `toolcraft-schema`
485
+ - `UserError`, `ApprovalDeclinedError`
486
+ - Type exports: `Command`, `Group`, `Scope`, `HandlerContext`, `HumanInLoopConfig`, `HumanInLoopPending`, `HumanInLoopRuntimeOptions`, schema types from `toolcraft-schema`.
487
+
488
+ Subpath imports:
489
+
490
+ - `toolcraft/cli` — `runCLI`
491
+ - `toolcraft/sdk` — `createSDK`
492
+ - `toolcraft/mcp` — `runMCP`, `createMCPServer`
493
+ - `toolcraft/human-in-loop` — provider helpers
494
+ - `toolcraft/mcp-proxy` — proxy internals
@@ -14,6 +14,7 @@ const ignoredRoot = defineGroup({
14
14
  });
15
15
  const ignoredOptions = {
16
16
  casing: "kebab",
17
+ humanInLoop: {},
17
18
  version: "1.0.0",
18
19
  };
19
20
  const ignoredServiceOptions = {
package/dist/cli.d.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  import type { Group } from "./index.js";
2
+ import type { HumanInLoopRuntimeOptions } from "./human-in-loop/index.js";
2
3
  type Casing = "kebab" | "snake";
3
4
  export interface RunCLIOptions<TServices extends object = Record<string, unknown>> {
4
5
  apiVersion?: string;
5
6
  casing?: Casing;
7
+ humanInLoop?: HumanInLoopRuntimeOptions;
6
8
  rootDisplayName?: string;
7
9
  rootUsageName?: string;
8
10
  services?: TServices;