toolcraft 0.0.23 → 0.0.25

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 (152) hide show
  1. package/README.md +2 -2
  2. package/dist/cli.compile-check.js +1 -0
  3. package/dist/cli.d.ts +1 -0
  4. package/dist/cli.js +50 -13
  5. package/dist/error-report.js +32 -3
  6. package/dist/human-in-loop/approval-tasks.d.ts +1 -0
  7. package/dist/human-in-loop/approval-tasks.js +7 -5
  8. package/dist/human-in-loop/approvals-commands.js +51 -8
  9. package/dist/human-in-loop/runner.js +24 -19
  10. package/dist/human-in-loop/state-machine.d.ts +3 -3
  11. package/dist/human-in-loop/state-machine.js +13 -5
  12. package/dist/index.d.ts +5 -0
  13. package/dist/index.js +6 -1
  14. package/dist/mcp-proxy.js +85 -19
  15. package/dist/mcp.compile-check.js +1 -0
  16. package/dist/mcp.d.ts +1 -0
  17. package/dist/mcp.js +50 -8
  18. package/dist/renderer.js +119 -13
  19. package/dist/sdk.compile-check.js +1 -0
  20. package/dist/sdk.d.ts +1 -0
  21. package/dist/sdk.js +56 -11
  22. package/node_modules/@poe-code/agent-defs/dist/registry.d.ts +1 -1
  23. package/node_modules/@poe-code/agent-defs/dist/registry.js +22 -11
  24. package/node_modules/@poe-code/agent-defs/package.json +1 -1
  25. package/node_modules/@poe-code/agent-human-in-loop/dist/providers/osascript-script.js +5 -1
  26. package/node_modules/@poe-code/agent-human-in-loop/dist/providers/osascript.js +1 -1
  27. package/node_modules/@poe-code/agent-human-in-loop/package.json +1 -1
  28. package/node_modules/@poe-code/agent-mcp-config/dist/apply.d.ts +1 -1
  29. package/node_modules/@poe-code/agent-mcp-config/dist/apply.js +41 -92
  30. package/node_modules/@poe-code/agent-mcp-config/dist/configs.js +4 -1
  31. package/node_modules/@poe-code/agent-mcp-config/dist/shapes.d.ts +14 -2
  32. package/node_modules/@poe-code/agent-mcp-config/dist/shapes.js +11 -4
  33. package/node_modules/@poe-code/agent-mcp-config/package.json +1 -1
  34. package/node_modules/@poe-code/config-mutations/dist/execution/apply-mutation.js +200 -22
  35. package/node_modules/@poe-code/config-mutations/dist/execution/path-utils.js +7 -1
  36. package/node_modules/@poe-code/config-mutations/dist/formats/index.js +1 -1
  37. package/node_modules/@poe-code/config-mutations/dist/formats/json.js +11 -7
  38. package/node_modules/@poe-code/config-mutations/dist/formats/object.d.ts +4 -0
  39. package/node_modules/@poe-code/config-mutations/dist/formats/object.js +27 -0
  40. package/node_modules/@poe-code/config-mutations/dist/formats/toml.js +12 -9
  41. package/node_modules/@poe-code/config-mutations/dist/formats/yaml.js +12 -9
  42. package/node_modules/@poe-code/config-mutations/dist/mutations/file-mutation.d.ts +11 -1
  43. package/node_modules/@poe-code/config-mutations/dist/mutations/file-mutation.js +10 -1
  44. package/node_modules/@poe-code/config-mutations/dist/testing/mock-fs.js +25 -1
  45. package/node_modules/@poe-code/config-mutations/dist/types.d.ts +12 -2
  46. package/node_modules/@poe-code/config-mutations/package.json +1 -1
  47. package/node_modules/@poe-code/design-system/dist/acp/components.js +3 -1
  48. package/node_modules/@poe-code/design-system/dist/components/browser.d.ts +1 -1
  49. package/node_modules/@poe-code/design-system/dist/components/browser.js +6 -1
  50. package/node_modules/@poe-code/design-system/dist/components/color.js +9 -8
  51. package/node_modules/@poe-code/design-system/dist/components/command-errors.js +3 -2
  52. package/node_modules/@poe-code/design-system/dist/components/detail-card.d.ts +22 -0
  53. package/node_modules/@poe-code/design-system/dist/components/detail-card.js +69 -0
  54. package/node_modules/@poe-code/design-system/dist/components/help-formatter.js +88 -11
  55. package/node_modules/@poe-code/design-system/dist/components/index.d.ts +1 -1
  56. package/node_modules/@poe-code/design-system/dist/components/index.js +1 -1
  57. package/node_modules/@poe-code/design-system/dist/components/table.d.ts +2 -0
  58. package/node_modules/@poe-code/design-system/dist/components/table.js +82 -5
  59. package/node_modules/@poe-code/design-system/dist/components/template.d.ts +4 -0
  60. package/node_modules/@poe-code/design-system/dist/components/template.js +198 -32
  61. package/node_modules/@poe-code/design-system/dist/components/text.js +29 -5
  62. package/node_modules/@poe-code/design-system/dist/dashboard/ansi.d.ts +2 -2
  63. package/node_modules/@poe-code/design-system/dist/dashboard/ansi.js +77 -32
  64. package/node_modules/@poe-code/design-system/dist/dashboard/buffer.js +28 -5
  65. package/node_modules/@poe-code/design-system/dist/dashboard/components/output-pane.js +45 -28
  66. package/node_modules/@poe-code/design-system/dist/dashboard/terminal-width.d.ts +4 -0
  67. package/node_modules/@poe-code/design-system/dist/dashboard/terminal-width.js +71 -0
  68. package/node_modules/@poe-code/design-system/dist/dashboard/types.d.ts +1 -0
  69. package/node_modules/@poe-code/design-system/dist/explorer/events.d.ts +6 -0
  70. package/node_modules/@poe-code/design-system/dist/explorer/reducer.js +32 -10
  71. package/node_modules/@poe-code/design-system/dist/explorer/render/detail.js +3 -0
  72. package/node_modules/@poe-code/design-system/dist/explorer/runtime.js +57 -6
  73. package/node_modules/@poe-code/design-system/dist/explorer/state.d.ts +1 -0
  74. package/node_modules/@poe-code/design-system/dist/explorer/state.js +12 -15
  75. package/node_modules/@poe-code/design-system/dist/index.d.ts +3 -1
  76. package/node_modules/@poe-code/design-system/dist/index.js +2 -1
  77. package/node_modules/@poe-code/design-system/dist/prompts/primitives/intro.js +2 -1
  78. package/node_modules/@poe-code/design-system/dist/prompts/primitives/log.js +8 -5
  79. package/node_modules/@poe-code/design-system/dist/prompts/primitives/note.js +1 -1
  80. package/node_modules/@poe-code/design-system/dist/static/menu.js +8 -2
  81. package/node_modules/@poe-code/design-system/dist/static/spinner.js +10 -4
  82. package/node_modules/@poe-code/design-system/dist/terminal-markdown/parser/frontmatter.js +9 -2
  83. package/node_modules/@poe-code/design-system/dist/terminal-markdown/renderer.js +19 -2
  84. package/node_modules/@poe-code/design-system/package.json +2 -1
  85. package/node_modules/@poe-code/process-runner/dist/docker/args.d.ts +1 -0
  86. package/node_modules/@poe-code/process-runner/dist/docker/args.js +11 -3
  87. package/node_modules/@poe-code/process-runner/dist/docker/docker-execution-env.js +377 -130
  88. package/node_modules/@poe-code/process-runner/dist/docker/docker-runner.js +78 -10
  89. package/node_modules/@poe-code/process-runner/dist/docker/env-file.d.ts +6 -0
  90. package/node_modules/@poe-code/process-runner/dist/docker/env-file.js +49 -0
  91. package/node_modules/@poe-code/process-runner/dist/host/host-execution-env.js +3 -2
  92. package/node_modules/@poe-code/process-runner/dist/host/host-runner.js +21 -5
  93. package/node_modules/@poe-code/process-runner/dist/index.d.ts +1 -0
  94. package/node_modules/@poe-code/process-runner/dist/index.js +1 -0
  95. package/node_modules/@poe-code/process-runner/dist/testing/mock-runner.js +30 -8
  96. package/node_modules/@poe-code/process-runner/dist/types.d.ts +6 -0
  97. package/node_modules/@poe-code/process-runner/dist/workspace-transfer.d.ts +61 -0
  98. package/node_modules/@poe-code/process-runner/dist/workspace-transfer.js +503 -0
  99. package/node_modules/@poe-code/process-runner/package.json +1 -1
  100. package/node_modules/@poe-code/task-list/README.md +0 -2
  101. package/node_modules/@poe-code/task-list/dist/backends/gh-issues-client.js +3 -0
  102. package/node_modules/@poe-code/task-list/dist/backends/gh-issues-sync.js +89 -59
  103. package/node_modules/@poe-code/task-list/dist/backends/gh-issues.d.ts +9 -3
  104. package/node_modules/@poe-code/task-list/dist/backends/gh-issues.js +460 -99
  105. package/node_modules/@poe-code/task-list/dist/backends/markdown-dir.js +156 -154
  106. package/node_modules/@poe-code/task-list/dist/backends/utils.d.ts +2 -0
  107. package/node_modules/@poe-code/task-list/dist/backends/utils.js +79 -0
  108. package/node_modules/@poe-code/task-list/dist/backends/yaml-file.js +120 -132
  109. package/node_modules/@poe-code/task-list/dist/index.d.ts +3 -1
  110. package/node_modules/@poe-code/task-list/dist/index.js +2 -0
  111. package/node_modules/@poe-code/task-list/dist/move.d.ts +2 -0
  112. package/node_modules/@poe-code/task-list/dist/move.js +215 -0
  113. package/node_modules/@poe-code/task-list/dist/open.js +3 -4
  114. package/node_modules/@poe-code/task-list/dist/state-machine.js +3 -1
  115. package/node_modules/@poe-code/task-list/dist/state.js +9 -0
  116. package/node_modules/@poe-code/task-list/dist/types.d.ts +48 -13
  117. package/node_modules/@poe-code/task-list/package.json +1 -2
  118. package/node_modules/auth-store/dist/create-secret-store.js +4 -1
  119. package/node_modules/auth-store/dist/encrypted-file-store.d.ts +8 -0
  120. package/node_modules/auth-store/dist/encrypted-file-store.js +104 -8
  121. package/node_modules/auth-store/dist/index.d.ts +1 -1
  122. package/node_modules/auth-store/dist/keychain-store.d.ts +4 -1
  123. package/node_modules/auth-store/dist/keychain-store.js +18 -16
  124. package/node_modules/auth-store/dist/provider-store.d.ts +5 -1
  125. package/node_modules/auth-store/dist/provider-store.js +55 -7
  126. package/node_modules/auth-store/dist/types.d.ts +3 -1
  127. package/node_modules/auth-store/package.json +2 -1
  128. package/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.js +46 -15
  129. package/node_modules/mcp-oauth/dist/client/loopback-authorization.js +49 -12
  130. package/node_modules/mcp-oauth/dist/client/token-endpoint.js +6 -1
  131. package/node_modules/mcp-oauth/dist/server/jwks-token-verifier.js +1 -1
  132. package/node_modules/mcp-oauth/package.json +1 -0
  133. package/node_modules/tiny-mcp-client/.turbo/turbo-build.log +1 -1
  134. package/node_modules/tiny-mcp-client/dist/internal.d.ts +9 -4
  135. package/node_modules/tiny-mcp-client/dist/internal.js +244 -66
  136. package/node_modules/tiny-mcp-client/dist/oauth-discovery.d.ts +1 -1
  137. package/node_modules/tiny-mcp-client/dist/oauth-discovery.js +4 -7
  138. package/node_modules/tiny-mcp-client/package.json +2 -1
  139. package/node_modules/tiny-mcp-client/src/http-oauth.integration.test.ts +1 -1
  140. package/node_modules/tiny-mcp-client/src/http-oauth.test.ts +46 -0
  141. package/node_modules/tiny-mcp-client/src/internal.ts +287 -76
  142. package/node_modules/tiny-mcp-client/src/mcp-client-sdk.test.ts +32 -0
  143. package/node_modules/tiny-mcp-client/src/mcp-client-tiny-stdio-test-server-tools.test.ts +1 -1
  144. package/node_modules/tiny-mcp-client/src/oauth-discovery.ts +5 -10
  145. package/node_modules/tiny-mcp-client/src/transports.test.ts +588 -6
  146. package/package.json +10 -12
  147. package/node_modules/@poe-code/file-lock/README.md +0 -52
  148. package/node_modules/@poe-code/file-lock/dist/index.d.ts +0 -1
  149. package/node_modules/@poe-code/file-lock/dist/index.js +0 -1
  150. package/node_modules/@poe-code/file-lock/dist/lock.d.ts +0 -27
  151. package/node_modules/@poe-code/file-lock/dist/lock.js +0 -203
  152. package/node_modules/@poe-code/file-lock/package.json +0 -23
package/dist/mcp-proxy.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { existsSync } from "node:fs";
2
- import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
2
+ import { lstat, mkdir, readFile, rename, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
+ import { createHash, randomUUID } from "node:crypto";
4
5
  import { createLogger } from "@poe-code/design-system";
5
6
  import { HttpTransport, McpClient, StdioTransport } from "tiny-mcp-client";
6
7
  import { convertJsonSchema } from "./json-schema-converter.js";
@@ -153,6 +154,7 @@ async function ensureConnected(connection) {
153
154
  }
154
155
  async function readCache(cachePath) {
155
156
  try {
157
+ await assertCachePathHasNoSymlinks(cachePath);
156
158
  const raw = await readFile(cachePath, "utf8");
157
159
  const parsed = JSON.parse(raw);
158
160
  if (parsed === null ||
@@ -168,6 +170,7 @@ async function readCache(cachePath) {
168
170
  fetchedAt: typeof parsed.fetchedAt === "string" ? parsed.fetchedAt : new Date(0).toISOString(),
169
171
  tools: parsed.tools,
170
172
  upstream: parsed.upstream,
173
+ configFingerprint: typeof parsed.configFingerprint === "string" ? parsed.configFingerprint : undefined,
171
174
  version: parsed.version === 1 ? 1 : 1,
172
175
  };
173
176
  }
@@ -181,12 +184,17 @@ async function readCache(cachePath) {
181
184
  }
182
185
  async function writeCache(cachePath, cache) {
183
186
  const directory = path.dirname(cachePath);
184
- const tempPath = `${cachePath}.tmp`;
187
+ const tempPath = `${cachePath}.tmp-${randomUUID()}`;
188
+ await assertCachePathHasNoSymlinks(cachePath);
189
+ await assertCachePathHasNoSymlinks(tempPath);
185
190
  await mkdir(directory, { recursive: true });
191
+ await assertCachePathHasNoSymlinks(directory);
186
192
  await writeFile(tempPath, `${JSON.stringify(cache, null, 2)}\n`);
193
+ await assertCachePathHasNoSymlinks(tempPath);
194
+ await assertCachePathHasNoSymlinks(cachePath);
187
195
  await rename(tempPath, cachePath);
188
196
  }
189
- async function fetchCache(name, config, cachePath) {
197
+ async function fetchCache(name, config) {
190
198
  const logger = createLogger((message) => {
191
199
  process.stderr.write(`${message}\n`);
192
200
  });
@@ -210,27 +218,16 @@ async function fetchCache(name, config, cachePath) {
210
218
  $schema: MCP_PROXY_SCHEMA_URL,
211
219
  version: 1,
212
220
  upstream,
221
+ configFingerprint: fingerprintMcpServerConfig(config),
213
222
  fetchedAt: new Date().toISOString(),
214
223
  tools,
215
224
  };
216
- await writeCache(cachePath, cache);
217
- logger.info(`MCP ${name}: wrote ${cachePath}`);
218
225
  return cache;
219
226
  }
220
227
  finally {
221
228
  await client.close();
222
229
  }
223
230
  }
224
- async function deleteCacheIfPresent(cachePath) {
225
- try {
226
- await unlink(cachePath);
227
- }
228
- catch (error) {
229
- if (error.code !== "ENOENT") {
230
- throw error;
231
- }
232
- }
233
- }
234
231
  function populateGroupFromTools(group, tools, rename, connection) {
235
232
  removeProxyChildren(group);
236
233
  for (const tool of tools) {
@@ -262,6 +259,31 @@ function populateGroupFromTools(group, tools, rename, connection) {
262
259
  parent.children.push(createProxyCommand(parent, tool, commandName, connection));
263
260
  }
264
261
  }
262
+ function replaceProxyChildrenSafely(group, tools, rename, connection) {
263
+ const previousChildren = snapshotGroupChildren(group);
264
+ try {
265
+ populateGroupFromTools(group, tools, rename, connection);
266
+ }
267
+ catch (error) {
268
+ for (const [capturedGroup, children] of previousChildren) {
269
+ capturedGroup.children = children;
270
+ }
271
+ throw error;
272
+ }
273
+ }
274
+ function snapshotGroupChildren(group) {
275
+ const snapshot = new Map();
276
+ const visit = (current) => {
277
+ snapshot.set(current, [...current.children]);
278
+ for (const child of current.children) {
279
+ if (child.kind === "group") {
280
+ visit(child);
281
+ }
282
+ }
283
+ };
284
+ visit(group);
285
+ return snapshot;
286
+ }
265
287
  function isRefreshRequested(name, refresh) {
266
288
  if (refresh === "all") {
267
289
  return true;
@@ -279,19 +301,31 @@ async function resolveSingleProxy(group, options) {
279
301
  const cachePath = resolveCachePath(name, options.projectRoot);
280
302
  const refresh = parseRefreshEnv(process.env.TOOLCRAFT_MCP_REFRESH);
281
303
  let cache;
304
+ let shouldWriteCache = false;
282
305
  if (isRefreshRequested(name, refresh)) {
283
- await deleteCacheIfPresent(cachePath);
284
- cache = await fetchCache(name, config, cachePath);
306
+ cache = await fetchCache(name, config);
307
+ shouldWriteCache = true;
285
308
  }
286
309
  else {
287
- cache = (await readCache(cachePath)) ?? (await fetchCache(name, config, cachePath));
310
+ const storedCache = await readCache(cachePath);
311
+ if (storedCache && cacheMatchesConfig(storedCache, config)) {
312
+ cache = storedCache;
313
+ }
314
+ else {
315
+ cache = await fetchCache(name, config);
316
+ shouldWriteCache = true;
317
+ }
288
318
  }
289
319
  const tools = filterAllowlistedTools(cache.tools, internal.tools);
290
320
  validateRenameMap(name, tools, internal.rename);
291
321
  const previousConnection = getProxyConnection(group);
292
322
  const nextConnection = createConnection(name, config);
293
323
  try {
294
- populateGroupFromTools(group, tools, internal.rename, nextConnection);
324
+ replaceProxyChildrenSafely(group, tools, internal.rename, nextConnection);
325
+ if (shouldWriteCache) {
326
+ await writeCache(cachePath, cache);
327
+ createLogger((message) => process.stderr.write(`${message}\n`)).info(`MCP ${name}: wrote ${cachePath}`);
328
+ }
295
329
  setProxyConnection(group, nextConnection);
296
330
  }
297
331
  catch (error) {
@@ -309,6 +343,12 @@ async function resolveSingleProxy(group, options) {
309
343
  throw new Error(`couldn't discover MCP ${name}: ${error instanceof Error ? error.message : String(error)}`);
310
344
  }
311
345
  }
346
+ function cacheMatchesConfig(cache, config) {
347
+ return cache.configFingerprint === fingerprintMcpServerConfig(config);
348
+ }
349
+ function fingerprintMcpServerConfig(config) {
350
+ return createHash("sha256").update(JSON.stringify(config)).digest("hex");
351
+ }
312
352
  function collectProxyGroups(root) {
313
353
  const groups = [];
314
354
  function visit(group) {
@@ -348,8 +388,34 @@ export function resolveCachePath(name, projectRoot) {
348
388
  if (resolvedProjectRoot === undefined) {
349
389
  throw new Error(`Could not find package.json above "${process.cwd()}" while resolving MCP cache path.`);
350
390
  }
391
+ if (name.length === 0 || name === "." || name === ".." || name.includes("/") || name.includes("\\")) {
392
+ throw new Error(`MCP proxy group name must be a file-safe name: "${name}".`);
393
+ }
351
394
  return path.join(resolvedProjectRoot, ".toolcraft", "mcp", `${name}.json`);
352
395
  }
396
+ async function assertCachePathHasNoSymlinks(filePath) {
397
+ let currentPath = filePath;
398
+ while (true) {
399
+ try {
400
+ if ((await lstat(currentPath)).isSymbolicLink()) {
401
+ throw new Error(`MCP cache path must not contain symbolic links: ${currentPath}.`);
402
+ }
403
+ }
404
+ catch (error) {
405
+ if (error.code !== "ENOENT") {
406
+ throw error;
407
+ }
408
+ }
409
+ if (path.basename(currentPath) === ".toolcraft") {
410
+ return;
411
+ }
412
+ const parentPath = path.dirname(currentPath);
413
+ if (parentPath === currentPath) {
414
+ return;
415
+ }
416
+ currentPath = parentPath;
417
+ }
418
+ }
353
419
  export function parseRefreshEnv(value) {
354
420
  const trimmed = value?.trim();
355
421
  if (trimmed === undefined || trimmed.length === 0) {
@@ -15,6 +15,7 @@ const ignoredRoot = defineGroup({
15
15
  ],
16
16
  });
17
17
  const ignoredOptions = {
18
+ approvals: false,
18
19
  name: "toolcraft-test",
19
20
  version: "1.0.0",
20
21
  tools: ["usage"],
package/dist/mcp.d.ts CHANGED
@@ -7,6 +7,7 @@ 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
+ approvals?: boolean;
10
11
  name: string;
11
12
  version?: string;
12
13
  humanInLoop?: HumanInLoopRuntimeOptions;
package/dist/mcp.js CHANGED
@@ -1,4 +1,4 @@
1
- import { access, readFile, writeFile } from "node:fs/promises";
1
+ import { access, lstat, readFile, rename, unlink, writeFile } from "node:fs/promises";
2
2
  import { createServer, JSON_RPC_ERROR_CODES, ToolError } from "tiny-stdio-mcp-server";
3
3
  import { toJsonSchema } from "toolcraft-schema";
4
4
  import { ToolcraftBugError, UserError, assertCommandRequirements, resolveCommandSecrets } from "./index.js";
@@ -104,7 +104,10 @@ function createFs() {
104
104
  catch {
105
105
  return false;
106
106
  }
107
- }
107
+ },
108
+ lstat: async (path) => lstat(path),
109
+ rename: async (fromPath, toPath) => rename(fromPath, toPath),
110
+ unlink: async (path) => unlink(path)
108
111
  };
109
112
  }
110
113
  function createEnv(values = process.env) {
@@ -181,8 +184,24 @@ function matchesAllowlist(toolName, allowlist) {
181
184
  function formatToolName(path) {
182
185
  return path.map((segment) => formatSegment(segment, "snake")).join("__");
183
186
  }
187
+ function validateUniqueMCPParameterFields(schema, casing) {
188
+ const sourceKeysByField = new Map();
189
+ for (const [key, rawChildSchema] of Object.entries(schema.shape)) {
190
+ const field = formatSegment(key, casing);
191
+ const existingKey = sourceKeysByField.get(field);
192
+ if (existingKey !== undefined) {
193
+ throw new UserError(`Parameters "${existingKey}" and "${key}" use conflicting MCP field "${field}".`);
194
+ }
195
+ sourceKeysByField.set(field, key);
196
+ const childSchema = unwrapOptional(rawChildSchema);
197
+ if (childSchema.kind === "object") {
198
+ validateUniqueMCPParameterFields(childSchema, casing);
199
+ }
200
+ }
201
+ }
184
202
  function enumerateTools(root, casing, allowlist, omitRootToolNamePrefix) {
185
203
  const tools = [];
204
+ const commandPathsByToolName = new Map();
186
205
  function visit(node, toolPath, commandPath) {
187
206
  if (node.kind === "command") {
188
207
  if (!node.scope.includes("mcp")) {
@@ -196,9 +215,16 @@ function enumerateTools(root, casing, allowlist, omitRootToolNamePrefix) {
196
215
  if (params === undefined || params.kind !== "object") {
197
216
  throw new ToolcraftBugError(`command "${name}" must define an object params schema for MCP.`);
198
217
  }
218
+ validateUniqueMCPParameterFields(params, casing);
219
+ const resolvedCommandPath = [...commandPath, node.name].join(".");
220
+ const existingPath = commandPathsByToolName.get(name);
221
+ if (existingPath !== undefined) {
222
+ throw new UserError(`MCP commands "${existingPath}" and "${resolvedCommandPath}" use conflicting tool name "${name}".`);
223
+ }
224
+ commandPathsByToolName.set(name, resolvedCommandPath);
199
225
  tools.push({
200
226
  command: node,
201
- commandPath: [...commandPath, node.name].join("."),
227
+ commandPath: resolvedCommandPath,
202
228
  name,
203
229
  description: buildToolDescription(node.description, params, casing),
204
230
  inputSchema: applySchemaCasing(toJsonSchema(params), casing)
@@ -360,7 +386,12 @@ function validateObjectSchema(schema, value, casing, label, errors) {
360
386
  const fieldLabel = label.length === 0 ? inputKey : `${label}.${inputKey}`;
361
387
  if (!hasValue) {
362
388
  if (childSchema.default !== undefined) {
363
- result[outputKey] = childSchema.default;
389
+ Object.defineProperty(result, outputKey, {
390
+ value: childSchema.default,
391
+ enumerable: true,
392
+ configurable: true,
393
+ writable: true
394
+ });
364
395
  continue;
365
396
  }
366
397
  if (isOptional(rawChildSchema)) {
@@ -369,7 +400,12 @@ function validateObjectSchema(schema, value, casing, label, errors) {
369
400
  errors.push({ path: fieldLabel, message: `Missing required parameter "${fieldLabel}".` });
370
401
  continue;
371
402
  }
372
- result[outputKey] = validateSchemaValue(rawChildSchema, value[inputKey], casing, fieldLabel, errors);
403
+ Object.defineProperty(result, outputKey, {
404
+ value: validateSchemaValue(rawChildSchema, value[inputKey], casing, fieldLabel, errors),
405
+ enumerable: true,
406
+ configurable: true,
407
+ writable: true
408
+ });
373
409
  }
374
410
  return result;
375
411
  }
@@ -430,7 +466,11 @@ function createResolvedMCPServer(root, options) {
430
466
  validateServices(services);
431
467
  const tools = enumerateTools(root, casing, options.tools, options.omitRootToolNamePrefix ?? false);
432
468
  const version = resolveMCPVersion(options.version);
433
- const server = createServer({ name: options.name, version });
469
+ const server = createServer({
470
+ name: options.name,
471
+ version,
472
+ validateToolArguments: false
473
+ });
434
474
  for (const tool of tools) {
435
475
  server.tool(tool.name, tool.description, tool.inputSchema, async (argumentsValue) => {
436
476
  let params;
@@ -516,7 +556,8 @@ function createDeferredMCPServer(root, options) {
516
556
  });
517
557
  }
518
558
  export function createMCPServer(roots, options) {
519
- const root = mergeApprovalsGroup(normalizeRoots(roots));
559
+ const normalizedRoot = normalizeRoots(roots);
560
+ const root = options.approvals === false ? normalizedRoot : mergeApprovalsGroup(normalizedRoot);
520
561
  if (!hasMcpProxyGroups(root)) {
521
562
  return createResolvedMCPServer(root, options);
522
563
  }
@@ -524,7 +565,8 @@ export function createMCPServer(roots, options) {
524
565
  }
525
566
  export async function runMCP(roots, options) {
526
567
  enableSourceMaps();
527
- const root = mergeApprovalsGroup(normalizeRoots(roots));
568
+ const normalizedRoot = normalizeRoots(roots);
569
+ const root = options.approvals === false ? normalizedRoot : mergeApprovalsGroup(normalizedRoot);
528
570
  await resolveMcpProxies(root, { projectRoot: options.projectRoot });
529
571
  const server = createResolvedMCPServer(root, options);
530
572
  await server.listen();
package/dist/renderer.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import YAML from "yaml";
2
+ import { renderDetailCard } from "@poe-code/design-system";
2
3
  function isObject(value) {
3
4
  return value !== null && typeof value === "object" && !Array.isArray(value);
4
5
  }
@@ -62,19 +63,117 @@ function stringifyJson(value, spaces) {
62
63
  return String(value);
63
64
  }
64
65
  }
66
+ function humanizeKey(key) {
67
+ let output = "";
68
+ let capitalizeNext = true;
69
+ for (const char of key) {
70
+ if (char === "_" || char === "-") {
71
+ output += " ";
72
+ capitalizeNext = false;
73
+ continue;
74
+ }
75
+ if (char >= "A" && char <= "Z" && output.length > 0 && !output.endsWith(" ")) {
76
+ output += " ";
77
+ }
78
+ if (capitalizeNext) {
79
+ output += char.toUpperCase();
80
+ capitalizeNext = false;
81
+ continue;
82
+ }
83
+ output += char;
84
+ }
85
+ return output;
86
+ }
87
+ function detailRows(result, depth = 0) {
88
+ const rows = [];
89
+ for (const [key, value] of Object.entries(result)) {
90
+ const label = `${" ".repeat(depth)}${humanizeKey(key)}`;
91
+ if (isObject(value)) {
92
+ if (Object.keys(value).length === 0) {
93
+ rows.push({ label, value: "{}" });
94
+ continue;
95
+ }
96
+ rows.push({ label, value: "" });
97
+ rows.push(...detailRows(value, depth + 1));
98
+ continue;
99
+ }
100
+ rows.push({ label, value: displayScalar(value) });
101
+ }
102
+ return rows;
103
+ }
104
+ function displayScalar(value) {
105
+ if (typeof value === "boolean") {
106
+ return value ? "Yes" : "No";
107
+ }
108
+ if (Array.isArray(value) && value.every((entry) => !isObject(entry) && !Array.isArray(entry))) {
109
+ return value.map((entry) => displayScalar(entry)).join(", ") || "—";
110
+ }
111
+ return stringifyValue(value) || "—";
112
+ }
113
+ function isUrl(value) {
114
+ return typeof value === "string" && (value.startsWith("https://") || value.startsWith("http://"));
115
+ }
116
+ function compactUrl(value) {
117
+ try {
118
+ const url = new URL(value);
119
+ const tail = url.pathname.split("/").filter(Boolean).at(-1);
120
+ return tail ? `${url.hostname}/…/${tail}` : url.hostname;
121
+ }
122
+ catch {
123
+ return value;
124
+ }
125
+ }
126
+ function displayRowValue(value) {
127
+ return isUrl(value) ? compactUrl(value) : displayScalar(value);
128
+ }
129
+ function directScalarRows(result) {
130
+ return Object.entries(result)
131
+ .filter(([, value]) => !isObject(value) && !Array.isArray(value))
132
+ .map(([key, value]) => ({ label: humanizeKey(key), value: displayRowValue(value) }));
133
+ }
134
+ function directObjectSections(result) {
135
+ return Object.entries(result)
136
+ .filter(([, value]) => isObject(value))
137
+ .map(([key, value]) => ({ title: humanizeKey(key), rows: detailRows(value) }))
138
+ .filter((section) => section.rows.length > 0);
139
+ }
140
+ function renderObjectCard(result, primitives, title) {
141
+ const scalarRows = directScalarRows(result);
142
+ const nestedSections = directObjectSections(result);
143
+ const listRows = Object.entries(result)
144
+ .filter(([, value]) => Array.isArray(value))
145
+ .map(([key, value]) => ({ label: humanizeKey(key), value: displayScalar(value) }));
146
+ return renderDetailCard({
147
+ theme: primitives.getTheme(),
148
+ title,
149
+ sections: [
150
+ { rows: scalarRows },
151
+ ...nestedSections,
152
+ { title: "Lists", rows: listRows }
153
+ ]
154
+ });
155
+ }
156
+ function richResultTitle(command) {
157
+ const description = command.description?.trim();
158
+ if (description && !description.includes("\n") && description.length <= 64) {
159
+ return description;
160
+ }
161
+ return command.name ? humanizeKey(command.name) : "Result";
162
+ }
65
163
  export function renderObjectTable(result, primitives) {
66
- const rows = Object.entries(result).map(([key, value]) => ({
67
- key,
68
- value: stringifyValue(value),
69
- }));
164
+ const rows = detailRows(result);
165
+ if (rows.length === 0) {
166
+ return "{}";
167
+ }
70
168
  return primitives.renderTable({
71
169
  theme: primitives.getTheme(),
170
+ variant: "detail",
72
171
  columns: [
73
172
  {
74
- name: "key",
75
- title: "Key",
173
+ name: "label",
174
+ title: "Label",
76
175
  alignment: "left",
77
- maxLen: Math.max("Key".length, ...rows.map((row) => row.key.length)),
176
+ maxLen: Math.max("Label".length, ...rows.map((row) => row.label.length)),
78
177
  },
79
178
  {
80
179
  name: "value",
@@ -111,9 +210,12 @@ export function renderArrayTable(result, primitives) {
111
210
  name,
112
211
  title: name,
113
212
  alignment: "left",
114
- maxLen: Math.max(name.length, ...result.map((row) => (name in row ? stringifyValue(row[name]).length : 0))),
213
+ maxLen: Math.max(name.length, ...result.map((row) => Object.prototype.hasOwnProperty.call(row, name) ? stringifyValue(row[name]).length : 0)),
115
214
  })),
116
- rows: result.map((row) => Object.fromEntries(columnNames.map((name) => [name, name in row ? stringifyValue(row[name]) : ""]))),
215
+ rows: result.map((row) => Object.fromEntries(columnNames.map((name) => [
216
+ name,
217
+ Object.prototype.hasOwnProperty.call(row, name) ? stringifyValue(row[name]) : ""
218
+ ]))),
117
219
  });
118
220
  }
119
221
  function renderArrayMarkdown(result) {
@@ -124,11 +226,13 @@ function renderArrayMarkdown(result) {
124
226
  const header = `| ${columnNames.join(" | ")} |`;
125
227
  const separator = `| ${columnNames.map(() => ":---").join(" | ")} |`;
126
228
  const rows = result.map((row) => `| ${columnNames
127
- .map((name) => (name in row ? stringifyValue(row[name]).replaceAll("|", "\\|") : ""))
229
+ .map((name) => Object.prototype.hasOwnProperty.call(row, name)
230
+ ? stringifyValue(row[name]).replaceAll("|", "\\|")
231
+ : "")
128
232
  .join(" | ")} |`);
129
233
  return [header, separator, ...rows].join("\n");
130
234
  }
131
- function autoRender(result, output, _primitives) {
235
+ function autoRender(command, result, output, primitives) {
132
236
  if (result === null || result === undefined) {
133
237
  if (output === "json") {
134
238
  return stringifyJson({ ok: true }, 2);
@@ -151,6 +255,7 @@ function autoRender(result, output, _primitives) {
151
255
  if (output === "json") {
152
256
  return stringifyJson(result, 2);
153
257
  }
258
+ return renderObjectCard(result, primitives, richResultTitle(command));
154
259
  }
155
260
  if (isArrayOfObjects(result)) {
156
261
  if (output === "md") {
@@ -159,6 +264,7 @@ function autoRender(result, output, _primitives) {
159
264
  if (output === "json") {
160
265
  return stringifyJson(result, 2);
161
266
  }
267
+ return renderArrayTable(result, primitives);
162
268
  }
163
269
  if (output === "rich") {
164
270
  return YAML.stringify(result);
@@ -175,7 +281,7 @@ export function renderResult(command, result, output, primitives, write = (chunk
175
281
  const unwrapped = unwrapMcpEnvelope(result);
176
282
  result = unwrapped.result;
177
283
  if (unwrapped.mcpError) {
178
- const payload = autoRender(result, output, primitives);
284
+ const payload = autoRender(command, result, output, primitives);
179
285
  if (payload.length > 0) {
180
286
  write(`${payload}\n`, "stderr");
181
287
  }
@@ -199,7 +305,7 @@ export function renderResult(command, result, output, primitives, write = (chunk
199
305
  command.render.rich(result, primitives);
200
306
  return { mcpError: false };
201
307
  }
202
- const payload = autoRender(result, output, primitives);
308
+ const payload = autoRender(command, result, output, primitives);
203
309
  if (payload.length > 0) {
204
310
  write(`${payload}\n`);
205
311
  }
@@ -89,6 +89,7 @@ const ignoredRoot = defineGroup({
89
89
  ],
90
90
  });
91
91
  const ignoredOptions = {
92
+ approvals: false,
92
93
  casing: "camel",
93
94
  services: {
94
95
  logger: console,
package/dist/sdk.d.ts CHANGED
@@ -61,6 +61,7 @@ type SDKNodeShape<TNode, TInheritedScope extends ScopeInput, TInheritedHumanInLo
61
61
  } : never : EmptyRecord;
62
62
  type SDKChildrenShape<TChildren, TInheritedScope extends ScopeInput, TInheritedHumanInLoopMode extends HumanInLoopMode | undefined> = Simplify<UnionToIntersection<SDKNodeShape<RawChildrenValue<TChildren>, TInheritedScope, TInheritedHumanInLoopMode>>>;
63
63
  export interface CreateSDKOptions<TServices extends object = Record<string, unknown>> {
64
+ approvals?: boolean;
64
65
  services?: TServices;
65
66
  casing?: "camel";
66
67
  humanInLoop?: HumanInLoopRuntimeOptions;
package/dist/sdk.js CHANGED
@@ -1,5 +1,5 @@
1
- import { access, readFile, writeFile } from "node:fs/promises";
2
- import { ToolcraftBugError, assertCommandRequirements, resolveCommandSecrets } from "./index.js";
1
+ import { access, lstat, readFile, rename, unlink, writeFile } from "node:fs/promises";
2
+ import { ToolcraftBugError, UserError, assertCommandRequirements, resolveCommandSecrets } from "./index.js";
3
3
  import { writeErrorReport } from "./error-report.js";
4
4
  import { mergeApprovalsGroup } from "./human-in-loop/approvals-commands.js";
5
5
  import { invokeWithHumanInLoop } from "./human-in-loop/index.js";
@@ -85,7 +85,10 @@ function createFs() {
85
85
  catch {
86
86
  return false;
87
87
  }
88
- }
88
+ },
89
+ lstat: async (path) => lstat(path),
90
+ rename: async (fromPath, toPath) => rename(fromPath, toPath),
91
+ unlink: async (path) => unlink(path)
89
92
  };
90
93
  }
91
94
  function createEnv(values = process.env) {
@@ -206,7 +209,12 @@ function validateObjectSchema(schema, value, label, errors) {
206
209
  const fieldLabel = label.length === 0 ? inputKey : `${label}.${inputKey}`;
207
210
  if (!hasValue) {
208
211
  if (childSchema.default !== undefined) {
209
- result[outputKey] = childSchema.default;
212
+ Object.defineProperty(result, outputKey, {
213
+ value: childSchema.default,
214
+ enumerable: true,
215
+ configurable: true,
216
+ writable: true
217
+ });
210
218
  continue;
211
219
  }
212
220
  if (isOptional(rawChildSchema)) {
@@ -215,10 +223,30 @@ function validateObjectSchema(schema, value, label, errors) {
215
223
  errors.push({ path: fieldLabel, message: `Missing required parameter "${fieldLabel}".` });
216
224
  continue;
217
225
  }
218
- result[outputKey] = validateSchemaValue(rawChildSchema, value[inputKey], fieldLabel, errors);
226
+ Object.defineProperty(result, outputKey, {
227
+ value: validateSchemaValue(rawChildSchema, value[inputKey], fieldLabel, errors),
228
+ enumerable: true,
229
+ configurable: true,
230
+ writable: true
231
+ });
219
232
  }
220
233
  return result;
221
234
  }
235
+ function validateUniqueSDKParameterMembers(schema) {
236
+ const sourceKeysByMember = new Map();
237
+ for (const [key, rawChildSchema] of Object.entries(schema.shape)) {
238
+ const member = formatSegment(key);
239
+ const existingKey = sourceKeysByMember.get(member);
240
+ if (existingKey !== undefined) {
241
+ throw new UserError(`Parameters "${existingKey}" and "${key}" use conflicting SDK member "${member}".`);
242
+ }
243
+ sourceKeysByMember.set(member, key);
244
+ const childSchema = unwrapOptional(rawChildSchema);
245
+ if (childSchema.kind === "object") {
246
+ validateUniqueSDKParameterMembers(childSchema);
247
+ }
248
+ }
249
+ }
222
250
  function validateSDKArguments(schema, argumentsValue) {
223
251
  const errors = [];
224
252
  const result = validateObjectSchema(schema, argumentsValue ?? {}, "", errors);
@@ -238,7 +266,7 @@ function defineMember(target, key, value) {
238
266
  }
239
267
  export function createSDK(root, options = {}) {
240
268
  enableSourceMaps();
241
- const mergedRoot = mergeApprovalsGroup(root);
269
+ const mergedRoot = options.approvals === false ? root : mergeApprovalsGroup(root);
242
270
  if (!hasMcpProxyGroups(mergedRoot)) {
243
271
  return createResolvedSDK(mergedRoot, options);
244
272
  }
@@ -251,6 +279,10 @@ function createResolvedSDK(root, options = {}) {
251
279
  validateServices(services);
252
280
  function build(node, path) {
253
281
  if (node.kind === "command") {
282
+ const sdkParamsSchema = filterSchemaForScope(node.params, "sdk");
283
+ if (sdkParamsSchema?.kind === "object") {
284
+ validateUniqueSDKParameterMembers(sdkParamsSchema);
285
+ }
254
286
  return async (params) => {
255
287
  const commandPath = [...path, node.name].join(".");
256
288
  let secrets;
@@ -298,19 +330,32 @@ function createResolvedSDK(root, options = {}) {
298
330
  };
299
331
  }
300
332
  const output = {};
333
+ const sourceNamesByMember = new Map();
301
334
  const nextPath = node === root ? path : [...path, node.name];
302
335
  for (const child of node.children) {
336
+ let childValue;
303
337
  if (child.kind === "command") {
304
338
  if (!child.scope.includes("sdk")) {
305
339
  continue;
306
340
  }
307
- defineMember(output, formatSegment(child.name), build(child, nextPath));
308
- continue;
341
+ childValue = build(child, nextPath);
342
+ }
343
+ else {
344
+ childValue = build(child, nextPath);
345
+ if (!isPlainObject(childValue) || Object.keys(childValue).length === 0) {
346
+ continue;
347
+ }
348
+ }
349
+ const member = formatSegment(child.name);
350
+ if (member === "then") {
351
+ throw new UserError(`SDK member "${child.name}" uses reserved member "then".`);
309
352
  }
310
- const childValue = build(child, nextPath);
311
- if (isPlainObject(childValue) && Object.keys(childValue).length > 0) {
312
- defineMember(output, formatSegment(child.name), childValue);
353
+ const existingName = sourceNamesByMember.get(member);
354
+ if (existingName !== undefined) {
355
+ throw new UserError(`SDK members "${existingName}" and "${child.name}" use conflicting member "${member}".`);
313
356
  }
357
+ sourceNamesByMember.set(member, child.name);
358
+ defineMember(output, member, childValue);
314
359
  }
315
360
  return output;
316
361
  }
@@ -1,3 +1,3 @@
1
1
  import type { AgentDefinition } from "./types.js";
2
- export declare const allAgents: AgentDefinition[];
2
+ export declare const allAgents: readonly AgentDefinition[];
3
3
  export declare function resolveAgentId(input: string): string | undefined;