caplets 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from "node:module";
3
3
  import minproc, { stdin, stdout, default as process$1 } from "node:process";
4
- import { execFileSync } from "node:child_process";
5
- import minpath, { basename, dirname, extname, isAbsolute, join, parse, posix, relative, resolve, win32 } from "node:path";
4
+ import { execFileSync, spawn } from "node:child_process";
5
+ import minpath, { basename, delimiter, dirname, extname, isAbsolute, join, parse, posix, relative, resolve, win32 } from "node:path";
6
6
  import { accessSync, chmodSync, constants, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, statSync, watch, writeFileSync } from "node:fs";
7
7
  import { createInterface } from "node:readline/promises";
8
8
  import { createServer } from "node:http";
@@ -180,7 +180,7 @@ const allowsEval = /* @__PURE__ */ cached(() => {
180
180
  return false;
181
181
  }
182
182
  });
183
- function isPlainObject$6(o) {
183
+ function isPlainObject$7(o) {
184
184
  if (isObject(o) === false) return false;
185
185
  const ctor = o.constructor;
186
186
  if (ctor === void 0) return true;
@@ -191,7 +191,7 @@ function isPlainObject$6(o) {
191
191
  return true;
192
192
  }
193
193
  function shallowClone(o) {
194
- if (isPlainObject$6(o)) return { ...o };
194
+ if (isPlainObject$7(o)) return { ...o };
195
195
  if (Array.isArray(o)) return [...o];
196
196
  if (o instanceof Map) return new Map(o);
197
197
  if (o instanceof Set) return new Set(o);
@@ -274,7 +274,7 @@ function omit(schema, mask) {
274
274
  }));
275
275
  }
276
276
  function extend(schema, shape) {
277
- if (!isPlainObject$6(shape)) throw new Error("Invalid input to extend: expected a plain object");
277
+ if (!isPlainObject$7(shape)) throw new Error("Invalid input to extend: expected a plain object");
278
278
  const checks = schema._zod.def.checks;
279
279
  if (checks && checks.length > 0) {
280
280
  const existingShape = schema._zod.def.shape;
@@ -290,7 +290,7 @@ function extend(schema, shape) {
290
290
  } }));
291
291
  }
292
292
  function safeExtend(schema, shape) {
293
- if (!isPlainObject$6(shape)) throw new Error("Invalid input to safeExtend: expected a plain object");
293
+ if (!isPlainObject$7(shape)) throw new Error("Invalid input to safeExtend: expected a plain object");
294
294
  return clone(schema, mergeDefs(schema._zod.def, { get shape() {
295
295
  const _shape = {
296
296
  ...schema._zod.def.shape,
@@ -1904,7 +1904,7 @@ function mergeValues$1(a, b) {
1904
1904
  valid: true,
1905
1905
  data: a
1906
1906
  };
1907
- if (isPlainObject$6(a) && isPlainObject$6(b)) {
1907
+ if (isPlainObject$7(a) && isPlainObject$7(b)) {
1908
1908
  const bKeys = Object.keys(b);
1909
1909
  const sharedKeys = Object.keys(a).filter((key) => bKeys.indexOf(key) !== -1);
1910
1910
  const newObj = {
@@ -1980,7 +1980,7 @@ const $ZodRecord = /* @__PURE__ */ $constructor("$ZodRecord", (inst, def) => {
1980
1980
  $ZodType.init(inst, def);
1981
1981
  inst._zod.parse = (payload, ctx) => {
1982
1982
  const input = payload.value;
1983
- if (!isPlainObject$6(input)) {
1983
+ if (!isPlainObject$7(input)) {
1984
1984
  payload.issues.push({
1985
1985
  expected: "record",
1986
1986
  code: "invalid_type",
@@ -9619,7 +9619,7 @@ const { program, createCommand, createArgument, createOption, CommanderError, In
9619
9619
  })))(), 1)).default;
9620
9620
  //#endregion
9621
9621
  //#region package.json
9622
- var version = "0.10.0";
9622
+ var version = "0.11.0";
9623
9623
  //#endregion
9624
9624
  //#region node_modules/.pnpm/pkce-challenge@5.0.1/node_modules/pkce-challenge/dist/index.node.js
9625
9625
  let crypto;
@@ -19049,6 +19049,34 @@ const capletHttpApiSchema = object$1({
19049
19049
  "headers"
19050
19050
  ]);
19051
19051
  });
19052
+ const capletCliToolOutputSchema = object$1({ type: _enum(["text", "json"]).optional() }).strict();
19053
+ const capletCliToolAnnotationsSchema = object$1({
19054
+ readOnlyHint: boolean().optional(),
19055
+ destructiveHint: boolean().optional(),
19056
+ idempotentHint: boolean().optional(),
19057
+ openWorldHint: boolean().optional()
19058
+ }).strict();
19059
+ const capletCliToolActionSchema = object$1({
19060
+ description: string().min(1).optional().describe("Action capability description."),
19061
+ inputSchema: record(string(), unknown()).optional().describe("JSON Schema for call_tool arguments."),
19062
+ outputSchema: record(string(), unknown()).optional().describe("JSON Schema for structuredContent returned by this action."),
19063
+ command: string().min(1).describe("Executable command to spawn without a shell."),
19064
+ args: array(string()).optional().describe("Arguments passed to the command."),
19065
+ env: record(string(), string()).optional().describe("Additional environment variables."),
19066
+ cwd: string().min(1).optional().describe("Working directory for this action."),
19067
+ timeoutMs: number$1().int().positive().optional(),
19068
+ maxOutputBytes: number$1().int().positive().optional(),
19069
+ output: capletCliToolOutputSchema.optional(),
19070
+ annotations: capletCliToolAnnotationsSchema.optional()
19071
+ }).strict();
19072
+ const capletCliToolsSchema = object$1({
19073
+ actions: record(string().regex(SERVER_ID_PATTERN), capletCliToolActionSchema).refine((actions) => Object.keys(actions).length > 0, "CLI tools backend must define at least one action").describe("Configured CLI actions keyed by stable tool name."),
19074
+ cwd: string().min(1).optional().describe("Default working directory for CLI actions."),
19075
+ env: record(string(), string()).optional().describe("Default environment variables."),
19076
+ timeoutMs: number$1().int().positive().optional(),
19077
+ maxOutputBytes: number$1().int().positive().optional(),
19078
+ disabled: boolean().optional().describe("When true, omit this Caplet from discovery.")
19079
+ }).strict();
19052
19080
  const capletFileSchema = object$1({
19053
19081
  $schema: string().url().optional().describe("Optional JSON Schema URL for editor validation."),
19054
19082
  name: string().trim().min(1).max(80).describe("Human-readable Caplet display name."),
@@ -19057,11 +19085,12 @@ const capletFileSchema = object$1({
19057
19085
  mcpServer: capletMcpServerSchema.describe("MCP server backend configuration for this Caplet.").optional(),
19058
19086
  openapiEndpoint: capletOpenApiEndpointSchema.describe("OpenAPI endpoint backend configuration for this Caplet.").optional(),
19059
19087
  graphqlEndpoint: capletGraphQlEndpointSchema.describe("GraphQL endpoint backend configuration for this Caplet.").optional(),
19060
- httpApi: capletHttpApiSchema.describe("HTTP API backend configuration for this Caplet.").optional()
19088
+ httpApi: capletHttpApiSchema.describe("HTTP API backend configuration for this Caplet.").optional(),
19089
+ cliTools: capletCliToolsSchema.describe("CLI tools backend configuration for this Caplet.").optional()
19061
19090
  }).strict().superRefine((frontmatter, ctx) => {
19062
- if (Number(Boolean(frontmatter.mcpServer)) + Number(Boolean(frontmatter.openapiEndpoint)) + Number(Boolean(frontmatter.graphqlEndpoint)) + Number(Boolean(frontmatter.httpApi)) !== 1) ctx.addIssue({
19091
+ if (Number(Boolean(frontmatter.mcpServer)) + Number(Boolean(frontmatter.openapiEndpoint)) + Number(Boolean(frontmatter.graphqlEndpoint)) + Number(Boolean(frontmatter.httpApi)) + Number(Boolean(frontmatter.cliTools)) !== 1) ctx.addIssue({
19063
19092
  code: "custom",
19064
- message: "Caplet file must define exactly one backend: mcpServer, openapiEndpoint, graphqlEndpoint, or httpApi"
19093
+ message: "Caplet file must define exactly one backend: mcpServer, openapiEndpoint, graphqlEndpoint, httpApi, or cliTools"
19065
19094
  });
19066
19095
  });
19067
19096
  function loadCapletFiles(root) {
@@ -19070,29 +19099,35 @@ function loadCapletFiles(root) {
19070
19099
  const openapiEndpoints = {};
19071
19100
  const graphqlEndpoints = {};
19072
19101
  const httpApis = {};
19102
+ const cliTools = {};
19073
19103
  for (const candidate of discoverCapletFiles(root)) {
19074
- if (servers[candidate.id] || openapiEndpoints[candidate.id] || graphqlEndpoints[candidate.id] || httpApis[candidate.id]) throw new CapletsError("CONFIG_INVALID", `Duplicate Caplet ID ${candidate.id} under ${root}`);
19104
+ if (servers[candidate.id] || openapiEndpoints[candidate.id] || graphqlEndpoints[candidate.id] || httpApis[candidate.id] || cliTools[candidate.id]) throw new CapletsError("CONFIG_INVALID", `Duplicate Caplet ID ${candidate.id} under ${root}`);
19075
19105
  const config = readCapletFile(candidate.path);
19076
- if (isPlainObject$5(config) && config.backend === "openapi") {
19106
+ if (isPlainObject$6(config) && config.backend === "openapi") {
19077
19107
  const { backend: _backend, ...endpoint } = config;
19078
19108
  openapiEndpoints[candidate.id] = endpoint;
19079
- } else if (isPlainObject$5(config) && config.backend === "graphql") {
19109
+ } else if (isPlainObject$6(config) && config.backend === "graphql") {
19080
19110
  const { backend: _backend, ...endpoint } = config;
19081
19111
  graphqlEndpoints[candidate.id] = endpoint;
19082
- } else if (isPlainObject$5(config) && config.backend === "http") {
19112
+ } else if (isPlainObject$6(config) && config.backend === "http") {
19083
19113
  const { backend: _backend, ...endpoint } = config;
19084
19114
  httpApis[candidate.id] = endpoint;
19115
+ } else if (isPlainObject$6(config) && config.backend === "cli") {
19116
+ const { backend: _backend, ...endpoint } = config;
19117
+ cliTools[candidate.id] = endpoint;
19085
19118
  } else servers[candidate.id] = config;
19086
19119
  }
19087
19120
  const hasServers = Object.keys(servers).length > 0;
19088
19121
  const hasOpenApi = Object.keys(openapiEndpoints).length > 0;
19089
19122
  const hasGraphQl = Object.keys(graphqlEndpoints).length > 0;
19090
19123
  const hasHttpApis = Object.keys(httpApis).length > 0;
19091
- return hasServers || hasOpenApi || hasGraphQl || hasHttpApis ? {
19124
+ const hasCliTools = Object.keys(cliTools).length > 0;
19125
+ return hasServers || hasOpenApi || hasGraphQl || hasHttpApis || hasCliTools ? {
19092
19126
  ...hasServers ? { mcpServers: servers } : {},
19093
19127
  ...hasOpenApi ? { openapiEndpoints } : {},
19094
19128
  ...hasGraphQl ? { graphqlEndpoints } : {},
19095
- ...hasHttpApis ? { httpApis } : {}
19129
+ ...hasHttpApis ? { httpApis } : {},
19130
+ ...hasCliTools ? { cliTools } : {}
19096
19131
  } : void 0;
19097
19132
  }
19098
19133
  function discoverCapletFiles(root) {
@@ -19158,6 +19193,16 @@ function capletToServerConfig(frontmatter, body, baseDir) {
19158
19193
  ...frontmatter.tags ? { tags: frontmatter.tags } : {},
19159
19194
  body
19160
19195
  };
19196
+ if (frontmatter.cliTools) return {
19197
+ ...frontmatter.cliTools,
19198
+ cwd: normalizeLocalPath$1(frontmatter.cliTools.cwd, baseDir),
19199
+ actions: normalizeCliToolActions(frontmatter.cliTools.actions, baseDir),
19200
+ backend: "cli",
19201
+ name: frontmatter.name,
19202
+ description: frontmatter.description,
19203
+ ...frontmatter.tags ? { tags: frontmatter.tags } : {},
19204
+ body
19205
+ };
19161
19206
  return {
19162
19207
  ...frontmatter.mcpServer,
19163
19208
  name: frontmatter.name,
@@ -19166,6 +19211,12 @@ function capletToServerConfig(frontmatter, body, baseDir) {
19166
19211
  body
19167
19212
  };
19168
19213
  }
19214
+ function normalizeCliToolActions(actions, baseDir) {
19215
+ return Object.fromEntries(Object.entries(actions).map(([name, action]) => [name, {
19216
+ ...action,
19217
+ cwd: normalizeLocalPath$1(action.cwd, baseDir)
19218
+ }]));
19219
+ }
19169
19220
  function normalizeGraphQlOperations(operations, baseDir) {
19170
19221
  if (!operations) return;
19171
19222
  return Object.fromEntries(Object.entries(operations).map(([name, operation]) => [name, {
@@ -19200,7 +19251,7 @@ function parseFrontmatter(text, path) {
19200
19251
  value: text
19201
19252
  });
19202
19253
  matter(file, { strip: true });
19203
- if (!isPlainObject$5(file.data.matter) || Object.keys(file.data.matter).length === 0) throw new Error("empty frontmatter");
19254
+ if (!isPlainObject$6(file.data.matter) || Object.keys(file.data.matter).length === 0) throw new Error("empty frontmatter");
19204
19255
  return {
19205
19256
  frontmatter: file.data.matter,
19206
19257
  body: String(file)
@@ -19209,7 +19260,7 @@ function parseFrontmatter(text, path) {
19209
19260
  throw new CapletsError("CONFIG_INVALID", `Caplet file at ${path} has invalid YAML frontmatter`, redactSecrets(error));
19210
19261
  }
19211
19262
  }
19212
- function isPlainObject$5(value) {
19263
+ function isPlainObject$6(value) {
19213
19264
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
19214
19265
  }
19215
19266
  function validateCapletId(id, path) {
@@ -19409,7 +19460,39 @@ const publicHttpApiSchema = object$1({
19409
19460
  disabled: boolean().default(false).describe("When true, omit this HTTP API Caplet.")
19410
19461
  }).strict();
19411
19462
  const normalizedHttpApiSchema = publicHttpApiSchema.extend({ body: string().optional() });
19412
- function configSchemaFor(serverValueSchema, openApiEndpointValueSchema, graphQlEndpointValueSchema, httpApiValueSchema) {
19463
+ const cliToolOutputSchema = object$1({ type: _enum(["text", "json"]).default("text").describe("How stdout should be represented in structuredContent.") }).strict();
19464
+ const cliToolAnnotationsSchema = object$1({
19465
+ readOnlyHint: boolean().optional(),
19466
+ destructiveHint: boolean().optional(),
19467
+ idempotentHint: boolean().optional(),
19468
+ openWorldHint: boolean().optional()
19469
+ }).strict();
19470
+ const cliToolActionSchema = object$1({
19471
+ description: string().min(1).optional().describe("Action capability description."),
19472
+ inputSchema: record(string(), unknown()).optional().describe("JSON Schema for call_tool arguments."),
19473
+ outputSchema: record(string(), unknown()).optional().describe("JSON Schema for structuredContent returned by this action."),
19474
+ command: string().min(1).describe("Executable command to spawn without a shell."),
19475
+ args: array(string()).optional().describe("Arguments passed to the command."),
19476
+ env: record(string(), string()).optional().describe("Additional environment variables for the command."),
19477
+ cwd: string().min(1).optional().describe("Working directory for this action."),
19478
+ timeoutMs: number$1().int().positive().optional().describe("Command timeout in milliseconds."),
19479
+ maxOutputBytes: number$1().int().positive().optional().describe("Maximum combined stdout and stderr bytes to keep."),
19480
+ output: cliToolOutputSchema.optional(),
19481
+ annotations: cliToolAnnotationsSchema.optional()
19482
+ }).strict();
19483
+ const publicCliToolsSchema = object$1({
19484
+ name: string().trim().min(1).max(80).describe("Human-readable CLI tools display name."),
19485
+ description: string().describe("Capability description shown to agents before CLI actions are disclosed.").refine((value) => value.trim().length >= 10, "description must contain at least 10 non-whitespace characters").refine((value) => value.length <= 1500, "description must be at most 1500 characters"),
19486
+ actions: record(string().regex(SERVER_ID_PATTERN), cliToolActionSchema).refine((actions) => Object.keys(actions).length > 0, "CLI tools backend must define at least one action").describe("Configured CLI actions keyed by stable tool name."),
19487
+ cwd: string().min(1).optional().describe("Default working directory for CLI actions."),
19488
+ env: record(string(), string()).optional().describe("Default environment variables for CLI actions."),
19489
+ tags: array(string().trim().min(1).max(80)).optional(),
19490
+ timeoutMs: number$1().int().positive().default(6e4).describe("Default timeout in milliseconds for CLI actions."),
19491
+ maxOutputBytes: number$1().int().positive().default(1e6).describe("Default maximum combined stdout and stderr bytes to keep."),
19492
+ disabled: boolean().default(false).describe("When true, omit this CLI tools Caplet.")
19493
+ }).strict();
19494
+ const normalizedCliToolsSchema = publicCliToolsSchema.extend({ body: string().optional() });
19495
+ function configSchemaFor(serverValueSchema, openApiEndpointValueSchema, graphQlEndpointValueSchema, httpApiValueSchema, cliToolsValueSchema) {
19413
19496
  return object$1({
19414
19497
  $schema: string().url().optional().describe("Optional JSON Schema URL for editor validation."),
19415
19498
  version: literal(1).default(1).describe("Caplets config schema version."),
@@ -19418,7 +19501,8 @@ function configSchemaFor(serverValueSchema, openApiEndpointValueSchema, graphQlE
19418
19501
  mcpServers: record(string().regex(SERVER_ID_PATTERN), serverValueSchema).default({}).describe("Downstream MCP servers keyed by stable server ID."),
19419
19502
  openapiEndpoints: record(string().regex(SERVER_ID_PATTERN), openApiEndpointValueSchema).default({}).describe("OpenAPI endpoints keyed by stable Caplet ID."),
19420
19503
  graphqlEndpoints: record(string().regex(SERVER_ID_PATTERN), graphQlEndpointValueSchema).default({}).describe("GraphQL endpoints keyed by stable Caplet ID."),
19421
- httpApis: record(string().regex(SERVER_ID_PATTERN), httpApiValueSchema).default({}).describe("HTTP APIs keyed by stable Caplet ID.")
19504
+ httpApis: record(string().regex(SERVER_ID_PATTERN), httpApiValueSchema).default({}).describe("HTTP APIs keyed by stable Caplet ID."),
19505
+ cliTools: record(string().regex(SERVER_ID_PATTERN), cliToolsValueSchema).default({}).describe("CLI tools keyed by stable Caplet ID.")
19422
19506
  }).strict().superRefine((config, ctx) => {
19423
19507
  if (config.defaultSearchLimit > config.maxSearchLimit) ctx.addIssue({
19424
19508
  code: "custom",
@@ -19607,10 +19691,34 @@ function configSchemaFor(serverValueSchema, openApiEndpointValueSchema, graphQlE
19607
19691
  "headers"
19608
19692
  ]);
19609
19693
  }
19694
+ for (const [server, rawValue] of Object.entries(config.cliTools)) {
19695
+ const raw = rawValue;
19696
+ const duplicateBackend = config.mcpServers[server] ? "mcpServers" : config.openapiEndpoints[server] ? "openapiEndpoints" : config.graphqlEndpoints[server] ? "graphqlEndpoints" : config.httpApis[server] ? "httpApis" : void 0;
19697
+ if (duplicateBackend) ctx.addIssue({
19698
+ code: "custom",
19699
+ path: ["cliTools", server],
19700
+ message: `Caplet ID ${server} is already used by ${duplicateBackend}`
19701
+ });
19702
+ if (!SERVER_ID_PATTERN.test(server)) ctx.addIssue({
19703
+ code: "custom",
19704
+ path: ["cliTools", server],
19705
+ message: "CLI tools ID must match ^[a-zA-Z0-9_-]{1,64}$"
19706
+ });
19707
+ for (const actionName of Object.keys(raw.actions)) if (!SERVER_ID_PATTERN.test(actionName)) ctx.addIssue({
19708
+ code: "custom",
19709
+ path: [
19710
+ "cliTools",
19711
+ server,
19712
+ "actions",
19713
+ actionName
19714
+ ],
19715
+ message: "CLI action ID must match ^[a-zA-Z0-9_-]{1,64}$"
19716
+ });
19717
+ }
19610
19718
  });
19611
19719
  }
19612
- const configFileSchema = configSchemaFor(publicServerSchema, publicOpenApiEndpointSchema, publicGraphQlEndpointSchema, publicHttpApiSchema);
19613
- const normalizedConfigFileSchema = configSchemaFor(normalizedServerSchema, normalizedOpenApiEndpointSchema, normalizedGraphQlEndpointSchema, normalizedHttpApiSchema);
19720
+ const configFileSchema = configSchemaFor(publicServerSchema, publicOpenApiEndpointSchema, publicGraphQlEndpointSchema, publicHttpApiSchema, publicCliToolsSchema);
19721
+ const normalizedConfigFileSchema = configSchemaFor(normalizedServerSchema, normalizedOpenApiEndpointSchema, normalizedGraphQlEndpointSchema, normalizedHttpApiSchema, normalizedCliToolsSchema);
19614
19722
  function loadConfig(path = resolveConfigPath(), projectPath = resolveProjectConfigPath()) {
19615
19723
  const hasUserConfig = existsSync(path);
19616
19724
  const hasProjectConfig = existsSync(projectPath);
@@ -19621,7 +19729,7 @@ function loadConfig(path = resolveConfigPath(), projectPath = resolveProjectConf
19621
19729
  if (!hasUserConfig && !hasProjectConfig && !userCaplets && !projectCaplets) throw new CapletsError("CONFIG_NOT_FOUND", `Caplets config not found at ${path} or ${projectPath}`);
19622
19730
  try {
19623
19731
  const config = parseConfig(mergeConfigInputs(userConfig, userCaplets, projectConfig, projectCaplets));
19624
- if (Object.keys(config.mcpServers).length === 0 && Object.keys(config.openapiEndpoints).length === 0 && Object.keys(config.graphqlEndpoints).length === 0 && Object.keys(config.httpApis).length === 0) throw new CapletsError("CONFIG_INVALID", "Caplets config must define at least one MCP server, OpenAPI endpoint, GraphQL endpoint, or HTTP API");
19732
+ if (Object.keys(config.mcpServers).length === 0 && Object.keys(config.openapiEndpoints).length === 0 && Object.keys(config.graphqlEndpoints).length === 0 && Object.keys(config.httpApis).length === 0 && Object.keys(config.cliTools).length === 0) throw new CapletsError("CONFIG_INVALID", "Caplets config must define at least one MCP server, OpenAPI endpoint, GraphQL endpoint, HTTP API, or CLI tools backend");
19625
19733
  return config;
19626
19734
  } catch (error) {
19627
19735
  if (error instanceof CapletsError) throw error;
@@ -19646,12 +19754,13 @@ function normalizeLocalPaths(input, baseDir) {
19646
19754
  return stripUndefined({
19647
19755
  ...input,
19648
19756
  openapiEndpoints: normalizeEndpointPaths(input.openapiEndpoints, baseDir, normalizeOpenApiPath),
19649
- graphqlEndpoints: normalizeEndpointPaths(input.graphqlEndpoints, baseDir, normalizeGraphQlPath)
19757
+ graphqlEndpoints: normalizeEndpointPaths(input.graphqlEndpoints, baseDir, normalizeGraphQlPath),
19758
+ cliTools: normalizeEndpointPaths(input.cliTools, baseDir, normalizeCliToolsPaths)
19650
19759
  });
19651
19760
  }
19652
19761
  function normalizeEndpointPaths(endpoints, baseDir, normalize) {
19653
19762
  if (!endpoints) return;
19654
- return Object.fromEntries(Object.entries(endpoints).map(([id, endpoint]) => [id, isPlainObject$4(endpoint) ? normalize(endpoint, baseDir) : endpoint]));
19763
+ return Object.fromEntries(Object.entries(endpoints).map(([id, endpoint]) => [id, isPlainObject$5(endpoint) ? normalize(endpoint, baseDir) : endpoint]));
19655
19764
  }
19656
19765
  function normalizeOpenApiPath(endpoint, baseDir) {
19657
19766
  return {
@@ -19660,7 +19769,7 @@ function normalizeOpenApiPath(endpoint, baseDir) {
19660
19769
  };
19661
19770
  }
19662
19771
  function normalizeGraphQlPath(endpoint, baseDir) {
19663
- const operations = isPlainObject$4(endpoint.operations) ? Object.fromEntries(Object.entries(endpoint.operations).map(([name, operation]) => [name, isPlainObject$4(operation) ? {
19772
+ const operations = isPlainObject$5(endpoint.operations) ? Object.fromEntries(Object.entries(endpoint.operations).map(([name, operation]) => [name, isPlainObject$5(operation) ? {
19664
19773
  ...operation,
19665
19774
  documentPath: normalizeLocalPath(operation.documentPath, baseDir)
19666
19775
  } : operation])) : endpoint.operations;
@@ -19670,6 +19779,17 @@ function normalizeGraphQlPath(endpoint, baseDir) {
19670
19779
  operations
19671
19780
  };
19672
19781
  }
19782
+ function normalizeCliToolsPaths(endpoint, baseDir) {
19783
+ const actions = isPlainObject$5(endpoint.actions) ? Object.fromEntries(Object.entries(endpoint.actions).map(([name, action]) => [name, isPlainObject$5(action) ? {
19784
+ ...action,
19785
+ cwd: normalizeLocalPath(action.cwd, baseDir)
19786
+ } : action])) : endpoint.actions;
19787
+ return {
19788
+ ...endpoint,
19789
+ cwd: normalizeLocalPath(endpoint.cwd, baseDir),
19790
+ actions
19791
+ };
19792
+ }
19673
19793
  function normalizeLocalPath(value, baseDir) {
19674
19794
  if (typeof value !== "string" || !value || isAbsolute(value) || hasEnvReference(value)) return value;
19675
19795
  return join(baseDir, value);
@@ -19678,6 +19798,7 @@ function rejectUntrustedProjectExecutableBackends(input, path) {
19678
19798
  if (input.openapiEndpoints && Object.keys(input.openapiEndpoints).length > 0) throw new CapletsError("CONFIG_INVALID", `Project config at ${path} cannot define openapiEndpoints; use trusted project Caplet files or user config`);
19679
19799
  if (input.graphqlEndpoints && Object.keys(input.graphqlEndpoints).length > 0) throw new CapletsError("CONFIG_INVALID", `Project config at ${path} cannot define graphqlEndpoints; use trusted project Caplet files or user config`);
19680
19800
  if (input.httpApis && Object.keys(input.httpApis).length > 0) throw new CapletsError("CONFIG_INVALID", `Project config at ${path} cannot define httpApis; use trusted project Caplet files or user config`);
19801
+ if (input.cliTools && Object.keys(input.cliTools).length > 0) throw new CapletsError("CONFIG_INVALID", `Project config at ${path} cannot define cliTools; use trusted project Caplet files or user config`);
19681
19802
  return input;
19682
19803
  }
19683
19804
  function mergeConfigInputs(...inputs) {
@@ -19702,6 +19823,10 @@ function mergeConfigInputs(...inputs) {
19702
19823
  httpApis: {
19703
19824
  ...merged?.httpApis,
19704
19825
  ...input.httpApis
19826
+ },
19827
+ cliTools: {
19828
+ ...merged?.cliTools,
19829
+ ...input.cliTools
19705
19830
  }
19706
19831
  };
19707
19832
  }
@@ -19738,6 +19863,12 @@ function parseConfig(input) {
19738
19863
  server,
19739
19864
  backend: "http"
19740
19865
  });
19866
+ const cliTools = {};
19867
+ for (const [server, raw] of Object.entries(parsed.data.cliTools)) cliTools[server] = stripUndefined({
19868
+ ...raw,
19869
+ server,
19870
+ backend: "cli"
19871
+ });
19741
19872
  return {
19742
19873
  version: parsed.data.version,
19743
19874
  options: {
@@ -19747,7 +19878,8 @@ function parseConfig(input) {
19747
19878
  mcpServers: servers,
19748
19879
  openapiEndpoints,
19749
19880
  graphqlEndpoints,
19750
- httpApis
19881
+ httpApis,
19882
+ cliTools
19751
19883
  };
19752
19884
  }
19753
19885
  function validateEndpointAuthHeaders(auth, ctx, path) {
@@ -19776,10 +19908,10 @@ function interpolateConfig(value, path = []) {
19776
19908
  return value;
19777
19909
  }
19778
19910
  function isPublicMetadataPath(path) {
19779
- if (path.length < 3 || path[0] !== "mcpServers" && path[0] !== "openapiEndpoints" && path[0] !== "graphqlEndpoints" && path[0] !== "httpApis") return false;
19911
+ if (path.length < 3 || path[0] !== "mcpServers" && path[0] !== "openapiEndpoints" && path[0] !== "graphqlEndpoints" && path[0] !== "httpApis" && path[0] !== "cliTools") return false;
19780
19912
  return NON_INTERPOLATED_SERVER_FIELDS.has(path[2] ?? "");
19781
19913
  }
19782
- function isPlainObject$4(value) {
19914
+ function isPlainObject$5(value) {
19783
19915
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
19784
19916
  }
19785
19917
  function hasEnvReference(value) {
@@ -19868,6 +20000,194 @@ async function maybeReadManualInput() {
19868
20000
  }
19869
20001
  }
19870
20002
  //#endregion
20003
+ //#region src/cli/author.ts
20004
+ function authorCliCaplet(id, options = {}) {
20005
+ const repo = resolve(options.repo ?? process.cwd());
20006
+ const include = options.include !== void 0 ? parseInclude(options.include) : options.command ? /* @__PURE__ */ new Set() : new Set([
20007
+ "git",
20008
+ "gh",
20009
+ "package"
20010
+ ]);
20011
+ const actions = {};
20012
+ if (include.has("git") || options.command === "git") Object.assign(actions, gitActions(repo));
20013
+ if (include.has("gh") || options.command === "gh") Object.assign(actions, ghActions(repo));
20014
+ if (include.has("package") || isPackageCommand(options.command)) Object.assign(actions, packageActions(repo, options.command));
20015
+ if (options.command && Object.keys(actions).length === 0) actions[`${sanitizeId(options.command)}_version`] = {
20016
+ description: `Print ${options.command} version information.`,
20017
+ command: options.command,
20018
+ args: ["--version"],
20019
+ annotations: { readOnlyHint: true }
20020
+ };
20021
+ if (Object.keys(actions).length === 0) throw new Error("No CLI actions could be generated for the requested options");
20022
+ const text = renderCaplet({
20023
+ name: titleize(id),
20024
+ description: `Curated CLI tools for ${basename(repo)} workflows.`,
20025
+ cwd: repo,
20026
+ actions
20027
+ });
20028
+ const output = options.output ?? "-";
20029
+ if (output !== "-") {
20030
+ writeFileSync(output, text);
20031
+ return {
20032
+ path: output,
20033
+ text
20034
+ };
20035
+ }
20036
+ return { text };
20037
+ }
20038
+ function parseInclude(value) {
20039
+ if (!value) return /* @__PURE__ */ new Set();
20040
+ return new Set(value.split(",").map((entry) => entry.trim()).filter(Boolean));
20041
+ }
20042
+ function gitActions(repo) {
20043
+ return {
20044
+ git_status: {
20045
+ description: "Show concise Git working tree status.",
20046
+ command: "git",
20047
+ args: ["status", "--short"],
20048
+ cwd: repo,
20049
+ annotations: { readOnlyHint: true }
20050
+ },
20051
+ git_current_branch: {
20052
+ description: "Print the current Git branch name.",
20053
+ command: "git",
20054
+ args: ["branch", "--show-current"],
20055
+ cwd: repo,
20056
+ annotations: { readOnlyHint: true }
20057
+ },
20058
+ git_changed_files: {
20059
+ description: "List changed tracked and untracked files.",
20060
+ command: "git",
20061
+ args: [
20062
+ "status",
20063
+ "--porcelain=v1",
20064
+ "--untracked-files=all"
20065
+ ],
20066
+ cwd: repo,
20067
+ annotations: { readOnlyHint: true }
20068
+ }
20069
+ };
20070
+ }
20071
+ function ghActions(repo) {
20072
+ return {
20073
+ gh_pr_status: {
20074
+ description: "Show pull request status for the current branch as JSON.",
20075
+ command: "gh",
20076
+ args: [
20077
+ "pr",
20078
+ "status",
20079
+ "--json",
20080
+ "currentBranch"
20081
+ ],
20082
+ cwd: repo,
20083
+ output: { type: "json" },
20084
+ annotations: {
20085
+ readOnlyHint: true,
20086
+ openWorldHint: true
20087
+ }
20088
+ },
20089
+ gh_issue_list: {
20090
+ description: "List open GitHub issues as JSON.",
20091
+ command: "gh",
20092
+ args: [
20093
+ "issue",
20094
+ "list",
20095
+ "--json",
20096
+ "number,title,state,url"
20097
+ ],
20098
+ cwd: repo,
20099
+ output: { type: "json" },
20100
+ annotations: {
20101
+ readOnlyHint: true,
20102
+ openWorldHint: true
20103
+ }
20104
+ }
20105
+ };
20106
+ }
20107
+ function packageActions(repo, explicitCommand) {
20108
+ const packageJsonPath = join(repo, "package.json");
20109
+ if (!existsSync(packageJsonPath)) return {};
20110
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
20111
+ const manager = explicitCommand && isPackageCommand(explicitCommand) ? explicitCommand : detectPackageManager(repo, packageJson);
20112
+ const scripts = packageJson.scripts ?? {};
20113
+ const actions = {};
20114
+ for (const script of [
20115
+ "test",
20116
+ "lint",
20117
+ "typecheck",
20118
+ "build",
20119
+ "verify"
20120
+ ]) {
20121
+ if (!scripts[script]) continue;
20122
+ actions[`package_${script}`] = {
20123
+ description: `Run the package ${script} script.`,
20124
+ command: manager,
20125
+ args: ["run", script],
20126
+ cwd: repo,
20127
+ annotations: { readOnlyHint: false },
20128
+ ...script === "test" || script === "verify" ? { timeoutMs: 12e4 } : {}
20129
+ };
20130
+ }
20131
+ return actions;
20132
+ }
20133
+ function detectPackageManager(repo, packageJson) {
20134
+ if (packageJson.packageManager?.startsWith("pnpm@")) return "pnpm";
20135
+ if (packageJson.packageManager?.startsWith("bun@")) return "bun";
20136
+ if (packageJson.packageManager?.startsWith("yarn@")) return "yarn";
20137
+ if (existsSync(join(repo, "pnpm-lock.yaml"))) return "pnpm";
20138
+ if (existsSync(join(repo, "bun.lockb")) || existsSync(join(repo, "bun.lock"))) return "bun";
20139
+ if (existsSync(join(repo, "yarn.lock"))) return "yarn";
20140
+ return "npm";
20141
+ }
20142
+ function isPackageCommand(command) {
20143
+ return command === "pnpm" || command === "npm" || command === "bun" || command === "yarn";
20144
+ }
20145
+ function renderCaplet(input) {
20146
+ const lines = [
20147
+ "---",
20148
+ "$schema: https://raw.githubusercontent.com/spiritledsoftware/caplets/main/schemas/caplet.schema.json",
20149
+ `name: ${yamlString(input.name)}`,
20150
+ `description: ${yamlString(input.description)}`,
20151
+ "tags:",
20152
+ " - cli",
20153
+ " - code",
20154
+ "cliTools:",
20155
+ ` cwd: ${yamlString(input.cwd)}`,
20156
+ " actions:"
20157
+ ];
20158
+ for (const [name, action] of Object.entries(input.actions).sort(([left], [right]) => left.localeCompare(right))) {
20159
+ lines.push(` ${name}:`);
20160
+ if (action.description) lines.push(` description: ${yamlString(action.description)}`);
20161
+ lines.push(` command: ${yamlString(action.command)}`);
20162
+ if (action.args?.length) {
20163
+ lines.push(" args:");
20164
+ for (const arg of action.args) lines.push(` - ${yamlString(arg)}`);
20165
+ }
20166
+ if (action.cwd && action.cwd !== input.cwd) lines.push(` cwd: ${yamlString(action.cwd)}`);
20167
+ if (action.timeoutMs) lines.push(` timeoutMs: ${action.timeoutMs}`);
20168
+ if (action.maxOutputBytes) lines.push(` maxOutputBytes: ${action.maxOutputBytes}`);
20169
+ if (action.output) {
20170
+ lines.push(" output:");
20171
+ lines.push(` type: ${action.output.type}`);
20172
+ }
20173
+ if (action.annotations) {
20174
+ lines.push(" annotations:");
20175
+ for (const [key, value] of Object.entries(action.annotations)) lines.push(` ${key}: ${value ? "true" : "false"}`);
20176
+ }
20177
+ }
20178
+ lines.push("---", "", `# ${input.name}`, "", input.description, "");
20179
+ return `${lines.join("\n")}`;
20180
+ }
20181
+ function yamlString(value) {
20182
+ return JSON.stringify(value);
20183
+ }
20184
+ function titleize(id) {
20185
+ return id.split(/[-_]/).filter(Boolean).map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`).join(" ");
20186
+ }
20187
+ function sanitizeId(value) {
20188
+ return value.replace(/[^a-zA-Z0-9_-]+/g, "_").slice(0, 48) || "cli";
20189
+ }
20190
+ //#endregion
19871
20191
  //#region src/cli/init.ts
19872
20192
  function initConfig(options = {}) {
19873
20193
  const path = resolveConfigPath(options.path);
@@ -19919,7 +20239,9 @@ function allCaplets$1(config) {
19919
20239
  return [
19920
20240
  ...Object.values(config.mcpServers),
19921
20241
  ...Object.values(config.openapiEndpoints),
19922
- ...Object.values(config.graphqlEndpoints)
20242
+ ...Object.values(config.graphqlEndpoints),
20243
+ ...Object.values(config.httpApis),
20244
+ ...Object.values(config.cliTools)
19923
20245
  ];
19924
20246
  }
19925
20247
  function formatCapletList(rows) {
@@ -20159,6 +20481,14 @@ function createProgram(io = {}) {
20159
20481
  });
20160
20482
  for (const caplet of result.installed) writeOut(`Installed ${caplet.id} to ${caplet.destination}\n`);
20161
20483
  });
20484
+ program.command("author").description("Generate reviewable Caplet files.").command("cli").description("Generate a CLI tools Caplet.").argument("<id>", "Caplet ID/display seed").option("--repo <path>", "repository path to inspect").option("--include <items>", "comma-separated generators to include: git,gh,package").option("--command <name>", "single CLI command template to generate").option("--output <path>", "output path, or - for stdout", "-").action((id, options) => {
20485
+ const result = authorCliCaplet(id, options);
20486
+ if (result.path) {
20487
+ writeOut(`Wrote CLI Caplet to ${result.path}\n`);
20488
+ return;
20489
+ }
20490
+ writeOut(result.text);
20491
+ });
20162
20492
  const config = program.command("config").description("Inspect Caplets config locations.");
20163
20493
  config.command("path").description("Print the effective user config path.").action(() => {
20164
20494
  writeOut(`${resolveConfigPath(envConfigPath())}\n`);
@@ -25865,7 +26195,7 @@ var Protocol = class {
25865
26195
  };
25866
26196
  }
25867
26197
  };
25868
- function isPlainObject$3(value) {
26198
+ function isPlainObject$4(value) {
25869
26199
  return value !== null && typeof value === "object" && !Array.isArray(value);
25870
26200
  }
25871
26201
  function mergeCapabilities(base, additional) {
@@ -25875,7 +26205,7 @@ function mergeCapabilities(base, additional) {
25875
26205
  const addValue = additional[k];
25876
26206
  if (addValue === void 0) continue;
25877
26207
  const baseValue = result[k];
25878
- if (isPlainObject$3(baseValue) && isPlainObject$3(addValue)) result[k] = {
26208
+ if (isPlainObject$4(baseValue) && isPlainObject$4(addValue)) result[k] = {
25879
26209
  ...baseValue,
25880
26210
  ...addValue
25881
26211
  };
@@ -33526,6 +33856,273 @@ const EMPTY_COMPLETION_RESULT = { completion: {
33526
33856
  hasMore: false
33527
33857
  } };
33528
33858
  //#endregion
33859
+ //#region src/cli-tools.ts
33860
+ const DEFAULT_INPUT_SCHEMA$1 = {
33861
+ type: "object",
33862
+ additionalProperties: true
33863
+ };
33864
+ var CliToolsManager = class {
33865
+ registry;
33866
+ constructor(registry) {
33867
+ this.registry = registry;
33868
+ }
33869
+ updateRegistry(registry) {
33870
+ this.registry = registry;
33871
+ }
33872
+ invalidate(_serverId) {}
33873
+ async checkTools(config) {
33874
+ const startedAt = Date.now();
33875
+ try {
33876
+ for (const action of actionsFor(config)) {
33877
+ const cwdTemplate = action.cwd ?? config.cwd;
33878
+ if (cwdTemplate && !cwdTemplate.includes("$input")) {
33879
+ if (!existsSync(interpolateRequiredString(cwdTemplate, {}, "cwd"))) throw new CapletsError("CONFIG_INVALID", `CLI cwd does not exist for ${config.server}/${action.name}`);
33880
+ }
33881
+ if (!action.command.includes("$input")) resolveCommandPath(action.command);
33882
+ }
33883
+ this.registry.setStatus(config.server, "available");
33884
+ return {
33885
+ server: config.server,
33886
+ status: "available",
33887
+ toolCount: Object.keys(config.actions).length,
33888
+ elapsedMs: Date.now() - startedAt
33889
+ };
33890
+ } catch (error) {
33891
+ const safe = toSafeError(error, "SERVER_UNAVAILABLE");
33892
+ this.registry.setStatus(config.server, "unavailable", safe);
33893
+ return {
33894
+ server: config.server,
33895
+ status: "unavailable",
33896
+ elapsedMs: Date.now() - startedAt,
33897
+ error: safe
33898
+ };
33899
+ }
33900
+ }
33901
+ async listTools(config) {
33902
+ return actionsFor(config).map((action) => this.toTool(action));
33903
+ }
33904
+ async getTool(config, toolName) {
33905
+ return this.toTool(getAction(config, toolName));
33906
+ }
33907
+ async callTool(config, toolName, args) {
33908
+ const action = getAction(config, toolName);
33909
+ validateInput(action, args);
33910
+ const execution = resolveExecution(config, action, args);
33911
+ const startedAt = Date.now();
33912
+ const controller = new AbortController();
33913
+ const timeout = setTimeout(() => controller.abort(), execution.timeoutMs);
33914
+ try {
33915
+ const result = await spawnCommand(execution, controller.signal, () => Date.now() - startedAt);
33916
+ const structured = parseStructuredResult(action, result, result.exitCode !== 0);
33917
+ return {
33918
+ content: [{
33919
+ type: "text",
33920
+ text: JSON.stringify(structured, null, 2)
33921
+ }],
33922
+ structuredContent: structured,
33923
+ isError: result.exitCode !== 0
33924
+ };
33925
+ } catch (error) {
33926
+ if (isAbortError$1(error)) throw new CapletsError("TOOL_CALL_TIMEOUT", `CLI tool timed out for ${config.server}/${toolName}`);
33927
+ if (error instanceof CapletsError) throw error;
33928
+ throw new CapletsError("DOWNSTREAM_TOOL_ERROR", `CLI tool failed for ${config.server}/${toolName}`, toSafeError(error));
33929
+ } finally {
33930
+ clearTimeout(timeout);
33931
+ }
33932
+ }
33933
+ compact(config, tool) {
33934
+ return {
33935
+ server: config.server,
33936
+ tool: tool.name,
33937
+ ...tool.description ? { description: tool.description } : {},
33938
+ ...tool.annotations ? { annotations: tool.annotations } : {},
33939
+ hasInputSchema: Boolean(tool.inputSchema),
33940
+ hasOutputSchema: Boolean(tool.outputSchema)
33941
+ };
33942
+ }
33943
+ search(config, tools, query, limit) {
33944
+ const needle = query.toLocaleLowerCase();
33945
+ return tools.filter((tool) => `${tool.name}\n${tool.description ?? ""}`.toLocaleLowerCase().includes(needle)).sort((left, right) => left.name.localeCompare(right.name)).slice(0, limit).map((tool) => this.compact(config, tool));
33946
+ }
33947
+ toTool(action) {
33948
+ return {
33949
+ name: action.name,
33950
+ ...action.description ? { description: action.description } : {},
33951
+ inputSchema: action.inputSchema ?? DEFAULT_INPUT_SCHEMA$1,
33952
+ ...action.outputSchema ? { outputSchema: action.outputSchema } : {},
33953
+ ...action.annotations ? { annotations: action.annotations } : {}
33954
+ };
33955
+ }
33956
+ };
33957
+ function actionsFor(config) {
33958
+ return Object.entries(config.actions).map(([name, action]) => ({
33959
+ name,
33960
+ ...action
33961
+ })).sort((left, right) => left.name.localeCompare(right.name));
33962
+ }
33963
+ function getAction(config, toolName) {
33964
+ const actions = actionsFor(config);
33965
+ const action = actions.find((candidate) => candidate.name === toolName);
33966
+ if (!action) throw new CapletsError("TOOL_NOT_FOUND", `Tool ${toolName} was not found on ${config.server}`, {
33967
+ server: config.server,
33968
+ tool: toolName,
33969
+ suggestions: actions.map((candidate) => candidate.name).filter((name) => name.toLocaleLowerCase().includes(toolName.toLocaleLowerCase()[0] ?? "")).slice(0, 5)
33970
+ });
33971
+ return action;
33972
+ }
33973
+ function resolveExecution(config, action, input) {
33974
+ const cwd = interpolateString(action.cwd ?? config.cwd, input, "cwd");
33975
+ if (cwd && !existsSync(cwd)) throw new CapletsError("CONFIG_INVALID", `CLI cwd does not exist for ${config.server}/${action.name}`);
33976
+ const env = {
33977
+ ...process.env,
33978
+ ...resolveEnv(config.env, input),
33979
+ ...resolveEnv(action.env, input)
33980
+ };
33981
+ return {
33982
+ command: interpolateString(action.command, input, "command") ?? action.command,
33983
+ args: (action.args ?? []).map((arg, index) => interpolateRequiredString(arg, input, `args.${index}`)),
33984
+ ...cwd ? { cwd } : {},
33985
+ env,
33986
+ timeoutMs: action.timeoutMs ?? config.timeoutMs,
33987
+ maxOutputBytes: action.maxOutputBytes ?? config.maxOutputBytes
33988
+ };
33989
+ }
33990
+ function resolveEnv(env, input) {
33991
+ if (!env) return {};
33992
+ return Object.fromEntries(Object.entries(env).map(([key, value]) => [key, interpolateRequiredString(value, input, `env.${key}`)]));
33993
+ }
33994
+ function interpolateString(value, input, field) {
33995
+ return value === void 0 ? void 0 : interpolateRequiredString(value, input, field);
33996
+ }
33997
+ function interpolateRequiredString(value, input, field) {
33998
+ return value.replace(/\$input(?:\.([A-Za-z0-9_.-]+))?/g, (_match, path) => {
33999
+ if (!path) throw new CapletsError("REQUEST_INVALID", `CLI ${field} cannot interpolate $input directly`);
34000
+ const selected = valueAtPath$1(input, path);
34001
+ if (selected === void 0 || selected === null) throw new CapletsError("REQUEST_INVALID", `CLI ${field} references missing input ${path}`);
34002
+ if (typeof selected !== "string" && typeof selected !== "number" && typeof selected !== "boolean") throw new CapletsError("REQUEST_INVALID", `CLI ${field} input ${path} must be a string, number, or boolean`);
34003
+ return String(selected);
34004
+ });
34005
+ }
34006
+ function valueAtPath$1(input, path) {
34007
+ let current = input;
34008
+ for (const segment of path.split(".")) {
34009
+ if (!current || typeof current !== "object" || Array.isArray(current)) return;
34010
+ current = current[segment];
34011
+ }
34012
+ return current;
34013
+ }
34014
+ function validateInput(action, input) {
34015
+ const schema = action.inputSchema;
34016
+ if (!schema) return;
34017
+ const required = Array.isArray(schema.required) ? schema.required : [];
34018
+ for (const key of required) if (typeof key === "string" && (input[key] === void 0 || input[key] === null)) throw new CapletsError("REQUEST_INVALID", `CLI tool ${action.name} requires input ${key}`);
34019
+ const properties = isPlainObject$3(schema.properties) ? schema.properties : {};
34020
+ for (const [key, property] of Object.entries(properties)) {
34021
+ if (input[key] === void 0 || !isPlainObject$3(property) || typeof property.type !== "string") continue;
34022
+ if (!matchesJsonType(input[key], property.type)) throw new CapletsError("REQUEST_INVALID", `CLI tool ${action.name} input ${key} must be ${property.type}`);
34023
+ }
34024
+ }
34025
+ function matchesJsonType(value, type) {
34026
+ switch (type) {
34027
+ case "string": return typeof value === "string";
34028
+ case "number":
34029
+ case "integer": return typeof value === "number" && (type === "number" || Number.isInteger(value));
34030
+ case "boolean": return typeof value === "boolean";
34031
+ case "object": return isPlainObject$3(value);
34032
+ case "array": return Array.isArray(value);
34033
+ case "null": return value === null;
34034
+ default: return true;
34035
+ }
34036
+ }
34037
+ function spawnCommand(execution, signal, elapsedMs) {
34038
+ return new Promise((resolve, reject) => {
34039
+ let stdout = "";
34040
+ let stderr = "";
34041
+ let outputBytes = 0;
34042
+ const child = spawn(execution.command, execution.args, {
34043
+ cwd: execution.cwd,
34044
+ env: execution.env,
34045
+ shell: false,
34046
+ signal,
34047
+ windowsHide: true
34048
+ });
34049
+ child.on("error", reject);
34050
+ const append = (stream, chunk) => {
34051
+ outputBytes += chunk.byteLength;
34052
+ if (outputBytes > execution.maxOutputBytes) {
34053
+ child.kill();
34054
+ reject(new CapletsError("DOWNSTREAM_TOOL_ERROR", "CLI tool output exceeded byte limit"));
34055
+ return;
34056
+ }
34057
+ if (stream === "stdout") stdout += chunk.toString("utf8");
34058
+ else stderr += chunk.toString("utf8");
34059
+ };
34060
+ child.stdout?.on("data", (chunk) => append("stdout", chunk));
34061
+ child.stderr?.on("data", (chunk) => append("stderr", chunk));
34062
+ child.on("close", (exitCode, childSignal) => {
34063
+ resolve({
34064
+ exitCode,
34065
+ signal: childSignal,
34066
+ stdout,
34067
+ stderr,
34068
+ elapsedMs: elapsedMs()
34069
+ });
34070
+ });
34071
+ });
34072
+ }
34073
+ function parseStructuredResult(action, result, tolerateInvalidJson = false) {
34074
+ const structured = {
34075
+ exitCode: result.exitCode,
34076
+ stdout: result.stdout,
34077
+ stderr: result.stderr,
34078
+ elapsedMs: result.elapsedMs,
34079
+ ...result.signal ? { signal: result.signal } : {}
34080
+ };
34081
+ if (action.output?.type === "json" && result.stdout.trim()) try {
34082
+ structured.json = JSON.parse(result.stdout);
34083
+ } catch (error) {
34084
+ if (tolerateInvalidJson) {
34085
+ structured.jsonParseError = toSafeError(error);
34086
+ return structured;
34087
+ }
34088
+ throw new CapletsError("DOWNSTREAM_PROTOCOL_ERROR", `CLI tool ${action.name} stdout was not valid JSON`, toSafeError(error));
34089
+ }
34090
+ return structured;
34091
+ }
34092
+ function resolveCommandPath(command) {
34093
+ if (isAbsolute(command) || /[\\/]/.test(command)) {
34094
+ assertExecutable(command);
34095
+ return command;
34096
+ }
34097
+ for (const directory of (process.env.PATH ?? "").split(delimiter)) {
34098
+ if (!directory) continue;
34099
+ const candidate = join(directory, command);
34100
+ if (isExecutable(candidate)) return candidate;
34101
+ if (process.platform === "win32") for (const ext of (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";")) {
34102
+ const windowsCandidate = join(directory, `${command}${ext.toLowerCase()}`);
34103
+ if (isExecutable(windowsCandidate)) return windowsCandidate;
34104
+ }
34105
+ }
34106
+ throw new CapletsError("SERVER_UNAVAILABLE", `CLI command ${command} was not found on PATH`);
34107
+ }
34108
+ function assertExecutable(path) {
34109
+ if (!isExecutable(path)) throw new CapletsError("SERVER_UNAVAILABLE", `CLI command ${path} is not executable`);
34110
+ }
34111
+ function isExecutable(path) {
34112
+ try {
34113
+ accessSync(path, constants.X_OK);
34114
+ return true;
34115
+ } catch {
34116
+ return false;
34117
+ }
34118
+ }
34119
+ function isAbortError$1(error) {
34120
+ return error instanceof Error && error.name === "AbortError";
34121
+ }
34122
+ function isPlainObject$3(value) {
34123
+ return value !== null && typeof value === "object" && !Array.isArray(value);
34124
+ }
34125
+ //#endregion
33529
34126
  //#region node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_zod@4.4.3/node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/client.js
33530
34127
  /**
33531
34128
  * Experimental client task features for MCP SDK.
@@ -61265,7 +61862,7 @@ function openApiCacheKey(endpoint) {
61265
61862
  //#endregion
61266
61863
  //#region src/capability-description.mjs
61267
61864
  function capabilityDescription(server) {
61268
- const backendName = server.backend === "mcp" ? "MCP server" : server.backend === "openapi" ? "OpenAPI endpoint" : server.backend === "graphql" ? "GraphQL endpoint" : "HTTP API";
61865
+ const backendName = server.backend === "mcp" ? "MCP server" : server.backend === "openapi" ? "OpenAPI endpoint" : server.backend === "graphql" ? "GraphQL endpoint" : server.backend === "http" ? "HTTP API" : "CLI tools";
61269
61866
  const checkOperation = server.backend === "mcp" ? "check_mcp_server" : "check_backend";
61270
61867
  const hint = [
61271
61868
  `Use this Caplet to inspect and call tools from its ${backendName} backend.`,
@@ -61295,7 +61892,7 @@ var ServerRegistry = class {
61295
61892
  return this.allCaplets().filter((server) => !server.disabled);
61296
61893
  }
61297
61894
  get(serverId) {
61298
- const server = this.config.mcpServers[serverId] ?? this.config.openapiEndpoints[serverId] ?? this.config.graphqlEndpoints[serverId] ?? this.config.httpApis[serverId];
61895
+ const server = this.config.mcpServers[serverId] ?? this.config.openapiEndpoints[serverId] ?? this.config.graphqlEndpoints[serverId] ?? this.config.httpApis[serverId] ?? this.config.cliTools[serverId];
61299
61896
  return server?.disabled ? void 0 : server;
61300
61897
  }
61301
61898
  require(serverId) {
@@ -61346,7 +61943,8 @@ var ServerRegistry = class {
61346
61943
  ...Object.values(this.config.mcpServers),
61347
61944
  ...Object.values(this.config.openapiEndpoints),
61348
61945
  ...Object.values(this.config.graphqlEndpoints),
61349
- ...Object.values(this.config.httpApis)
61946
+ ...Object.values(this.config.httpApis),
61947
+ ...Object.values(this.config.cliTools)
61350
61948
  ];
61351
61949
  }
61352
61950
  };
@@ -61372,6 +61970,13 @@ function backendDetail(server) {
61372
61970
  requestTimeoutMs: server.requestTimeoutMs,
61373
61971
  configuredActions: Object.keys(server.actions).length
61374
61972
  };
61973
+ if (server.backend === "cli") return {
61974
+ type: "cli",
61975
+ disabled: server.disabled,
61976
+ timeoutMs: server.timeoutMs,
61977
+ maxOutputBytes: server.maxOutputBytes,
61978
+ configuredActions: Object.keys(server.actions).length
61979
+ };
61375
61980
  return {
61376
61981
  type: "mcp",
61377
61982
  transport: server.transport,
@@ -61503,7 +62108,7 @@ const operations = [
61503
62108
  const generatedToolInputDescriptions = {
61504
62109
  operation: [
61505
62110
  "Caplets wrapper operation to perform for this configured Caplet backend.",
61506
- "Use get_caplet to read the full Caplet card, check_backend to check any backend, check_mcp_server to check an MCP backend, list_tools or search_tools to discover downstream tools, get_tool to read a downstream input schema, and call_tool to run one downstream tool or OpenAPI operation.",
62111
+ "Use get_caplet to read the full Caplet card, check_backend to check any backend, check_mcp_server to check an MCP backend, list_tools or search_tools to discover downstream tools, get_tool to read a downstream input schema, and call_tool to run one downstream tool, operation, action, or CLI command.",
61507
62112
  "For call_tool, pass downstream inputs only inside the top-level \"arguments\" object."
61508
62113
  ].join(" "),
61509
62114
  query: "Required only for search_tools. Example: {\"operation\":\"search_tools\",\"query\":\"web search\",\"limit\":5}. Do not use query for call_tool; put downstream query values under arguments.query.",
@@ -61520,16 +62125,16 @@ const generatedToolInputSchema = object$1({
61520
62125
  arguments: record(string(), unknown()).optional().describe(generatedToolInputDescriptions.arguments),
61521
62126
  fields: array(string().min(1)).min(1).optional().describe(generatedToolInputDescriptions.fields)
61522
62127
  }).strict();
61523
- async function handleServerTool(server, request, registry, downstream, openapi, graphql, http) {
62128
+ async function handleServerTool(server, request, registry, downstream, openapi, graphql, http, cli) {
61524
62129
  const parsed = validateOperationRequest(request, registry.config.options.maxSearchLimit);
61525
62130
  switch (parsed.operation) {
61526
62131
  case "get_caplet": return jsonResult(registry.detail(server));
61527
- case "check_backend": return jsonResult(await backendFor(server, downstream, openapi, graphql, http).check(server));
62132
+ case "check_backend": return jsonResult(await backendFor(server, downstream, openapi, graphql, http, cli).check(server));
61528
62133
  case "check_mcp_server":
61529
62134
  if (server.backend !== "mcp") throw new CapletsError("REQUEST_INVALID", "check_mcp_server is only valid for MCP-backed Caplets; use check_backend");
61530
62135
  return jsonResult(await downstream.checkServer(server));
61531
62136
  case "list_tools": {
61532
- const backend = backendFor(server, downstream, openapi, graphql, http);
62137
+ const backend = backendFor(server, downstream, openapi, graphql, http, cli);
61533
62138
  const tools = await backend.listTools(server);
61534
62139
  return jsonResult({
61535
62140
  server: server.server,
@@ -61537,7 +62142,7 @@ async function handleServerTool(server, request, registry, downstream, openapi,
61537
62142
  });
61538
62143
  }
61539
62144
  case "search_tools": {
61540
- const backend = backendFor(server, downstream, openapi, graphql, http);
62145
+ const backend = backendFor(server, downstream, openapi, graphql, http, cli);
61541
62146
  const tools = await backend.listTools(server);
61542
62147
  const limit = parsed.limit ?? registry.config.options.defaultSearchLimit;
61543
62148
  return jsonResult({
@@ -61547,14 +62152,14 @@ async function handleServerTool(server, request, registry, downstream, openapi,
61547
62152
  });
61548
62153
  }
61549
62154
  case "get_tool": {
61550
- const tool = await backendFor(server, downstream, openapi, graphql, http).getTool(server, parsed.tool);
62155
+ const tool = await backendFor(server, downstream, openapi, graphql, http, cli).getTool(server, parsed.tool);
61551
62156
  return jsonResult({
61552
62157
  server: server.server,
61553
62158
  tool
61554
62159
  });
61555
62160
  }
61556
62161
  case "call_tool": {
61557
- const backend = backendFor(server, downstream, openapi, graphql, http);
62162
+ const backend = backendFor(server, downstream, openapi, graphql, http, cli);
61558
62163
  if (parsed.fields === void 0) return backend.callTool(server, parsed.tool, parsed.arguments);
61559
62164
  if (server.backend === "graphql") throw new CapletsError("REQUEST_INVALID", "call_tool.fields is not supported for GraphQL-backed Caplets; select fields in the GraphQL operation document instead");
61560
62165
  const tool = await backend.getTool(server, parsed.tool);
@@ -61647,7 +62252,7 @@ function projectCallToolResult(result, outputSchema, fields) {
61647
62252
  function isPlainObject(value) {
61648
62253
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
61649
62254
  }
61650
- function backendFor(server, downstream, openapi, graphql, http) {
62255
+ function backendFor(server, downstream, openapi, graphql, http, cli) {
61651
62256
  if (server.backend === "mcp") return {
61652
62257
  check: (...args) => downstream.checkServer(...args),
61653
62258
  listTools: (...args) => downstream.listTools(...args),
@@ -61678,6 +62283,17 @@ function backendFor(server, downstream, openapi, graphql, http) {
61678
62283
  search: (...args) => http.search(...args)
61679
62284
  };
61680
62285
  }
62286
+ if (server.backend === "cli") {
62287
+ if (!cli) throw new CapletsError("INTERNAL_ERROR", "CLI tools manager is not configured");
62288
+ return {
62289
+ check: (...args) => cli.checkTools(...args),
62290
+ listTools: (...args) => cli.listTools(...args),
62291
+ getTool: (...args) => cli.getTool(...args),
62292
+ callTool: (...args) => cli.callTool(...args),
62293
+ compact: (...args) => cli.compact(...args),
62294
+ search: (...args) => cli.search(...args)
62295
+ };
62296
+ }
61681
62297
  if (!openapi) throw new CapletsError("INTERNAL_ERROR", "OpenAPI manager is not configured");
61682
62298
  return {
61683
62299
  check: (...args) => openapi.checkEndpoint(...args),
@@ -61697,6 +62313,7 @@ var CapletsRuntime = class {
61697
62313
  openapi;
61698
62314
  graphql;
61699
62315
  http;
62316
+ cli;
61700
62317
  tools = /* @__PURE__ */ new Map();
61701
62318
  paths;
61702
62319
  watchDebounceMs;
@@ -61718,6 +62335,7 @@ var CapletsRuntime = class {
61718
62335
  this.openapi = new OpenApiManager(this.registry, selectAuthOptions(options.authDir));
61719
62336
  this.graphql = new GraphQLManager(this.registry, selectAuthOptions(options.authDir));
61720
62337
  this.http = new HttpActionManager(this.registry, selectAuthOptions(options.authDir));
62338
+ this.cli = new CliToolsManager(this.registry);
61721
62339
  this.server = options.server ?? new McpServer({
61722
62340
  name: "caplets",
61723
62341
  version
@@ -61794,6 +62412,7 @@ var CapletsRuntime = class {
61794
62412
  this.openapi.updateRegistry(nextRegistry);
61795
62413
  this.graphql.updateRegistry(nextRegistry);
61796
62414
  this.http.updateRegistry(nextRegistry);
62415
+ this.cli.updateRegistry(nextRegistry);
61797
62416
  let invalidated = true;
61798
62417
  try {
61799
62418
  await this.invalidateChangedBackends(previousConfig, nextConfig);
@@ -61852,7 +62471,7 @@ var CapletsRuntime = class {
61852
62471
  }
61853
62472
  async handleTool(serverId, request) {
61854
62473
  try {
61855
- return await handleServerTool(this.registry.require(serverId), request, this.registry, this.downstream, this.openapi, this.graphql, this.http);
62474
+ return await handleServerTool(this.registry.require(serverId), request, this.registry, this.downstream, this.openapi, this.graphql, this.http, this.cli);
61856
62475
  } catch (error) {
61857
62476
  return errorResult(error);
61858
62477
  }
@@ -61869,6 +62488,7 @@ var CapletsRuntime = class {
61869
62488
  if (before?.backend === "openapi" || after?.backend === "openapi" || !after) this.openapi.invalidate(serverId);
61870
62489
  if (before?.backend === "graphql" || after?.backend === "graphql" || !after) this.graphql.invalidate(serverId);
61871
62490
  if (before?.backend === "http" || after?.backend === "http" || !after) this.http.invalidate(serverId);
62491
+ if (before?.backend === "cli" || after?.backend === "cli" || !after) this.cli.invalidate(serverId);
61872
62492
  }
61873
62493
  }
61874
62494
  resetWatchers() {
@@ -61961,14 +62581,15 @@ function allCaplets(config) {
61961
62581
  ...Object.values(config.mcpServers),
61962
62582
  ...Object.values(config.openapiEndpoints),
61963
62583
  ...Object.values(config.graphqlEndpoints),
61964
- ...Object.values(config.httpApis)
62584
+ ...Object.values(config.httpApis),
62585
+ ...Object.values(config.cliTools)
61965
62586
  ];
61966
62587
  }
61967
62588
  function nextEnabledServers(config) {
61968
62589
  return allCaplets(config).filter((server) => !server.disabled);
61969
62590
  }
61970
62591
  function capletById(config, serverId) {
61971
- return config.mcpServers[serverId] ?? config.openapiEndpoints[serverId] ?? config.graphqlEndpoints[serverId] ?? config.httpApis[serverId];
62592
+ return config.mcpServers[serverId] ?? config.openapiEndpoints[serverId] ?? config.graphqlEndpoints[serverId] ?? config.httpApis[serverId] ?? config.cliTools[serverId];
61972
62593
  }
61973
62594
  function serializeCaplet(caplet) {
61974
62595
  return JSON.stringify(caplet ?? null);