@caplets/core 0.16.0 → 0.18.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,4 +1,6 @@
1
- import { $ as getLiteralValue, A as ErrorCode, B as ListToolsRequestSchema, C as CompleteRequestSchema, Ct as string, Dt as __commonJSMin, E as CreateTaskResultSchema, Et as toSafeError, F as LATEST_PROTOCOL_VERSION, G as SetLevelRequestSchema, H as McpError, I as ListPromptsRequestSchema, J as assertCompleteRequestResourceTemplate, L as ListResourceTemplatesRequestSchema, M as InitializeRequestSchema, N as InitializedNotificationSchema, O as ElicitResultSchema, Ot as __require, P as JSONRPCMessageSchema, Q as isJSONRPCResultResponse, R as ListResourcesRequestSchema, S as CallToolResultSchema, St as object, T as CreateMessageResultWithToolsSchema, Tt as CapletsError, U as ReadResourceRequestSchema, V as LoggingLevelSchema, W as SUPPORTED_PROTOCOL_VERSIONS, X as isJSONRPCErrorResponse, Y as isInitializeRequest, Z as isJSONRPCRequest, _ as AjvJsonSchemaValidator, _t as discoverCapletFiles, a as capabilityDescription, at as normalizeObjectSchema, b as toJsonSchemaCompat, bt as ZodOptional, c as deleteTokenBundle, ct as safeParseAsync, dt as parseConfig, et as getObjectShape, f as ReadBuffer, ft as DEFAULT_AUTH_DIR, g as assertToolsCallTaskCapability, gt as resolveProjectConfigPath, h as assertClientRequestTaskCapability, ht as resolveProjectCapletsRoot, i as ServerRegistry, it as isZ4Schema, j as GetPromptRequestSchema, k as EmptyResultSchema, kt as __toESM, l as isTokenBundleExpired, lt as loadConfig, mt as resolveConfigPath, n as generatedToolInputSchema, nt as getSchemaDescription, o as runGenericOAuthFlow, ot as objectFromShape, p as serializeMessage, pt as resolveCapletsRoot, q as assertCompleteRequestPrompt, r as handleServerTool, rt as isSchemaOptional, s as runOAuthFlow, st as safeParse, t as CapletsEngine, tt as getParseErrorMessage, u as readTokenBundle, ut as loadConfigWithSources, v as Protocol, vt as validateCapletFile, w as CreateMessageResultSchema, wt as url, x as CallToolRequestSchema, xt as literal, y as mergeCapabilities, yt as SERVER_ID_PATTERN, z as ListRootsResultSchema } from "./engine-Brwid_mq.js";
1
+ import { $ as assertCompleteRequestPrompt, A as CreateMessageResultSchema, At as toSafeError, B as JSONRPCMessageSchema, C as AjvJsonSchemaValidator, D as CallToolRequestSchema, Dt as CAPLETS_ERROR_CODES, E as toJsonSchemaCompat, Et as SERVER_ID_PATTERN, F as EmptyResultSchema, G as ListRootsResultSchema, H as ListPromptsRequestSchema, I as ErrorCode, J as McpError, K as ListToolsRequestSchema, L as GetPromptRequestSchema, M as CreateTaskResultSchema, Nt as __require, O as CallToolResultSchema, Ot as CapletsError, P as ElicitResultSchema, Pt as __toESM, R as InitializeRequestSchema, S as assertToolsCallTaskCapability, St as resolveProjectCapletsRoot, T as mergeCapabilities, Tt as validateCapletFile, U as ListResourceTemplatesRequestSchema, V as LATEST_PROTOCOL_VERSION, W as ListResourcesRequestSchema, X as SUPPORTED_PROTOCOL_VERSIONS, Y as ReadResourceRequestSchema, Z as SetLevelRequestSchema, _t as parseConfig, a as resolveCapletsServer, at as getLiteralValue, bt as resolveCapletsRoot, c as ServerRegistry, ct as getSchemaDescription, d as runOAuthFlow, dt as normalizeObjectSchema, et as assertCompleteRequestResourceTemplate, f as startGenericOAuthFlow, ft as objectFromShape, g as readTokenBundle, gt as loadConfigWithSources, h as isTokenBundleExpired, ht as loadConfig, i as resolveCapletsMode, it as isJSONRPCResultResponse, j as CreateMessageResultWithToolsSchema, jt as __commonJSMin, k as CompleteRequestSchema, kt as redactSecrets, l as capabilityDescription, lt as isSchemaOptional, m as deleteTokenBundle, mt as safeParseAsync, nt as isJSONRPCErrorResponse, o as CapletsEngine, ot as getObjectShape, p as startOAuthFlow, pt as safeParse, q as LoggingLevelSchema, r as parseServerBaseUrl, rt as isJSONRPCRequest, s as handleServerTool, st as getParseErrorMessage, t as controlUrlForBase, tt as isInitializeRequest, u as runGenericOAuthFlow, ut as isZ4Schema, v as ReadBuffer, w as Protocol, wt as discoverCapletFiles, x as assertClientRequestTaskCapability, xt as resolveConfigPath, y as serializeMessage, z as InitializedNotificationSchema } from "./options-DM1cMRcp.js";
2
+ import { A as url, C as object, D as string, b as literal, d as ZodOptional, o as generatedToolInputSchema, s as generatedToolInputSchemaForCaplet } from "./generated-tool-input-schema-B6rce396.js";
3
+ import { a as formatCapletList, c as resolveCliConfigPaths, l as cliCommands, n as completionScript, o as formatConfigPaths, s as listCaplets, t as completeCliWords, u as completionShells } from "./completion-CxGG6ae3.js";
2
4
  import { accessSync, chmodSync, closeSync, constants, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
3
5
  import { basename, dirname, join, parse, relative, resolve } from "node:path";
4
6
  import { execFileSync } from "node:child_process";
@@ -1319,7 +1321,7 @@ const EMPTY_COMPLETION_RESULT = { completion: {
1319
1321
  } };
1320
1322
  //#endregion
1321
1323
  //#region package.json
1322
- var version = "0.16.0";
1324
+ var version = "0.18.0";
1323
1325
  //#endregion
1324
1326
  //#region src/serve/session.ts
1325
1327
  var CapletsMcpSession = class {
@@ -1363,6 +1365,7 @@ var CapletsMcpSession = class {
1363
1365
  if (!previousCaplet || serializeCaplet(previousCaplet) !== serializeCaplet(caplet)) tool.update({
1364
1366
  title: caplet.name,
1365
1367
  description: capabilityDescription(caplet),
1368
+ paramsSchema: generatedToolInputSchemaForCaplet(caplet).shape,
1366
1369
  callback: async (request) => this.handleTool(serverId, request),
1367
1370
  enabled: true
1368
1371
  });
@@ -1376,7 +1379,7 @@ var CapletsMcpSession = class {
1376
1379
  return this.server.registerTool(caplet.server, {
1377
1380
  title: caplet.name,
1378
1381
  description: capabilityDescription(caplet),
1379
- inputSchema: generatedToolInputSchema
1382
+ inputSchema: generatedToolInputSchemaForCaplet(caplet).shape
1380
1383
  }, async (request) => this.handleTool(caplet.server, request));
1381
1384
  }
1382
1385
  async handleTool(serverId, request) {
@@ -4921,49 +4924,56 @@ async function loginAuth(serverId, options) {
4921
4924
  }
4922
4925
  }
4923
4926
  function logoutAuth(serverId, options) {
4924
- assertLoginTarget(findAuthTarget(serverId, loadConfig(options.configPath)), serverId);
4925
- if (deleteTokenBundle(serverId, options.authDir)) options.writeOut(`Deleted OAuth credentials for \`${serverId}\`.\n`);
4927
+ if (logoutAuthResult(serverId, options).deleted) options.writeOut(`Deleted OAuth credentials for \`${serverId}\`.\n`);
4926
4928
  else options.writeOut(`No OAuth credentials found for \`${serverId}\`.\n`);
4927
4929
  }
4930
+ function logoutAuthResult(serverId, options) {
4931
+ assertLoginTarget(findAuthTarget(serverId, loadConfig(options.configPath)), serverId);
4932
+ return {
4933
+ server: serverId,
4934
+ deleted: deleteTokenBundle(serverId, options.authDir)
4935
+ };
4936
+ }
4928
4937
  function listAuth(options) {
4929
- const servers = authTargets(loadConfig(options.configPath)).sort((left, right) => left.server.localeCompare(right.server));
4938
+ const rows = listAuthRows(options);
4930
4939
  const format = options.format ?? "plain";
4931
4940
  if (format === "json") {
4932
- const rows = servers.map((server) => {
4933
- const bundle = readTokenBundle(server.server, options.authDir);
4934
- const status = !bundle ? "missing" : isTokenBundleExpired(bundle) ? "expired" : "authenticated";
4935
- return {
4936
- server: server.server,
4937
- status,
4938
- ...bundle?.expiresAt ? { expiresAt: bundle.expiresAt } : {},
4939
- ...bundle?.scope ? { scope: bundle.scope } : {}
4940
- };
4941
- });
4942
4941
  options.writeOut(`${JSON.stringify(rows, null, 2)}\n`);
4943
4942
  return;
4944
4943
  }
4945
- if (servers.length === 0) {
4946
- options.writeOut(format === "markdown" ? "## OAuth credentials\n\nNo configured remote OAuth servers found.\n" : "No configured remote OAuth servers found.\n");
4947
- return;
4948
- }
4949
- if (format === "markdown") options.writeOut("## OAuth credentials\n\n");
4950
- else options.writeOut("OAuth credentials\n\n");
4951
- for (const server of servers) {
4944
+ options.writeOut(formatAuthRows(rows, format));
4945
+ }
4946
+ function listAuthRows(options) {
4947
+ return authTargets(loadConfig(options.configPath)).sort((left, right) => left.server.localeCompare(right.server)).map((server) => {
4952
4948
  const bundle = readTokenBundle(server.server, options.authDir);
4953
4949
  const status = !bundle ? "missing" : isTokenBundleExpired(bundle) ? "expired" : "authenticated";
4954
- const details = [bundle?.expiresAt ? `expires ${bundle.expiresAt}` : void 0, bundle?.scope ? `scope ${bundle.scope}` : void 0].filter(Boolean).join("; ");
4950
+ return {
4951
+ server: server.server,
4952
+ status,
4953
+ ...bundle?.expiresAt ? { expiresAt: bundle.expiresAt } : {},
4954
+ ...bundle?.scope ? { scope: bundle.scope } : {}
4955
+ };
4956
+ });
4957
+ }
4958
+ function formatAuthRows(rows, format) {
4959
+ if (rows.length === 0) return format === "markdown" ? "## OAuth credentials\n\nNo configured remote OAuth servers found.\n" : "No configured remote OAuth servers found.\n";
4960
+ let output = "";
4961
+ if (format === "markdown") output += "## OAuth credentials\n\n";
4962
+ else output += "OAuth credentials\n\n";
4963
+ for (const row of rows) {
4964
+ const details = [row.expiresAt ? `expires ${row.expiresAt}` : void 0, row.scope ? `scope ${row.scope}` : void 0].filter(Boolean).join("; ");
4955
4965
  if (format === "markdown") {
4956
- options.writeOut(`- \`${server.server}\` — ${status}${details ? ` (${details})` : ""}\n`);
4966
+ output += `- \`${row.server}\` — ${row.status}${details ? ` (${details})` : ""}\n`;
4957
4967
  continue;
4958
4968
  }
4959
- options.writeOut([
4960
- server.server,
4961
- ` Status: ${status}`,
4962
- ...bundle?.expiresAt ? [` Expires: ${bundle.expiresAt}`] : [],
4963
- ...bundle?.scope ? [` Scope: ${bundle.scope}`] : []
4964
- ].join("\n"));
4965
- options.writeOut("\n\n");
4969
+ output += [
4970
+ row.server,
4971
+ ` Status: ${row.status}`,
4972
+ ...row.expiresAt ? [` Expires: ${row.expiresAt}`] : [],
4973
+ ...row.scope ? [` Scope: ${row.scope}`] : []
4974
+ ].join("\n") + "\n\n";
4966
4975
  }
4976
+ return output;
4967
4977
  }
4968
4978
  function findAuthTarget(serverId, config = loadConfig()) {
4969
4979
  return authTargets(config).find((server) => server.server === serverId);
@@ -5035,126 +5045,6 @@ function starterConfig() {
5035
5045
  }, null, 2);
5036
5046
  }
5037
5047
  //#endregion
5038
- //#region src/cli/inspection.ts
5039
- function listCaplets(configWithSources, options) {
5040
- const { config, sources, shadows } = configWithSources;
5041
- return allCaplets(config).filter((server) => options.includeDisabled || !server.disabled).map((server) => ({
5042
- server: server.server,
5043
- backend: server.backend,
5044
- name: server.name,
5045
- description: server.description,
5046
- disabled: server.disabled,
5047
- status: initialServerStatus(server),
5048
- source: sources[server.server]?.kind ?? "unknown",
5049
- path: sources[server.server]?.path ?? null,
5050
- shadows: shadows[server.server] ?? []
5051
- })).sort((left, right) => left.server.localeCompare(right.server));
5052
- }
5053
- function initialServerStatus(server) {
5054
- return server.disabled ? "disabled" : "not_started";
5055
- }
5056
- function allCaplets(config) {
5057
- return [
5058
- ...Object.values(config.mcpServers),
5059
- ...Object.values(config.openapiEndpoints),
5060
- ...Object.values(config.graphqlEndpoints),
5061
- ...Object.values(config.httpApis),
5062
- ...Object.values(config.cliTools)
5063
- ];
5064
- }
5065
- function formatCapletList(rows, format = "plain") {
5066
- return format === "markdown" ? formatCapletListMarkdown(rows) : formatCapletListPlain(rows);
5067
- }
5068
- function formatCapletListMarkdown(rows) {
5069
- if (rows.length === 0) return "## Configured Caplets\n\nNo configured Caplets found.\n";
5070
- const heading = [
5071
- "## Configured Caplets",
5072
- "",
5073
- `${rows.length} ${rows.length === 1 ? "Caplet" : "Caplets"} shown.`,
5074
- ""
5075
- ];
5076
- const entries = rows.flatMap((row) => [
5077
- `- \`${row.server}\` — ${row.name}`,
5078
- ` - Backend: ${row.backend}`,
5079
- ` - Status: ${row.status}`,
5080
- ` - Source: ${row.source}`,
5081
- ...row.disabled ? [" - Disabled: true"] : [],
5082
- ...row.path ? [` - Path: ${row.path}`] : []
5083
- ]);
5084
- const warnings = rows.flatMap((row) => row.shadows.map((shadow) => `Warning: ${formatSourceKind(row.source)} Caplet ${row.server} shadows ${formatSourceKind(shadow.kind)} Caplet at ${shadow.path}`));
5085
- if (warnings.length === 0) return `${[...heading, ...entries].join("\n")}\n`;
5086
- return `${[
5087
- ...heading,
5088
- ...entries,
5089
- "",
5090
- "Warnings:",
5091
- ...warnings.map((warning) => `- ${warning}`)
5092
- ].join("\n")}\n`;
5093
- }
5094
- function formatCapletListPlain(rows) {
5095
- if (rows.length === 0) return "No configured Caplets found.\n";
5096
- const entries = rows.map((row) => [
5097
- row.server,
5098
- ` Name: ${row.name}`,
5099
- ` Backend: ${row.backend}`,
5100
- ` Status: ${row.status}`,
5101
- ` Source: ${row.source}`,
5102
- ...row.disabled ? [" Disabled: true"] : [],
5103
- ...row.path ? [` Path: ${row.path}`] : []
5104
- ].join("\n")).join("\n\n");
5105
- const warnings = rows.flatMap((row) => row.shadows.map((shadow) => `Warning: ${formatSourceKind(row.source)} Caplet ${row.server} shadows ${formatSourceKind(shadow.kind)} Caplet at ${shadow.path}`));
5106
- if (warnings.length === 0) return `Configured Caplets (${rows.length})\n\n${entries}\n`;
5107
- return `Configured Caplets (${rows.length})\n\n${entries}\n\n${warnings.join("\n")}\n`;
5108
- }
5109
- function formatSourceKind(kind) {
5110
- if (kind.startsWith("project")) return "project";
5111
- if (kind.startsWith("global")) return "global";
5112
- return kind;
5113
- }
5114
- function resolveCliConfigPaths(envConfigPath, authDir) {
5115
- const configPath = resolveConfigPath(envConfigPath);
5116
- const effectiveAuthDir = authDir ?? DEFAULT_AUTH_DIR;
5117
- return {
5118
- userConfig: configPath,
5119
- projectConfig: resolveProjectConfigPath(),
5120
- userRoot: resolveCapletsRoot(configPath),
5121
- stateRoot: dirname(effectiveAuthDir),
5122
- projectRoot: resolveProjectCapletsRoot(),
5123
- authDir: effectiveAuthDir,
5124
- envConfig: envConfigPath ?? null
5125
- };
5126
- }
5127
- function formatConfigPaths(paths, format = "plain") {
5128
- if (format === "markdown") return formatConfigPathsMarkdown(paths);
5129
- return formatConfigPathsPlain(paths);
5130
- }
5131
- function formatConfigPathsMarkdown(paths) {
5132
- return [
5133
- "## Caplets paths",
5134
- "",
5135
- `- User config: ${paths.userConfig}`,
5136
- `- Project config: ${paths.projectConfig}`,
5137
- `- User Caplets root: ${paths.userRoot}`,
5138
- `- State root: ${paths.stateRoot}`,
5139
- `- Project Caplets root: ${paths.projectRoot}`,
5140
- `- Auth directory: ${paths.authDir}`,
5141
- `- CAPLETS_CONFIG: ${paths.envConfig ?? "unset"}`
5142
- ].join("\n") + "\n";
5143
- }
5144
- function formatConfigPathsPlain(paths) {
5145
- return [
5146
- "Caplets paths",
5147
- "",
5148
- `User config: ${paths.userConfig}`,
5149
- `Project config: ${paths.projectConfig}`,
5150
- `User root: ${paths.userRoot}`,
5151
- `State root: ${paths.stateRoot}`,
5152
- `Project root: ${paths.projectRoot}`,
5153
- `Auth directory: ${paths.authDir}`,
5154
- `CAPLETS_CONFIG: ${paths.envConfig ?? "unset"}`
5155
- ].join("\n") + "\n";
5156
- }
5157
- //#endregion
5158
5048
  //#region src/cli/install.ts
5159
5049
  function installCaplets(repo, options = {}) {
5160
5050
  const source = resolveInstallSource(repo);
@@ -5390,6 +5280,96 @@ function nearestExistingParent(path) {
5390
5280
  return nearestExistingParent(parent);
5391
5281
  }
5392
5282
  //#endregion
5283
+ //#region src/remote-control/client.ts
5284
+ var RemoteControlClient = class {
5285
+ #baseUrl;
5286
+ #requestInit;
5287
+ #fetch;
5288
+ constructor(options) {
5289
+ this.#baseUrl = options.baseUrl;
5290
+ this.#requestInit = options.requestInit;
5291
+ this.#fetch = options.fetch ?? fetch;
5292
+ }
5293
+ async request(command, args) {
5294
+ const controlUrl = controlUrlForBase(this.#baseUrl);
5295
+ let response;
5296
+ try {
5297
+ response = await this.#fetch(controlUrl, {
5298
+ ...this.#requestInit,
5299
+ method: "POST",
5300
+ headers: mergeJsonHeaders(this.#requestInit.headers),
5301
+ body: JSON.stringify({
5302
+ command,
5303
+ arguments: args
5304
+ })
5305
+ });
5306
+ } catch (error) {
5307
+ throw new CapletsError("SERVER_UNAVAILABLE", `Could not connect to Caplets server at ${safeBaseUrl(this.#baseUrl)}.`, toSafeError(error, "SERVER_UNAVAILABLE"));
5308
+ }
5309
+ if (response.status === 401 || response.status === 403) throw new CapletsError("AUTH_FAILED", "Caplets server authentication failed. Check CAPLETS_SERVER_USER and CAPLETS_SERVER_PASSWORD.");
5310
+ if (!response.ok) throw new CapletsError("SERVER_UNAVAILABLE", `Caplets server at ${safeBaseUrl(this.#baseUrl)} returned HTTP ${response.status}.`);
5311
+ const payload = await parseRemoteCliResponse(response);
5312
+ if (!payload.ok) throw new CapletsError(payload.error.code, redactRemoteMessage(payload.error.message), payload.error.nextAction === void 0 ? void 0 : { nextAction: payload.error.nextAction });
5313
+ return payload.result;
5314
+ }
5315
+ };
5316
+ function mergeJsonHeaders(headers) {
5317
+ const merged = new Headers(headers);
5318
+ merged.set("content-type", "application/json");
5319
+ return merged;
5320
+ }
5321
+ function safeBaseUrl(baseUrl) {
5322
+ const safe = new URL(baseUrl.href);
5323
+ safe.username = "";
5324
+ safe.password = "";
5325
+ safe.search = "";
5326
+ safe.hash = "";
5327
+ return safe.toString();
5328
+ }
5329
+ async function parseRemoteCliResponse(response) {
5330
+ let payload;
5331
+ try {
5332
+ payload = await response.json();
5333
+ } catch (error) {
5334
+ throw invalidRemoteControlResponse(error);
5335
+ }
5336
+ if (!isRecord(payload)) throw invalidRemoteControlResponse();
5337
+ if (payload.ok === true) {
5338
+ if (!("result" in payload)) throw invalidRemoteControlResponse();
5339
+ return {
5340
+ ok: true,
5341
+ result: payload.result
5342
+ };
5343
+ }
5344
+ if (payload.ok === false) {
5345
+ const error = payload.error;
5346
+ if (!isRecord(error) || typeof error.code !== "string" || typeof error.message !== "string") throw invalidRemoteControlResponse();
5347
+ if ("nextAction" in error && error.nextAction !== void 0 && typeof error.nextAction !== "string") throw invalidRemoteControlResponse();
5348
+ const errorResponse = {
5349
+ ok: false,
5350
+ error: {
5351
+ code: isCapletsErrorCode(error.code) ? error.code : "DOWNSTREAM_TOOL_ERROR",
5352
+ message: error.message
5353
+ }
5354
+ };
5355
+ if (typeof error.nextAction === "string") errorResponse.error.nextAction = error.nextAction;
5356
+ return errorResponse;
5357
+ }
5358
+ throw invalidRemoteControlResponse();
5359
+ }
5360
+ function invalidRemoteControlResponse(cause) {
5361
+ return new CapletsError("DOWNSTREAM_PROTOCOL_ERROR", "Caplets server returned an invalid remote control response.", cause === void 0 ? void 0 : toSafeError(cause, "DOWNSTREAM_PROTOCOL_ERROR"));
5362
+ }
5363
+ function isRecord(value) {
5364
+ return value !== null && typeof value === "object" && !Array.isArray(value);
5365
+ }
5366
+ function isCapletsErrorCode(value) {
5367
+ return CAPLETS_ERROR_CODES.includes(value);
5368
+ }
5369
+ function redactRemoteMessage(message) {
5370
+ return String(redactSecrets(message)).replace(/\b(authorization\s*:\s*(?:basic|bearer)\s+)[^\s,;]+/giu, "$1[REDACTED]").replace(/\b((?:access_)?token=)[^\s,;]+/giu, "$1[REDACTED]").replace(/\b((?:token|secret|authorization|auth|api[-_]?key|password|credential|clientsecret|client_secret|code|refresh(?:_token)?)\s*[=:]\s*)[^\s,;]+/giu, "$1[REDACTED]");
5371
+ }
5372
+ //#endregion
5393
5373
  //#region ../../node_modules/.pnpm/hono@4.12.19/node_modules/hono/dist/compose.js
5394
5374
  var compose = (middleware, onError, onNotFound) => {
5395
5375
  return (context, next) => {
@@ -8309,30 +8289,372 @@ var logger = (fn = console.log) => {
8309
8289
  };
8310
8290
  };
8311
8291
  //#endregion
8292
+ //#region src/remote-control/dispatch.ts
8293
+ const ENGINE_COMMANDS = new Set([
8294
+ "get_caplet",
8295
+ "check_backend",
8296
+ "list_tools",
8297
+ "search_tools",
8298
+ "get_tool",
8299
+ "call_tool",
8300
+ "list_resources",
8301
+ "search_resources",
8302
+ "list_resource_templates",
8303
+ "read_resource",
8304
+ "list_prompts",
8305
+ "search_prompts",
8306
+ "get_prompt",
8307
+ "complete"
8308
+ ]);
8309
+ async function dispatchRemoteCliRequest(request, context) {
8310
+ try {
8311
+ return {
8312
+ ok: true,
8313
+ result: await dispatch(request, context)
8314
+ };
8315
+ } catch (error) {
8316
+ const safe = toSafeError(error);
8317
+ const action = nextAction(safe.details);
8318
+ return {
8319
+ ok: false,
8320
+ error: {
8321
+ code: safe.code,
8322
+ message: redactControlErrorMessage(safe.message),
8323
+ ...action ? { nextAction: action } : {}
8324
+ }
8325
+ };
8326
+ }
8327
+ }
8328
+ async function dispatch(request, context) {
8329
+ assertObject(request, "remote control request");
8330
+ assertObject(request.arguments, "remote control request arguments");
8331
+ if (request.command === "list") return listCaplets(loadConfigWithSources(context.configPath, context.projectConfigPath), { includeDisabled: optionalBoolean(request.arguments, "includeDisabled") ?? false });
8332
+ if (ENGINE_COMMANDS.has(request.command)) {
8333
+ const caplet = requiredString(request.arguments, "caplet");
8334
+ const toolRequest = requiredEngineRequest(request.arguments, request.command);
8335
+ const engine = new CapletsEngine(context);
8336
+ try {
8337
+ return await engine.execute(caplet, toolRequest);
8338
+ } finally {
8339
+ await engine.close();
8340
+ }
8341
+ }
8342
+ if (request.command === "init") return {
8343
+ remote: true,
8344
+ path: initConfig({
8345
+ ...optionalProp("path", context.configPath),
8346
+ ...optionalProp("force", optionalBoolean(request.arguments, "force"))
8347
+ })
8348
+ };
8349
+ if (request.command === "add") return dispatchAdd(request.arguments, context);
8350
+ if (request.command === "install") return {
8351
+ remote: true,
8352
+ ...installCaplets(requiredString(request.arguments, "repo"), {
8353
+ ...optionalProp("capletIds", optionalStringArray(request.arguments, "capletIds")),
8354
+ destinationRoot: context.projectCapletsRoot,
8355
+ ...optionalProp("force", optionalBoolean(request.arguments, "force"))
8356
+ })
8357
+ };
8358
+ if (request.command === "complete_cli") {
8359
+ const shell = optionalString(request.arguments, "shell") ?? "bash";
8360
+ if (!completionShells.includes(shell)) return [];
8361
+ const engine = new CapletsEngine(context);
8362
+ try {
8363
+ return await engine.completeCliWords(optionalStringArray(request.arguments, "words") ?? [""]);
8364
+ } finally {
8365
+ await engine.close();
8366
+ }
8367
+ }
8368
+ if (request.command === "auth_list") return listAuthRows({
8369
+ ...optionalProp("configPath", context.configPath),
8370
+ ...optionalProp("authDir", context.authDir)
8371
+ });
8372
+ if (request.command === "auth_logout") return logoutAuthResult(requiredString(request.arguments, "server"), {
8373
+ ...optionalProp("configPath", context.configPath),
8374
+ ...optionalProp("authDir", context.authDir)
8375
+ });
8376
+ if (request.command === "auth_login_start") return startRemoteAuthLogin(requiredString(request.arguments, "server"), context);
8377
+ if (request.command === "auth_login_complete") return completeRemoteAuthLogin(requiredString(request.arguments, "flowId"), requiredString(request.arguments, "callbackUrl"), context);
8378
+ throw new CapletsError("UNKNOWN_OPERATION", `Unsupported remote control command ${request.command}`);
8379
+ }
8380
+ async function startRemoteAuthLogin(serverId, context) {
8381
+ if (!context.authFlowStore || !context.controlCallbackBaseUrl) throw new CapletsError("REQUEST_INVALID", "Remote auth login is not available on this server");
8382
+ const config = loadConfigWithSources(context.configPath, context.projectConfigPath).config;
8383
+ const target = findAuthTarget(serverId, config);
8384
+ assertLoginTarget(target, serverId);
8385
+ const flowId = randomUUID();
8386
+ const baseUrl = context.controlCallbackBaseUrl.endsWith("/") ? context.controlCallbackBaseUrl : `${context.controlCallbackBaseUrl}/`;
8387
+ const redirectUri = new URL(`auth/callback/${flowId}`, baseUrl).toString();
8388
+ const started = target.backend === "mcp" ? await startOAuthFlow(target, {
8389
+ redirectUri,
8390
+ ...optionalProp("authDir", context.authDir)
8391
+ }) : await startGenericOAuthFlow(target, {
8392
+ redirectUri,
8393
+ ...optionalProp("authDir", context.authDir)
8394
+ });
8395
+ if (!started.authorizationUrl) return {
8396
+ server: serverId,
8397
+ authenticated: true
8398
+ };
8399
+ const flow = context.authFlowStore.create({
8400
+ server: serverId,
8401
+ authorizationUrl: started.authorizationUrl,
8402
+ complete: started.complete
8403
+ }, flowId);
8404
+ return {
8405
+ server: serverId,
8406
+ flowId: flow.id,
8407
+ authorizationUrl: flow.authorizationUrl
8408
+ };
8409
+ }
8410
+ async function completeRemoteAuthLogin(flowId, callbackUrl, context) {
8411
+ const flow = context.authFlowStore?.get(flowId);
8412
+ if (!flow) throw new CapletsError("REQUEST_INVALID", `Unknown auth flow ${flowId}`);
8413
+ context.authFlowStore?.delete(flowId);
8414
+ await flow.complete(callbackUrl);
8415
+ return {
8416
+ server: flow.server,
8417
+ authenticated: true
8418
+ };
8419
+ }
8420
+ function dispatchAdd(args, context) {
8421
+ const kind = requiredString(args, "kind");
8422
+ const id = requiredString(args, "id");
8423
+ const options = remoteAddOptions$1(kind, optionalObject(args, "options"));
8424
+ switch (kind) {
8425
+ case "cli": return {
8426
+ remote: true,
8427
+ label: "CLI",
8428
+ ...addCliCaplet(id, {
8429
+ ...options,
8430
+ destinationRoot: context.projectCapletsRoot,
8431
+ print: false
8432
+ })
8433
+ };
8434
+ case "mcp": return {
8435
+ remote: true,
8436
+ label: "MCP",
8437
+ ...addMcpCaplet(id, {
8438
+ ...options,
8439
+ destinationRoot: context.projectCapletsRoot,
8440
+ print: false
8441
+ })
8442
+ };
8443
+ case "openapi": return {
8444
+ remote: true,
8445
+ label: "OpenAPI",
8446
+ ...addOpenApiCaplet(id, {
8447
+ ...options,
8448
+ destinationRoot: context.projectCapletsRoot,
8449
+ print: false
8450
+ })
8451
+ };
8452
+ case "graphql": return {
8453
+ remote: true,
8454
+ label: "GraphQL",
8455
+ ...addGraphqlCaplet(id, {
8456
+ ...options,
8457
+ destinationRoot: context.projectCapletsRoot,
8458
+ print: false
8459
+ })
8460
+ };
8461
+ case "http": return {
8462
+ remote: true,
8463
+ label: "HTTP",
8464
+ ...addHttpCaplet(id, {
8465
+ ...options,
8466
+ destinationRoot: context.projectCapletsRoot,
8467
+ print: false
8468
+ })
8469
+ };
8470
+ default: throw new CapletsError("REQUEST_INVALID", "add.kind must be cli, mcp, openapi, graphql, or http");
8471
+ }
8472
+ }
8473
+ function optionalProp(key, value) {
8474
+ return value === void 0 ? {} : { [key]: value };
8475
+ }
8476
+ function assertObject(value, label) {
8477
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw new CapletsError("REQUEST_INVALID", `${label} must be an object`);
8478
+ }
8479
+ function requiredString(args, key) {
8480
+ const value = args[key];
8481
+ if (typeof value !== "string" || value.length === 0) throw new CapletsError("REQUEST_INVALID", `${key} must be a non-empty string`);
8482
+ return value;
8483
+ }
8484
+ function optionalString(args, key) {
8485
+ const value = args[key];
8486
+ if (value === void 0) return;
8487
+ if (typeof value !== "string") throw new CapletsError("REQUEST_INVALID", `${key} must be a string`);
8488
+ return value;
8489
+ }
8490
+ function optionalObject(args, key) {
8491
+ const value = args[key];
8492
+ if (value === void 0) return {};
8493
+ assertObject(value, key);
8494
+ return value;
8495
+ }
8496
+ function requiredEngineRequest(args, command) {
8497
+ const toolRequest = optionalObject(args, "request");
8498
+ if (typeof toolRequest.operation !== "string") throw new CapletsError("REQUEST_INVALID", "request.operation must be a string");
8499
+ if (toolRequest.operation !== command) throw new CapletsError("REQUEST_INVALID", `request.operation must match remote command ${command}`);
8500
+ return toolRequest;
8501
+ }
8502
+ function remoteAddOptions$1(kind, options) {
8503
+ rejectServerOwnedAddOptions(options);
8504
+ switch (kind) {
8505
+ case "cli": return pickOptions(options, {
8506
+ repo: "string",
8507
+ include: "string",
8508
+ command: "string",
8509
+ force: "boolean"
8510
+ });
8511
+ case "mcp": return pickOptions(options, {
8512
+ command: "string",
8513
+ arg: "string-array",
8514
+ cwd: "string",
8515
+ env: "string-array",
8516
+ url: "string",
8517
+ transport: "string",
8518
+ tokenEnv: "string",
8519
+ force: "boolean"
8520
+ });
8521
+ case "openapi": return pickOptions(options, {
8522
+ spec: "string",
8523
+ baseUrl: "string",
8524
+ tokenEnv: "string",
8525
+ force: "boolean"
8526
+ });
8527
+ case "graphql": return pickOptions(options, {
8528
+ endpointUrl: "string",
8529
+ schema: "string",
8530
+ introspection: "boolean",
8531
+ tokenEnv: "string",
8532
+ force: "boolean"
8533
+ });
8534
+ case "http": return pickOptions(options, {
8535
+ baseUrl: "string",
8536
+ action: "string-array",
8537
+ tokenEnv: "string",
8538
+ force: "boolean"
8539
+ });
8540
+ default: return options;
8541
+ }
8542
+ }
8543
+ function pickOptions(options, schema) {
8544
+ const next = {};
8545
+ for (const [key, type] of Object.entries(schema)) {
8546
+ const value = options[key];
8547
+ if (value === void 0) continue;
8548
+ validateOptionType(key, value, type);
8549
+ next[key] = value;
8550
+ }
8551
+ return next;
8552
+ }
8553
+ function rejectServerOwnedAddOptions(options) {
8554
+ if ("output" in options) throw new CapletsError("REQUEST_INVALID", "Remote add output is not supported remotely; the server owns destinationRoot and output path selection");
8555
+ for (const key of ["destinationRoot", "print"]) if (key in options) throw new CapletsError("REQUEST_INVALID", `Remote add ${key} is not supported remotely; the server owns destinationRoot and print behavior`);
8556
+ }
8557
+ function validateOptionType(key, value, type) {
8558
+ if (type === "string" && typeof value !== "string") throw new CapletsError("REQUEST_INVALID", `add.options.${key} must be a string`);
8559
+ if (type === "boolean" && typeof value !== "boolean") throw new CapletsError("REQUEST_INVALID", `add.options.${key} must be a boolean`);
8560
+ if (type === "string-array") {
8561
+ if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) throw new CapletsError("REQUEST_INVALID", `add.options.${key} must be an array of strings`);
8562
+ }
8563
+ }
8564
+ function optionalBoolean(args, key) {
8565
+ const value = args[key];
8566
+ if (value === void 0) return;
8567
+ if (typeof value !== "boolean") throw new CapletsError("REQUEST_INVALID", `${key} must be a boolean`);
8568
+ return value;
8569
+ }
8570
+ function optionalStringArray(args, key) {
8571
+ const value = args[key];
8572
+ if (value === void 0) return;
8573
+ if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) throw new CapletsError("REQUEST_INVALID", `${key} must be an array of strings`);
8574
+ return value;
8575
+ }
8576
+ function nextAction(details) {
8577
+ if (details && typeof details === "object" && "nextAction" in details) {
8578
+ const value = details.nextAction;
8579
+ return typeof value === "string" ? value : void 0;
8580
+ }
8581
+ }
8582
+ function redactControlErrorMessage(message) {
8583
+ return message.replace(/(["'])(authorization|(?:access[_-]?)?token|refresh(?:[_-]?token)?|password|client[_-]?secret|clientsecret|api[-_]?key|apikey|secret|credential|code)\1\s*:\s*(["'])(?:\\.|[^\\])*?\3/giu, "$1$2$1:$3[REDACTED]$3").replace(/\b(authorization\s*:\s*(?:basic|bearer)\s+)[^\s,;]+/giu, "$1[REDACTED]").replace(/\b((?:access[_-]?)?token|refresh(?:[_-]?token)?|password|client[_-]?secret|clientsecret|api[-_]?key|apikey|secret|credential|code)(\s*[=:]\s*)[^\s,;]+/giu, "$1$2[REDACTED]");
8584
+ }
8585
+ //#endregion
8586
+ //#region src/remote-control/auth-flow.ts
8587
+ const DEFAULT_AUTH_FLOW_TTL_MS = 600 * 1e3;
8588
+ var RemoteAuthFlowStore = class {
8589
+ options;
8590
+ flows = /* @__PURE__ */ new Map();
8591
+ constructor(options = {}) {
8592
+ this.options = options;
8593
+ }
8594
+ create(flow, id = randomUUID()) {
8595
+ this.pruneExpired();
8596
+ const created = {
8597
+ id,
8598
+ createdAt: this.now(),
8599
+ ...flow
8600
+ };
8601
+ this.flows.set(created.id, created);
8602
+ return { ...created };
8603
+ }
8604
+ get(id) {
8605
+ this.pruneExpired();
8606
+ const flow = this.flows.get(id);
8607
+ if (flow && this.isExpired(flow)) {
8608
+ this.flows.delete(id);
8609
+ return;
8610
+ }
8611
+ return flow;
8612
+ }
8613
+ delete(id) {
8614
+ this.flows.delete(id);
8615
+ }
8616
+ pruneExpired() {
8617
+ for (const [id, flow] of this.flows) if (this.isExpired(flow)) this.flows.delete(id);
8618
+ }
8619
+ isExpired(flow) {
8620
+ return this.now() - flow.createdAt > (this.options.ttlMs ?? DEFAULT_AUTH_FLOW_TTL_MS);
8621
+ }
8622
+ now() {
8623
+ return this.options.now?.() ?? Date.now();
8624
+ }
8625
+ };
8626
+ //#endregion
8312
8627
  //#region src/serve/http.ts
8313
8628
  function createHttpServeApp(options, engine, io = {}) {
8314
8629
  const app = new Hono();
8315
8630
  const sessions = /* @__PURE__ */ new Map();
8316
8631
  const writeErr = io.writeErr ?? process.stderr.write.bind(process.stderr);
8632
+ const paths = servicePaths(options.path);
8633
+ const authFlowStore = io.authFlowStore ?? new RemoteAuthFlowStore();
8317
8634
  app.use("*", logger((message, ...rest) => {
8318
8635
  writeErr(`${[message, ...rest].join(" ")}\n`);
8319
8636
  }));
8320
- app.get("/", (c) => c.json({
8637
+ app.get(paths.base, (c) => c.json({
8321
8638
  name: "caplets",
8322
8639
  transport: "http",
8323
- mcp: options.path,
8324
- health: "/healthz",
8640
+ base: paths.base,
8641
+ mcp: paths.mcp,
8642
+ control: paths.control,
8643
+ health: paths.health,
8325
8644
  auth: {
8326
8645
  type: "basic",
8327
8646
  enabled: options.auth.enabled
8328
8647
  }
8329
8648
  }));
8330
- app.get("/healthz", (c) => c.json({
8649
+ app.get(paths.health, (c) => c.json({
8331
8650
  status: "ok",
8332
8651
  transport: "http",
8333
- mcpPath: options.path
8652
+ base: paths.base,
8653
+ mcpPath: paths.mcp,
8654
+ controlPath: paths.control,
8655
+ healthPath: paths.health
8334
8656
  }));
8335
- app.all(options.path, basicAuth(options.auth), async (c) => {
8657
+ app.all(paths.mcp, basicAuth(options.auth), async (c) => {
8336
8658
  const sessionId = c.req.header("mcp-session-id");
8337
8659
  if (sessionId) {
8338
8660
  const existing = sessions.get(sessionId);
@@ -8363,6 +8685,36 @@ function createHttpServeApp(options, engine, io = {}) {
8363
8685
  sessions.set(nextSessionId, session);
8364
8686
  return session.transport.handleRequest(c);
8365
8687
  });
8688
+ app.post(paths.control, basicAuth(options.auth), async (c) => {
8689
+ let request;
8690
+ try {
8691
+ const parsed = await c.req.json();
8692
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new CapletsError("REQUEST_INVALID", "Control request JSON must be an object");
8693
+ request = parsed;
8694
+ } catch (error) {
8695
+ const safe = toSafeError(error instanceof CapletsError ? error : new CapletsError("REQUEST_INVALID", "Control request body must be valid JSON", error), "REQUEST_INVALID");
8696
+ return c.json({
8697
+ ok: false,
8698
+ error: {
8699
+ code: safe.code,
8700
+ message: safe.message
8701
+ }
8702
+ });
8703
+ }
8704
+ return c.json(await dispatchRemoteCliRequest(request, controlContext(io, writeErr, authFlowStore, c.req.url, paths.control, options.trustProxy, (name) => c.req.header(name))));
8705
+ });
8706
+ app.get(routePath(paths.control, "auth/callback/:flowId"), async (c) => {
8707
+ const flowId = c.req.param("flowId");
8708
+ const result = await dispatchRemoteCliRequest({
8709
+ command: "auth_login_complete",
8710
+ arguments: {
8711
+ flowId,
8712
+ callbackUrl: c.req.url
8713
+ }
8714
+ }, controlContext(io, writeErr, authFlowStore, c.req.url, paths.control, options.trustProxy, (name) => c.req.header(name)));
8715
+ if (!result.ok) writeErr(`Caplets authentication failed for flow ${flowId}: ${result.error.message}\n`);
8716
+ return result.ok ? c.text("Caplets authentication complete. You can return to your terminal.") : c.text("Caplets authentication failed. Check server logs for details.", 400);
8717
+ });
8366
8718
  app.notFound((c) => c.json({ error: "not_found" }, 404));
8367
8719
  app.closeCapletsSessions = async () => {
8368
8720
  await Promise.allSettled([...sessions.values()].map(async (session) => {
@@ -8373,19 +8725,66 @@ function createHttpServeApp(options, engine, io = {}) {
8373
8725
  if (options.warnUnauthenticatedNetwork) writeErr(`Warning: Caplets MCP HTTP server is listening on ${options.host} without authentication.\n`);
8374
8726
  return app;
8375
8727
  }
8728
+ function controlContext(io, writeErr, authFlowStore, requestUrl, controlPath, trustProxy, header) {
8729
+ return {
8730
+ ...io.control,
8731
+ projectCapletsRoot: io.control?.projectCapletsRoot ?? resolveProjectCapletsRoot(),
8732
+ authFlowStore,
8733
+ controlCallbackBaseUrl: new URL(controlPath, publicRequestOrigin(requestUrl, trustProxy, header)).toString(),
8734
+ writeErr
8735
+ };
8736
+ }
8737
+ function publicRequestOrigin(requestUrl, trustProxy, header) {
8738
+ const url = new URL(requestUrl);
8739
+ if (!trustProxy) return `${url.protocol.slice(0, -1)}://${header("host") ?? url.host}`;
8740
+ const forwardedProto = firstForwardedValue(header("x-forwarded-proto"));
8741
+ const forwardedHost = firstForwardedValue(header("x-forwarded-host"));
8742
+ return `${forwardedProto === "http" || forwardedProto === "https" ? forwardedProto : url.protocol.slice(0, -1)}://${forwardedHost ?? header("host") ?? url.host}`;
8743
+ }
8744
+ function firstForwardedValue(value) {
8745
+ return value?.split(",", 1)[0]?.trim() || void 0;
8746
+ }
8376
8747
  async function serveHttp(options, engineOptions = {}, writeErr = (value) => process.stderr.write(value)) {
8377
8748
  const engine = new CapletsEngine(engineOptions);
8378
- const app = createHttpServeApp(options, engine, { writeErr });
8749
+ const app = createHttpServeApp(options, engine, {
8750
+ writeErr,
8751
+ control: {
8752
+ ...engineOptions,
8753
+ projectCapletsRoot: projectCapletsRootForEngineOptions(engineOptions)
8754
+ }
8755
+ });
8756
+ const paths = servicePaths(options.path);
8757
+ const origin = `http://${formatHost(options.host)}:${options.port}`;
8758
+ const baseUrl = `${origin}${paths.base === "/" ? "" : paths.base}`;
8379
8759
  installHttpSignalHandlers(serve({
8380
8760
  fetch: app.fetch,
8381
8761
  hostname: options.host,
8382
8762
  port: options.port
8383
8763
  }, () => {
8384
- writeErr(`Caplets MCP HTTP server listening on http://${formatHost(options.host)}:${options.port}${options.path}\n`);
8385
- writeErr(`Health check: http://${formatHost(options.host)}:${options.port}/healthz\n`);
8764
+ writeErr(`Caplets HTTP service listening on ${baseUrl}\n`);
8765
+ writeErr(`MCP endpoint: ${origin}${paths.mcp}\n`);
8766
+ writeErr(`Control endpoint: ${origin}${paths.control}\n`);
8767
+ writeErr(`Health check: ${origin}${paths.health}\n`);
8386
8768
  writeErr(`Basic Auth: ${options.auth.enabled ? `enabled (user: ${options.auth.user})` : "disabled"}\n`);
8387
8769
  }), app, engine, writeErr);
8388
8770
  }
8771
+ function projectCapletsRootForEngineOptions(engineOptions) {
8772
+ return engineOptions.projectConfigPath ? resolveProjectCapletsRootForConfigPath(engineOptions.projectConfigPath) : resolveProjectCapletsRoot();
8773
+ }
8774
+ function resolveProjectCapletsRootForConfigPath(projectConfigPath) {
8775
+ return dirname(projectConfigPath);
8776
+ }
8777
+ function routePath(base, path) {
8778
+ return base === "/" ? `/${path}` : `${base}/${path}`;
8779
+ }
8780
+ function servicePaths(base) {
8781
+ return {
8782
+ base,
8783
+ mcp: routePath(base, "mcp"),
8784
+ control: routePath(base, "control"),
8785
+ health: routePath(base, "healthz")
8786
+ };
8787
+ }
8389
8788
  async function createHttpSession(engine, sessionId, options, onClose) {
8390
8789
  const transport = new StreamableHTTPTransport({
8391
8790
  sessionIdGenerator: () => sessionId,
@@ -8465,7 +8864,8 @@ const HTTP_ONLY_OPTIONS = [
8465
8864
  "path",
8466
8865
  "user",
8467
8866
  "password",
8468
- "allowUnauthenticatedHttp"
8867
+ "allowUnauthenticatedHttp",
8868
+ "trustProxy"
8469
8869
  ];
8470
8870
  function resolveServeOptions(raw, env = process.env) {
8471
8871
  const transport = parseTransport(raw.transport ?? "stdio");
@@ -8474,9 +8874,10 @@ function resolveServeOptions(raw, env = process.env) {
8474
8874
  if (invalid.length > 0) throw new CapletsError("REQUEST_INVALID", `${invalid.map((key) => `--${key}`).join(", ")} ${invalid.length === 1 ? "is" : "are"} only valid with --transport http`);
8475
8875
  return { transport };
8476
8876
  }
8477
- const host = nonEmpty(raw.host, "--host") ?? "127.0.0.1";
8478
- const port = parsePort(raw.port ?? 5387);
8479
- const path = normalizeHttpPath(raw.path ?? "/mcp");
8877
+ const serverUrl = env.CAPLETS_SERVER_URL ? parseServeServerUrl(nonEmpty(env.CAPLETS_SERVER_URL, "CAPLETS_SERVER_URL")) : void 0;
8878
+ const host = nonEmpty(raw.host, "--host") ?? serverUrlHost(serverUrl) ?? "127.0.0.1";
8879
+ const port = parsePort(raw.port ?? (serverUrl?.port ? Number(serverUrl.port) : 5387));
8880
+ const path = normalizeHttpPath(raw.path ?? serverUrl?.pathname ?? "/");
8480
8881
  const userWasExplicit = raw.user !== void 0 || hasEnv(env.CAPLETS_SERVER_USER);
8481
8882
  const user = nonEmpty(raw.user, "--user") ?? nonEmpty(env.CAPLETS_SERVER_USER, "CAPLETS_SERVER_USER") ?? "caplets";
8482
8883
  const password = nonEmpty(raw.password, "--password") ?? nonEmpty(env.CAPLETS_SERVER_PASSWORD, "CAPLETS_SERVER_PASSWORD");
@@ -8498,13 +8899,22 @@ function resolveServeOptions(raw, env = process.env) {
8498
8899
  path,
8499
8900
  auth,
8500
8901
  warnUnauthenticatedNetwork: !loopback && !auth.enabled,
8501
- loopback
8902
+ loopback,
8903
+ trustProxy: raw.trustProxy === true
8502
8904
  };
8503
8905
  }
8504
8906
  function isLoopbackHost(host) {
8505
8907
  const normalized = host.toLocaleLowerCase();
8506
8908
  return normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1";
8507
8909
  }
8910
+ function parseServeServerUrl(value) {
8911
+ try {
8912
+ return parseServerBaseUrl(value);
8913
+ } catch (error) {
8914
+ if (error instanceof CapletsError && error.message.includes("must use https except loopback development URLs")) throw new CapletsError("REQUEST_INVALID", "CAPLETS_SERVER_URL must use https except loopback development URLs; use --host, --port, and --path separately for non-loopback HTTP bind addresses.");
8915
+ throw error;
8916
+ }
8917
+ }
8508
8918
  function parseTransport(value) {
8509
8919
  if (value === "stdio" || value === "http") return value;
8510
8920
  throw new CapletsError("REQUEST_INVALID", `Expected --transport to be stdio or http, got ${value}`);
@@ -8519,6 +8929,9 @@ function normalizeHttpPath(value) {
8519
8929
  if (value.includes("?") || value.includes("#")) throw new CapletsError("REQUEST_INVALID", "HTTP --path must not include a query string or fragment");
8520
8930
  return value === "/" ? value : value.replace(/\/+$/u, "");
8521
8931
  }
8932
+ function serverUrlHost(url) {
8933
+ return url?.hostname.replace(/^\[(.*)\]$/u, "$1");
8934
+ }
8522
8935
  function nonEmpty(value, label) {
8523
8936
  if (value === void 0) return;
8524
8937
  const trimmed = value.trim();
@@ -8653,9 +9066,14 @@ async function runCli(args, io = {}) {
8653
9066
  throw error;
8654
9067
  }
8655
9068
  }
9069
+ function normalizeCompletionWords(words) {
9070
+ return words.map((word) => word === "__CAPLETS_TRAILING_SPACE__" ? "" : word);
9071
+ }
8656
9072
  function createProgram(io = {}) {
8657
9073
  const writeOut = io.writeOut ?? ((value) => process.stdout.write(value));
8658
9074
  const writeErr = io.writeErr ?? ((value) => process.stderr.write(value));
9075
+ const env = io.env ?? process.env;
9076
+ const currentConfigPath = () => envConfigPath(env);
8659
9077
  const setExitCode = io.setExitCode ?? ((code) => {
8660
9078
  process.exitCode = code;
8661
9079
  });
@@ -8665,42 +9083,98 @@ function createProgram(io = {}) {
8665
9083
  writeErr,
8666
9084
  outputError: (value, write) => write(value)
8667
9085
  });
8668
- program.command("serve").description("Serve configured Caplets as an MCP server.").option("--transport <transport>", "server transport: stdio or http").option("--host <host>", "HTTP bind host").option("--port <port>", "HTTP bind port").option("--path <path>", "HTTP MCP endpoint path").option("--user <user>", "HTTP Basic Auth username").option("--password <password>", "HTTP Basic Auth password").option("--allow-unauthenticated-http", "allow unauthenticated HTTP serving on non-loopback hosts").action(async (options) => {
9086
+ program.command(cliCommands.completion).description("Print a shell completion script.").argument("<shell>", "completion shell: bash, zsh, fish, powershell, or cmd").action((shell) => {
9087
+ if (!completionShells.includes(shell)) throw new CapletsError("REQUEST_INVALID", "completion shell must be bash, zsh, fish, powershell, or cmd");
9088
+ writeOut(completionScript(shell));
9089
+ });
9090
+ program.command(cliCommands.completeHidden, { hidden: true }).description("Internal shell completion endpoint.").option("--shell <shell>", "completion shell").allowUnknownOption(true).argument("[words...]", "words to complete").action(async (words, options) => {
9091
+ const shell = completionShells.includes(options.shell) ? options.shell : "bash";
9092
+ const remote = remoteClientForCli(io);
9093
+ const configPath = currentConfigPath();
9094
+ const completionWords = normalizeCompletionWords(words);
9095
+ let suggestions = [];
9096
+ try {
9097
+ suggestions = remote ? await remote.request("complete_cli", {
9098
+ shell,
9099
+ words: completionWords
9100
+ }) : await completeCliWords(completionWords, configPath ? { configPath } : {});
9101
+ } catch {
9102
+ suggestions = [];
9103
+ }
9104
+ if (suggestions.length > 0) writeOut(`${suggestions.join("\n")}\n`);
9105
+ });
9106
+ program.command(cliCommands.serve).description("Serve configured Caplets as an MCP server.").option("--transport <transport>", "server transport: stdio or http").option("--host <host>", "HTTP bind host").option("--port <port>", "HTTP bind port").option("--path <path>", "HTTP service base path").option("--user <user>", "HTTP Basic Auth username").option("--password <password>", "HTTP Basic Auth password").option("--allow-unauthenticated-http", "allow unauthenticated HTTP serving on non-loopback hosts").option("--trust-proxy", "trust X-Forwarded-* headers from a reverse proxy").action(async (options) => {
8669
9107
  const resolved = resolveServeOptions(options);
8670
- const configPath = envConfigPath();
9108
+ const configPath = currentConfigPath();
8671
9109
  await (io.serve ?? ((serveOptions) => serveResolvedCaplets(serveOptions, {
8672
9110
  ...configPath ? { configPath } : {},
8673
9111
  ...io.authDir ? { authDir: io.authDir } : {}
8674
9112
  }, writeErr)))(resolved);
8675
9113
  });
8676
- program.command("init").description("Create a starter Caplets config file.").option("--force", "overwrite an existing config file").action((options) => {
8677
- const configPath = envConfigPath();
9114
+ program.command(cliCommands.init).description("Create a starter Caplets config file.").option("--force", "overwrite an existing config file").action(async (options) => {
9115
+ const remote = remoteClientForCli(io);
9116
+ if (remote) {
9117
+ writeOut(`Created remote Caplets config at ${(await remote.request("init", { force: Boolean(options.force) })).path}\n`);
9118
+ return;
9119
+ }
9120
+ const configPath = currentConfigPath();
8678
9121
  writeOut(`Created Caplets config at ${initConfig({
8679
9122
  ...configPath ? { path: configPath } : {},
8680
9123
  force: Boolean(options.force)
8681
9124
  })}\n`);
8682
9125
  });
8683
- program.command("list").description("List configured Caplets.").option("--all", "include disabled Caplets").option("--json", "print JSON output").option("--format <format>", "output format: plain, markdown, md, or json", parseOutputFormat).action((options) => {
8684
- const rows = listCaplets(loadConfigWithSources(envConfigPath()), { includeDisabled: Boolean(options.all) });
9126
+ program.command(cliCommands.list).description("List configured Caplets.").option("--all", "include disabled Caplets").option("--json", "print JSON output").option("--format <format>", "output format: plain, markdown, md, or json", parseOutputFormat).action(async (options) => {
9127
+ const includeDisabled = Boolean(options.all);
9128
+ const remote = remoteClientForCli(io);
9129
+ if (remote) {
9130
+ const rows = await remote.request("list", { includeDisabled });
9131
+ if (options.json || options.format === "json") {
9132
+ writeOut(`${JSON.stringify(rows, null, 2)}\n`);
9133
+ return;
9134
+ }
9135
+ writeOut(formatCapletList(rows, options.format ?? "plain"));
9136
+ return;
9137
+ }
9138
+ const rows = listCaplets(loadConfigWithSources(currentConfigPath()), { includeDisabled });
8685
9139
  if (options.json || options.format === "json") {
8686
9140
  writeOut(`${JSON.stringify(rows, null, 2)}\n`);
8687
9141
  return;
8688
9142
  }
8689
9143
  writeOut(formatCapletList(rows, options.format ?? "plain"));
8690
9144
  });
8691
- program.command("install").description("Install Caplets from a repo's caplets directory.").argument("<repo>", "local repo path, Git URL, or GitHub owner/repo").argument("[caplets...]", "optional Caplet IDs to install").option("-g, --global", "install to the user Caplets root").option("--force", "overwrite installed Caplets").action((repo, capletIds, options) => {
9145
+ program.command(cliCommands.install).description("Install Caplets from a repo's caplets directory.").argument("<repo>", "local repo path, Git URL, or GitHub owner/repo").argument("[caplets...]", "optional Caplet IDs to install").option("-g, --global", "install to the user Caplets root").option("--force", "overwrite installed Caplets").action(async (repo, capletIds, options) => {
9146
+ const remote = remoteClientForCli(io);
9147
+ if (remote) {
9148
+ if (options.global) writeErr("Warning: --global is not supported in remote mode; the server controls the installation destination.\n");
9149
+ const result = await remote.request("install", {
9150
+ repo,
9151
+ capletIds,
9152
+ force: Boolean(options.force)
9153
+ });
9154
+ for (const caplet of result.installed) writeOut(`Installed ${caplet.id} to remote ${caplet.destination}\n`);
9155
+ return;
9156
+ }
8692
9157
  const result = installCaplets(repo, {
8693
9158
  capletIds,
8694
9159
  force: Boolean(options.force),
8695
- destinationRoot: options.global ? resolveCapletsRoot(resolveConfigPath(envConfigPath())) : resolveProjectCapletsRoot()
9160
+ destinationRoot: options.global ? resolveCapletsRoot(resolveConfigPath(currentConfigPath())) : resolveProjectCapletsRoot()
8696
9161
  });
8697
9162
  for (const caplet of result.installed) writeOut(`Installed ${caplet.id} to ${caplet.destination}\n`);
8698
9163
  });
8699
- const add = program.command("add").description("Add generated Caplet files.");
8700
- add.command("cli").description("Add 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("-g, --global", "write to the user Caplets root").option("--print", "print generated Caplet text without writing a file").option("--output <path>", "output path").option("--force", "overwrite an existing destination file").action((id, options) => {
9164
+ const add = program.command(cliCommands.add).description("Add generated Caplet files.");
9165
+ add.command("cli").description("Add 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("-g, --global", "write to the user Caplets root").option("--print", "print generated Caplet text without writing a file").option("--output <path>", "output path").option("--force", "overwrite an existing destination file").action(async (id, options) => {
9166
+ const remote = remoteClientForCli(io);
9167
+ if (remote) {
9168
+ writeAddResult(writeOut, "CLI", await remote.request("add", {
9169
+ kind: "cli",
9170
+ id,
9171
+ options: remoteAddOptions(options)
9172
+ }));
9173
+ return;
9174
+ }
8701
9175
  const result = addCliCaplet(id, {
8702
9176
  ...options,
8703
- destinationRoot: options.global ? resolveCapletsRoot(resolveConfigPath(envConfigPath())) : resolveProjectCapletsRoot()
9177
+ destinationRoot: options.global ? resolveCapletsRoot(resolveConfigPath(currentConfigPath())) : resolveProjectCapletsRoot()
8704
9178
  });
8705
9179
  if (result.path) {
8706
9180
  writeOut(`Wrote CLI Caplet to ${result.path}\n`);
@@ -8708,58 +9182,100 @@ function createProgram(io = {}) {
8708
9182
  }
8709
9183
  writeOut(result.text);
8710
9184
  });
8711
- add.command("mcp").description("Add an MCP backend Caplet.").argument("<id>", "Caplet ID/display seed").option("--command <name>", "stdio command").option("--arg <value>", "stdio command argument", collect, []).option("--cwd <path>", "stdio working directory").option("--env <KEY=VALUE>", "stdio environment variable", collect, []).option("--url <url>", "remote MCP server URL").option("--transport <transport>", "remote transport: http or sse").option("--token-env <ENV>", "bearer token environment variable reference").option("-g, --global", "write to the user Caplets root").option("--print", "print generated Caplet text without writing a file").option("--output <path>", "output path").option("--force", "overwrite an existing destination file").action((id, options) => {
9185
+ add.command("mcp").description("Add an MCP backend Caplet.").argument("<id>", "Caplet ID/display seed").option("--command <name>", "stdio command").option("--arg <value>", "stdio command argument", collect, []).option("--cwd <path>", "stdio working directory").option("--env <KEY=VALUE>", "stdio environment variable", collect, []).option("--url <url>", "remote MCP server URL").option("--transport <transport>", "remote transport: http or sse").option("--token-env <ENV>", "bearer token environment variable reference").option("-g, --global", "write to the user Caplets root").option("--print", "print generated Caplet text without writing a file").option("--output <path>", "output path").option("--force", "overwrite an existing destination file").action(async (id, options) => {
9186
+ const remote = remoteClientForCli(io);
9187
+ if (remote) {
9188
+ writeAddResult(writeOut, "MCP", await remote.request("add", {
9189
+ kind: "mcp",
9190
+ id,
9191
+ options: remoteAddOptions(options)
9192
+ }));
9193
+ return;
9194
+ }
8712
9195
  writeAddResult(writeOut, "MCP", addMcpCaplet(id, {
8713
9196
  ...options,
8714
- destinationRoot: addDestinationRoot(options)
9197
+ destinationRoot: addDestinationRoot(options, currentConfigPath())
8715
9198
  }));
8716
9199
  });
8717
- add.command("openapi").description("Add an OpenAPI backend Caplet.").argument("<id>", "Caplet ID/display seed").option("--spec <path-or-url>", "OpenAPI spec path or URL").option("--base-url <url>", "request base URL override").option("--token-env <ENV>", "bearer token environment variable reference").option("-g, --global", "write to the user Caplets root").option("--print", "print generated Caplet text without writing a file").option("--output <path>", "output path").option("--force", "overwrite an existing destination file").action((id, options) => {
9200
+ add.command("openapi").description("Add an OpenAPI backend Caplet.").argument("<id>", "Caplet ID/display seed").option("--spec <path-or-url>", "OpenAPI spec path or URL").option("--base-url <url>", "request base URL override").option("--token-env <ENV>", "bearer token environment variable reference").option("-g, --global", "write to the user Caplets root").option("--print", "print generated Caplet text without writing a file").option("--output <path>", "output path").option("--force", "overwrite an existing destination file").action(async (id, options) => {
9201
+ const remote = remoteClientForCli(io);
9202
+ if (remote) {
9203
+ writeAddResult(writeOut, "OpenAPI", await remote.request("add", {
9204
+ kind: "openapi",
9205
+ id,
9206
+ options: remoteAddOptions(options)
9207
+ }));
9208
+ return;
9209
+ }
8718
9210
  writeAddResult(writeOut, "OpenAPI", addOpenApiCaplet(id, {
8719
9211
  ...options,
8720
- destinationRoot: addDestinationRoot(options)
9212
+ destinationRoot: addDestinationRoot(options, currentConfigPath())
8721
9213
  }));
8722
9214
  });
8723
- add.command("graphql").description("Add a GraphQL backend Caplet.").argument("<id>", "Caplet ID/display seed").option("--endpoint-url <url>", "GraphQL endpoint URL").option("--schema <path-or-url>", "GraphQL schema path or URL").option("--introspection", "load schema through endpoint introspection").option("--token-env <ENV>", "bearer token environment variable reference").option("-g, --global", "write to the user Caplets root").option("--print", "print generated Caplet text without writing a file").option("--output <path>", "output path").option("--force", "overwrite an existing destination file").action((id, options) => {
9215
+ add.command("graphql").description("Add a GraphQL backend Caplet.").argument("<id>", "Caplet ID/display seed").option("--endpoint-url <url>", "GraphQL endpoint URL").option("--schema <path-or-url>", "GraphQL schema path or URL").option("--introspection", "load schema through endpoint introspection").option("--token-env <ENV>", "bearer token environment variable reference").option("-g, --global", "write to the user Caplets root").option("--print", "print generated Caplet text without writing a file").option("--output <path>", "output path").option("--force", "overwrite an existing destination file").action(async (id, options) => {
9216
+ const remote = remoteClientForCli(io);
9217
+ if (remote) {
9218
+ writeAddResult(writeOut, "GraphQL", await remote.request("add", {
9219
+ kind: "graphql",
9220
+ id,
9221
+ options: remoteAddOptions(options)
9222
+ }));
9223
+ return;
9224
+ }
8724
9225
  writeAddResult(writeOut, "GraphQL", addGraphqlCaplet(id, {
8725
9226
  ...options,
8726
- destinationRoot: addDestinationRoot(options)
9227
+ destinationRoot: addDestinationRoot(options, currentConfigPath())
8727
9228
  }));
8728
9229
  });
8729
- add.command("http").description("Add an HTTP actions backend Caplet.").argument("<id>", "Caplet ID/display seed").option("--base-url <url>", "HTTP API base URL").option("--action <name:METHOD:/path>", "HTTP action", collect, []).option("--token-env <ENV>", "bearer token environment variable reference").option("-g, --global", "write to the user Caplets root").option("--print", "print generated Caplet text without writing a file").option("--output <path>", "output path").option("--force", "overwrite an existing destination file").action((id, options) => {
9230
+ add.command("http").description("Add an HTTP actions backend Caplet.").argument("<id>", "Caplet ID/display seed").option("--base-url <url>", "HTTP API base URL").option("--action <name:METHOD:/path>", "HTTP action", collect, []).option("--token-env <ENV>", "bearer token environment variable reference").option("-g, --global", "write to the user Caplets root").option("--print", "print generated Caplet text without writing a file").option("--output <path>", "output path").option("--force", "overwrite an existing destination file").action(async (id, options) => {
9231
+ const remote = remoteClientForCli(io);
9232
+ if (remote) {
9233
+ writeAddResult(writeOut, "HTTP", await remote.request("add", {
9234
+ kind: "http",
9235
+ id,
9236
+ options: remoteAddOptions(options)
9237
+ }));
9238
+ return;
9239
+ }
8730
9240
  writeAddResult(writeOut, "HTTP", addHttpCaplet(id, {
8731
9241
  ...options,
8732
- destinationRoot: addDestinationRoot(options)
9242
+ destinationRoot: addDestinationRoot(options, currentConfigPath())
8733
9243
  }));
8734
9244
  });
8735
- program.command("get-caplet").description("Print a configured Caplet card.").argument("<caplet>", "configured Caplet ID").option("--format <format>", "output format: markdown, md, plain, or json", parseOutputFormat).action(async (caplet, options) => {
9245
+ program.command(cliCommands.getCaplet).description("Print a configured Caplet card.").argument("<caplet>", "configured Caplet ID").option("--format <format>", "output format: markdown, md, plain, or json", parseOutputFormat).action(async (caplet, options) => {
8736
9246
  await executeOperation(caplet, { operation: "get_caplet" }, {
8737
9247
  writeOut,
8738
9248
  writeErr,
8739
9249
  setExitCode,
8740
9250
  authDir: io.authDir,
9251
+ env,
9252
+ remote: remoteClientForCli(io),
8741
9253
  format: options.format
8742
9254
  });
8743
9255
  });
8744
- program.command("check-backend").description("Check backend availability for a configured Caplet.").argument("<caplet>", "configured Caplet ID").option("--format <format>", "output format: markdown, md, plain, or json", parseOutputFormat).action(async (caplet, options) => {
9256
+ program.command(cliCommands.checkBackend).description("Check backend availability for a configured Caplet.").argument("<caplet>", "configured Caplet ID").option("--format <format>", "output format: markdown, md, plain, or json", parseOutputFormat).action(async (caplet, options) => {
8745
9257
  await executeOperation(caplet, { operation: "check_backend" }, {
8746
9258
  writeOut,
8747
9259
  writeErr,
8748
9260
  setExitCode,
8749
9261
  authDir: io.authDir,
9262
+ env,
9263
+ remote: remoteClientForCli(io),
8750
9264
  format: options.format
8751
9265
  });
8752
9266
  });
8753
- program.command("list-tools").description("List downstream tools for a configured Caplet.").argument("<caplet>", "configured Caplet ID").option("--format <format>", "output format: markdown, md, plain, or json", parseOutputFormat).action(async (caplet, options) => {
9267
+ program.command(cliCommands.listTools).description("List downstream tools for a configured Caplet.").argument("<caplet>", "configured Caplet ID").option("--format <format>", "output format: markdown, md, plain, or json", parseOutputFormat).action(async (caplet, options) => {
8754
9268
  await executeOperation(caplet, { operation: "list_tools" }, {
8755
9269
  writeOut,
8756
9270
  writeErr,
8757
9271
  setExitCode,
8758
9272
  authDir: io.authDir,
9273
+ env,
9274
+ remote: remoteClientForCli(io),
8759
9275
  format: options.format
8760
9276
  });
8761
9277
  });
8762
- program.command("search-tools").description("Search downstream tools for a configured Caplet.").argument("<caplet>", "configured Caplet ID").argument("<query>", "search query").option("--limit <n>", "maximum number of tools to return", parsePositiveInteger).option("--format <format>", "output format: markdown, md, plain, or json", parseOutputFormat).action(async (caplet, query, options) => {
9278
+ program.command(cliCommands.searchTools).description("Search downstream tools for a configured Caplet.").argument("<caplet>", "configured Caplet ID").argument("<query>", "search query").option("--limit <n>", "maximum number of tools to return", parsePositiveInteger).option("--format <format>", "output format: markdown, md, plain, or json", parseOutputFormat).action(async (caplet, query, options) => {
8763
9279
  await executeOperation(caplet, options.limit === void 0 ? {
8764
9280
  operation: "search_tools",
8765
9281
  query
@@ -8772,10 +9288,12 @@ function createProgram(io = {}) {
8772
9288
  writeErr,
8773
9289
  setExitCode,
8774
9290
  authDir: io.authDir,
9291
+ env,
9292
+ remote: remoteClientForCli(io),
8775
9293
  format: options.format
8776
9294
  });
8777
9295
  });
8778
- program.command("get-tool").description("Print one downstream tool schema.").argument("<caplet.tool>", "qualified target, split on the first dot").option("--format <format>", "output format: markdown, md, plain, or json", parseOutputFormat).action(async (target, options) => {
9296
+ program.command(cliCommands.getTool).description("Print one downstream tool schema.").argument("<caplet.tool>", "qualified target, split on the first dot").option("--format <format>", "output format: markdown, md, plain, or json", parseOutputFormat).action(async (target, options) => {
8779
9297
  const { caplet, tool } = parseQualifiedTarget(target);
8780
9298
  await executeOperation(caplet, {
8781
9299
  operation: "get_tool",
@@ -8785,10 +9303,12 @@ function createProgram(io = {}) {
8785
9303
  writeErr,
8786
9304
  setExitCode,
8787
9305
  authDir: io.authDir,
9306
+ env,
9307
+ remote: remoteClientForCli(io),
8788
9308
  format: options.format
8789
9309
  });
8790
9310
  });
8791
- program.command("call-tool").description("Call one downstream tool.").argument("<caplet.tool>", "qualified target, split on the first dot").option("--args <json-object>", "JSON object of downstream tool arguments").option("--field <path>", "project a field from structured output", collect, []).option("--format <format>", "output format: markdown, md, plain, or json", parseOutputFormat).action(async (target, options) => {
9311
+ program.command(cliCommands.callTool).description("Call one downstream tool.").argument("<caplet.tool>", "qualified target, split on the first dot").option("--args <json-object>", "JSON object of downstream tool arguments").option("--field <path>", "project a field from structured output", collect, []).option("--format <format>", "output format: markdown, md, plain, or json", parseOutputFormat).action(async (target, options) => {
8792
9312
  const { caplet, tool } = parseQualifiedTarget(target);
8793
9313
  await executeOperation(caplet, {
8794
9314
  operation: "call_tool",
@@ -8800,24 +9320,150 @@ function createProgram(io = {}) {
8800
9320
  writeErr,
8801
9321
  setExitCode,
8802
9322
  authDir: io.authDir,
9323
+ env,
9324
+ remote: remoteClientForCli(io),
9325
+ format: options.format
9326
+ });
9327
+ });
9328
+ program.command(cliCommands.listResources).description("List MCP resources for a configured MCP Caplet.").argument("<caplet>").option("--limit <n>", "maximum number of resources to return", parsePositiveInteger).option("--format <format>", "output format: markdown, md, plain, or json", parseOutputFormat).action(async (caplet, options) => executeOperation(caplet, options.limit === void 0 ? { operation: "list_resources" } : {
9329
+ operation: "list_resources",
9330
+ limit: options.limit
9331
+ }, {
9332
+ writeOut,
9333
+ writeErr,
9334
+ setExitCode,
9335
+ authDir: io.authDir,
9336
+ env,
9337
+ remote: remoteClientForCli(io),
9338
+ format: options.format
9339
+ }));
9340
+ program.command(cliCommands.searchResources).description("Search MCP resources and resource templates for a configured MCP Caplet.").argument("<caplet>").argument("<query>").option("--limit <n>", "maximum number of matches to return", parsePositiveInteger).option("--format <format>", "output format: markdown, md, plain, or json", parseOutputFormat).action(async (caplet, query, options) => executeOperation(caplet, options.limit === void 0 ? {
9341
+ operation: "search_resources",
9342
+ query
9343
+ } : {
9344
+ operation: "search_resources",
9345
+ query,
9346
+ limit: options.limit
9347
+ }, {
9348
+ writeOut,
9349
+ writeErr,
9350
+ setExitCode,
9351
+ authDir: io.authDir,
9352
+ env,
9353
+ remote: remoteClientForCli(io),
9354
+ format: options.format
9355
+ }));
9356
+ program.command(cliCommands.listResourceTemplates).description("List MCP resource templates for a configured MCP Caplet.").argument("<caplet>").option("--limit <n>", "maximum number of templates to return", parsePositiveInteger).option("--format <format>", "output format: markdown, md, plain, or json", parseOutputFormat).action(async (caplet, options) => executeOperation(caplet, options.limit === void 0 ? { operation: "list_resource_templates" } : {
9357
+ operation: "list_resource_templates",
9358
+ limit: options.limit
9359
+ }, {
9360
+ writeOut,
9361
+ writeErr,
9362
+ setExitCode,
9363
+ authDir: io.authDir,
9364
+ env,
9365
+ remote: remoteClientForCli(io),
9366
+ format: options.format
9367
+ }));
9368
+ program.command(cliCommands.readResource).description("Read one MCP resource by URI.").argument("<caplet>").argument("<uri>").option("--format <format>", "output format: markdown, md, plain, or json", parseOutputFormat).action(async (caplet, uri, options) => executeOperation(caplet, {
9369
+ operation: "read_resource",
9370
+ uri
9371
+ }, {
9372
+ writeOut,
9373
+ writeErr,
9374
+ setExitCode,
9375
+ authDir: io.authDir,
9376
+ env,
9377
+ remote: remoteClientForCli(io),
9378
+ format: options.format
9379
+ }));
9380
+ program.command(cliCommands.listPrompts).description("List MCP prompts for a configured MCP Caplet.").argument("<caplet>").option("--limit <n>", "maximum number of prompts to return", parsePositiveInteger).option("--format <format>", "output format: markdown, md, plain, or json", parseOutputFormat).action(async (caplet, options) => executeOperation(caplet, options.limit === void 0 ? { operation: "list_prompts" } : {
9381
+ operation: "list_prompts",
9382
+ limit: options.limit
9383
+ }, {
9384
+ writeOut,
9385
+ writeErr,
9386
+ setExitCode,
9387
+ authDir: io.authDir,
9388
+ env,
9389
+ remote: remoteClientForCli(io),
9390
+ format: options.format
9391
+ }));
9392
+ program.command(cliCommands.searchPrompts).description("Search MCP prompts for a configured MCP Caplet.").argument("<caplet>").argument("<query>").option("--limit <n>", "maximum number of prompts to return", parsePositiveInteger).option("--format <format>", "output format: markdown, md, plain, or json", parseOutputFormat).action(async (caplet, query, options) => executeOperation(caplet, options.limit === void 0 ? {
9393
+ operation: "search_prompts",
9394
+ query
9395
+ } : {
9396
+ operation: "search_prompts",
9397
+ query,
9398
+ limit: options.limit
9399
+ }, {
9400
+ writeOut,
9401
+ writeErr,
9402
+ setExitCode,
9403
+ authDir: io.authDir,
9404
+ env,
9405
+ remote: remoteClientForCli(io),
9406
+ format: options.format
9407
+ }));
9408
+ program.command(cliCommands.getPrompt).description("Get one MCP prompt by name.").argument("<caplet.prompt>", "qualified target, split on the first dot").option("--args <json-object>", "JSON object of prompt arguments").option("--format <format>", "output format: markdown, md, plain, or json", parseOutputFormat).action(async (target, options) => {
9409
+ const { caplet, tool: prompt } = parseQualifiedTarget(target);
9410
+ await executeOperation(caplet, {
9411
+ operation: "get_prompt",
9412
+ prompt,
9413
+ arguments: parseJsonObjectOption(options.args, "get-prompt --args")
9414
+ }, {
9415
+ writeOut,
9416
+ writeErr,
9417
+ setExitCode,
9418
+ authDir: io.authDir,
9419
+ env,
9420
+ remote: remoteClientForCli(io),
8803
9421
  format: options.format
8804
9422
  });
8805
9423
  });
8806
- const config = program.command("config").description("Inspect Caplets config locations.");
9424
+ program.command(cliCommands.complete).description("Complete an MCP prompt or resource-template argument.").argument("<caplet>").requiredOption("--argument <name>", "argument name").option("--value <value>", "argument prefix", "").option("--prompt <name>", "prompt name to complete").option("--resource-template <uri-template>", "resource template URI to complete").option("--format <format>", "output format: markdown, md, plain, or json", parseOutputFormat).action(async (caplet, options) => executeOperation(caplet, {
9425
+ operation: "complete",
9426
+ ref: completionRefFromOptions(options),
9427
+ argument: {
9428
+ name: options.argument,
9429
+ value: options.value
9430
+ }
9431
+ }, {
9432
+ writeOut,
9433
+ writeErr,
9434
+ setExitCode,
9435
+ authDir: io.authDir,
9436
+ env,
9437
+ remote: remoteClientForCli(io),
9438
+ format: options.format
9439
+ }));
9440
+ const config = program.command(cliCommands.config).description("Inspect Caplets config locations.");
8807
9441
  config.command("path").description("Print the effective user config path.").action(() => {
8808
- writeOut(`${resolveConfigPath(envConfigPath())}\n`);
9442
+ writeOut(`${resolveConfigPath(currentConfigPath())}\n`);
8809
9443
  });
8810
9444
  config.command("paths").description("Print resolved Caplets config, root, and auth paths.").option("--json", "print JSON output").option("--format <format>", "output format: plain, markdown, md, or json", parseOutputFormat).action((options) => {
8811
- const paths = resolveCliConfigPaths(envConfigPath(), io.authDir);
9445
+ const paths = resolveCliConfigPaths(currentConfigPath(), io.authDir);
8812
9446
  if (options.json || options.format === "json") {
8813
9447
  writeOut(`${JSON.stringify(paths, null, 2)}\n`);
8814
9448
  return;
8815
9449
  }
8816
9450
  writeOut(formatConfigPaths(paths, options.format ?? "plain"));
8817
9451
  });
8818
- const auth = program.command("auth").description("Manage OAuth credentials for remote servers.");
9452
+ const auth = program.command(cliCommands.auth).description("Manage OAuth credentials for remote servers.");
8819
9453
  auth.command("login").description("Authenticate a configured remote OAuth server.").argument("<server>", "configured server ID").option("--no-open", "print the authorization URL without opening a browser").action(async (serverId, options) => {
8820
- const configPath = envConfigPath();
9454
+ const remote = remoteClientForCli(io);
9455
+ if (remote) {
9456
+ const started = await remote.request("auth_login_start", { server: serverId });
9457
+ if (started.authorizationUrl) {
9458
+ writeOut(`Open this URL to authorize ${serverId}:\n${started.authorizationUrl}\n`);
9459
+ if (options.open !== false) await openBrowser(started.authorizationUrl);
9460
+ writeOut("Complete authentication in your browser. The server callback will store credentials.\n");
9461
+ return;
9462
+ }
9463
+ if (started.authenticated) writeOut(`Authenticated \`${serverId}\`.\n`);
9464
+ return;
9465
+ }
9466
+ const configPath = currentConfigPath();
8821
9467
  await loginAuth(serverId, {
8822
9468
  noOpen: options.open === false,
8823
9469
  writeOut,
@@ -8826,27 +9472,86 @@ function createProgram(io = {}) {
8826
9472
  ...io.authDir ? { authDir: io.authDir } : {}
8827
9473
  });
8828
9474
  });
8829
- auth.command("logout").description("Delete stored OAuth credentials for a server.").argument("<server>", "configured server ID").action((serverId) => {
8830
- const configPath = envConfigPath();
9475
+ auth.command("logout").description("Delete stored OAuth credentials for a server.").argument("<server>", "configured server ID").action(async (serverId) => {
9476
+ const remote = remoteClientForCli(io);
9477
+ if (remote) {
9478
+ writeOut((await remote.request("auth_logout", { server: serverId })).deleted ? `Deleted remote OAuth credentials for \`${serverId}\`.\n` : `No remote OAuth credentials found for \`${serverId}\`.\n`);
9479
+ return;
9480
+ }
9481
+ const configPath = currentConfigPath();
8831
9482
  logoutAuth(serverId, {
8832
9483
  writeOut,
8833
9484
  ...configPath ? { configPath } : {},
8834
9485
  ...io.authDir ? { authDir: io.authDir } : {}
8835
9486
  });
8836
9487
  });
8837
- auth.command("list").description("List servers with stored OAuth credentials.").option("--json", "print JSON output").option("--format <format>", "output format: plain, markdown, md, or json", parseOutputFormat).action((options) => {
8838
- const configPath = envConfigPath();
9488
+ auth.command("list").description("List servers with stored OAuth credentials.").option("--json", "print JSON output").option("--format <format>", "output format: plain, markdown, md, or json", parseOutputFormat).action(async (options) => {
9489
+ const configPath = currentConfigPath();
9490
+ const format = options.json || options.format === "json" ? "json" : options.format ?? "plain";
9491
+ const remote = remoteClientForCli(io);
9492
+ if (remote) {
9493
+ const rows = await remote.request("auth_list", {});
9494
+ if (format === "json") {
9495
+ writeOut(`${JSON.stringify(rows, null, 2)}\n`);
9496
+ return;
9497
+ }
9498
+ writeOut(formatAuthRows(rows, format));
9499
+ return;
9500
+ }
8839
9501
  listAuth({
8840
9502
  writeOut,
8841
- format: options.json || options.format === "json" ? "json" : options.format ?? "plain",
9503
+ format,
8842
9504
  ...configPath ? { configPath } : {},
8843
9505
  ...io.authDir ? { authDir: io.authDir } : {}
8844
9506
  });
8845
9507
  });
8846
9508
  return program;
8847
9509
  }
8848
- function envConfigPath() {
8849
- return process.env.CAPLETS_CONFIG?.trim() || void 0;
9510
+ function envConfigPath(env) {
9511
+ return env.CAPLETS_CONFIG?.trim() || void 0;
9512
+ }
9513
+ function remoteClientForCli(io) {
9514
+ const env = io.env ?? process.env;
9515
+ if (resolveCapletsMode({}, env).mode !== "remote") return;
9516
+ return new RemoteControlClient(resolveCapletsServer(io.fetch ? { fetch: io.fetch } : {}, env));
9517
+ }
9518
+ async function openBrowser(url) {
9519
+ const { spawn } = await import("node:child_process");
9520
+ spawn(process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open", process.platform === "win32" ? [
9521
+ "/c",
9522
+ "start",
9523
+ "",
9524
+ url
9525
+ ] : [url], {
9526
+ stdio: "ignore",
9527
+ detached: true
9528
+ }).unref();
9529
+ }
9530
+ function remoteCommandForOperation(operation) {
9531
+ switch (operation) {
9532
+ case "get_caplet":
9533
+ case "check_backend":
9534
+ case "list_tools":
9535
+ case "search_tools":
9536
+ case "get_tool":
9537
+ case "call_tool":
9538
+ case "list_resources":
9539
+ case "search_resources":
9540
+ case "list_resource_templates":
9541
+ case "read_resource":
9542
+ case "list_prompts":
9543
+ case "search_prompts":
9544
+ case "get_prompt":
9545
+ case "complete": return operation;
9546
+ default: return;
9547
+ }
9548
+ }
9549
+ function remoteAddOptions(options) {
9550
+ const { output, print, global, destinationRoot, ...remoteOptions } = options;
9551
+ if (global) throw new CapletsError("REQUEST_INVALID", "--global is not supported in remote mode; the server controls the add destination.");
9552
+ if (print) throw new CapletsError("REQUEST_INVALID", "--print is not supported in remote mode; the server controls add output.");
9553
+ if (output !== void 0) throw new CapletsError("REQUEST_INVALID", "--output is not supported in remote mode; the server controls the add destination.");
9554
+ return remoteOptions;
8850
9555
  }
8851
9556
  function collect(value, previous) {
8852
9557
  previous.push(value);
@@ -8885,11 +9590,48 @@ function parseCallToolArgs(value) {
8885
9590
  if (!isPlainObject(parsed)) throw new CapletsError("REQUEST_INVALID", "call-tool --args must be a JSON object");
8886
9591
  return parsed;
8887
9592
  }
9593
+ function parseJsonObjectOption(value, label) {
9594
+ if (value === void 0) return {};
9595
+ let parsed;
9596
+ try {
9597
+ parsed = JSON.parse(value);
9598
+ } catch (error) {
9599
+ throw new CapletsError("REQUEST_INVALID", `${label} must be valid JSON`, error);
9600
+ }
9601
+ if (!isPlainObject(parsed)) throw new CapletsError("REQUEST_INVALID", `${label} must be a JSON object`);
9602
+ return parsed;
9603
+ }
9604
+ function completionRefFromOptions(options) {
9605
+ if (options.prompt && options.resourceTemplate) throw new CapletsError("REQUEST_INVALID", "complete accepts either --prompt or --resource-template, not both");
9606
+ if (options.prompt) return {
9607
+ type: "prompt",
9608
+ name: options.prompt
9609
+ };
9610
+ if (options.resourceTemplate) return {
9611
+ type: "resourceTemplate",
9612
+ uri: options.resourceTemplate
9613
+ };
9614
+ throw new CapletsError("REQUEST_INVALID", "complete requires --prompt or --resource-template");
9615
+ }
8888
9616
  function isPlainObject(value) {
8889
9617
  return value !== null && typeof value === "object" && !Array.isArray(value);
8890
9618
  }
8891
9619
  async function executeOperation(caplet, request, io) {
8892
- const configPath = envConfigPath();
9620
+ const command = remoteCommandForOperation(request.operation);
9621
+ if (io.remote && command) {
9622
+ const result = await io.remote.request(command, {
9623
+ caplet,
9624
+ request
9625
+ });
9626
+ const output = cliOutputForOperation(result, {
9627
+ ...request,
9628
+ caplet
9629
+ }, io.format ?? "markdown");
9630
+ io.writeOut(typeof output === "string" ? `${output}\n` : `${JSON.stringify(output, null, 2)}\n`);
9631
+ if (isPlainObject(result) && result.isError === true) io.setExitCode(1);
9632
+ return;
9633
+ }
9634
+ const configPath = envConfigPath(io.env ?? process.env);
8893
9635
  const engine = new CapletsEngine({
8894
9636
  ...configPath ? { configPath } : {},
8895
9637
  ...io.authDir ? { authDir: io.authDir } : {},
@@ -9001,6 +9743,55 @@ function markdownSummaryForOperation(result, request) {
9001
9743
  "",
9002
9744
  "Use `--format json` to inspect the full structured result."
9003
9745
  ].filter((line) => line !== void 0).join("\n");
9746
+ case "list_resources":
9747
+ case "search_resources": {
9748
+ const resources = Array.isArray(payload.resources) ? payload.resources : [];
9749
+ const templates = Array.isArray(payload.resourceTemplates) ? payload.resourceTemplates : [];
9750
+ const matches = Array.isArray(payload.matches) ? payload.matches : [...resources, ...templates];
9751
+ return [
9752
+ `## MCP resources for \`${id}\``,
9753
+ "",
9754
+ `${matches.length} item${matches.length === 1 ? "" : "s"} found.`,
9755
+ "",
9756
+ ...formatResourceLines(matches, "markdown")
9757
+ ].join("\n");
9758
+ }
9759
+ case "list_resource_templates": {
9760
+ const templates = Array.isArray(payload.resourceTemplates) ? payload.resourceTemplates : [];
9761
+ return [
9762
+ `## MCP resource templates for \`${id}\``,
9763
+ "",
9764
+ ...formatResourceLines(templates, "markdown")
9765
+ ].join("\n");
9766
+ }
9767
+ case "read_resource": return [
9768
+ `## Resource \`${String(request.uri ?? "")}\``,
9769
+ "",
9770
+ summarizeResourceRead(payload),
9771
+ "",
9772
+ "Use `--format json` to inspect all contents."
9773
+ ].join("\n");
9774
+ case "list_prompts":
9775
+ case "search_prompts": {
9776
+ const prompts = Array.isArray(payload.prompts) ? payload.prompts : [];
9777
+ return [
9778
+ `## MCP prompts for \`${id}\``,
9779
+ "",
9780
+ ...formatPromptLines(prompts, "markdown")
9781
+ ].join("\n");
9782
+ }
9783
+ case "get_prompt": return [
9784
+ `## Prompt \`${String(request.caplet)}.${String(request.prompt)}\``,
9785
+ "",
9786
+ summarizePromptResult(payload),
9787
+ "",
9788
+ "Use `--format json` to inspect all messages."
9789
+ ].join("\n");
9790
+ case "complete": return [
9791
+ `## Completion for \`${id}\``,
9792
+ "",
9793
+ summarizeCompletionResult(payload)
9794
+ ].join("\n");
9004
9795
  default: return JSON.stringify(payload, null, 2);
9005
9796
  }
9006
9797
  }
@@ -9057,6 +9848,33 @@ function plainSummaryForOperation(result, request) {
9057
9848
  `Result: ${summarizeCallResult(payload)}`,
9058
9849
  "Use --format json to inspect the full structured result."
9059
9850
  ].filter((line) => Boolean(line)).join("\n");
9851
+ case "list_resources":
9852
+ case "search_resources": {
9853
+ const resources = Array.isArray(payload.resources) ? payload.resources : [];
9854
+ const templates = Array.isArray(payload.resourceTemplates) ? payload.resourceTemplates : [];
9855
+ const matches = Array.isArray(payload.matches) ? payload.matches : [...resources, ...templates];
9856
+ return [`MCP resources for ${id} (${matches.length}):`, ...formatResourceLines(matches, "plain")].join("\n");
9857
+ }
9858
+ case "list_resource_templates": {
9859
+ const templates = Array.isArray(payload.resourceTemplates) ? payload.resourceTemplates : [];
9860
+ return [`MCP resource templates for ${id}:`, ...formatResourceLines(templates, "plain")].join("\n");
9861
+ }
9862
+ case "read_resource": return [
9863
+ `Resource ${String(request.uri ?? "")}`,
9864
+ summarizeResourceRead(payload),
9865
+ "Use --format json to inspect all contents."
9866
+ ].join("\n");
9867
+ case "list_prompts":
9868
+ case "search_prompts": {
9869
+ const prompts = Array.isArray(payload.prompts) ? payload.prompts : [];
9870
+ return [`MCP prompts for ${id}:`, ...formatPromptLines(prompts, "plain")].join("\n");
9871
+ }
9872
+ case "get_prompt": return [
9873
+ `Prompt ${String(request.caplet)}.${String(request.prompt)}`,
9874
+ summarizePromptResult(payload),
9875
+ "Use --format json to inspect all messages."
9876
+ ].join("\n");
9877
+ case "complete": return [`Completion for ${id}`, summarizeCompletionResult(payload)].join("\n");
9060
9878
  default: return JSON.stringify(payload, null, 2);
9061
9879
  }
9062
9880
  }
@@ -9073,6 +9891,44 @@ function formatToolLines(tools, format) {
9073
9891
  return `- ${displayName}${flags ? ` (${flags})` : ""}${tool.description ? ` — ${compactDescription(String(tool.description))}` : ""}`;
9074
9892
  });
9075
9893
  }
9894
+ function formatResourceLines(resources, format) {
9895
+ if (resources.length === 0) return ["- none"];
9896
+ return resources.map((resource) => {
9897
+ if (!isPlainObject(resource)) return `- ${String(resource)}`;
9898
+ const name = String(resource.uri ?? resource.uriTemplate ?? "unknown");
9899
+ const displayName = format === "markdown" ? `\`${name}\`` : name;
9900
+ const label = typeof resource.name === "string" ? ` (${resource.name})` : "";
9901
+ return `- ${typeof resource.kind === "string" ? `${resource.kind}: ` : ""}${displayName}${label}${resource.description ? ` — ${compactDescription(String(resource.description))}` : ""}`;
9902
+ });
9903
+ }
9904
+ function formatPromptLines(prompts, format) {
9905
+ if (prompts.length === 0) return ["- none"];
9906
+ return prompts.map((prompt) => {
9907
+ if (!isPlainObject(prompt)) return `- ${String(prompt)}`;
9908
+ const name = String(prompt.prompt ?? prompt.name ?? "unknown");
9909
+ return `- ${format === "markdown" ? `\`${name}\`` : name}${Array.isArray(prompt.arguments) ? ` (${prompt.arguments.length} args)` : ""}${prompt.description ? ` — ${compactDescription(String(prompt.description))}` : ""}`;
9910
+ });
9911
+ }
9912
+ function summarizeResourceRead(payload) {
9913
+ const contents = Array.isArray(payload.contents) ? payload.contents : [];
9914
+ if (contents.length === 0) return "No contents returned.";
9915
+ const first = contents.find(isPlainObject);
9916
+ if (!first) return `${contents.length} content item${contents.length === 1 ? "" : "s"} returned.`;
9917
+ return previewValue(typeof first.text === "string" ? first.text : first.blob) ?? `${contents.length} content item${contents.length === 1 ? "" : "s"} returned.`;
9918
+ }
9919
+ function summarizePromptResult(payload) {
9920
+ const messages = Array.isArray(payload.messages) ? payload.messages : [];
9921
+ if (messages.length === 0) return "No messages returned.";
9922
+ const first = messages.find(isPlainObject);
9923
+ if (!first) return `${messages.length} message${messages.length === 1 ? "" : "s"} returned.`;
9924
+ return previewValue((isPlainObject(first.content) ? first.content : void 0)?.text ?? first.content) ?? `${messages.length} message${messages.length === 1 ? "" : "s"} returned.`;
9925
+ }
9926
+ function summarizeCompletionResult(payload) {
9927
+ const completion = isPlainObject(payload.completion) ? payload.completion : void 0;
9928
+ const values = Array.isArray(completion?.values) ? completion.values : [];
9929
+ if (values.length > 0) return values.map((value) => `- ${String(value)}`).join("\n");
9930
+ return previewValue(payload) ?? "No completions returned.";
9931
+ }
9076
9932
  function compactDescription(value) {
9077
9933
  const firstParagraph = value.trim().split(/\n\s*\n/u)[0] ?? "";
9078
9934
  const collapsed = (firstParagraph.match(/^.*?(?:[.!?](?=\s|$)|$)/u)?.[0] ?? firstParagraph).replace(/\s+/gu, " ").trim();
@@ -9135,12 +9991,12 @@ function schemaSummary(schema) {
9135
9991
  required.length > 0 ? `required ${required.join(", ")}` : "no required fields"
9136
9992
  ].filter((part) => Boolean(part)).join("; ");
9137
9993
  }
9138
- function addDestinationRoot(options) {
9139
- return options.global ? resolveCapletsRoot(resolveConfigPath(envConfigPath())) : resolveProjectCapletsRoot();
9994
+ function addDestinationRoot(options, configPath) {
9995
+ return options.global ? resolveCapletsRoot(resolveConfigPath(configPath)) : resolveProjectCapletsRoot();
9140
9996
  }
9141
9997
  function writeAddResult(writeOut, label, result) {
9142
9998
  if (result.path) {
9143
- writeOut(`Wrote ${label} Caplet to ${result.path}\n`);
9999
+ writeOut(`Wrote ${result.remote ? "remote " : ""}${label} Caplet to ${result.path}\n`);
9144
10000
  return;
9145
10001
  }
9146
10002
  writeOut(result.text);