@caplets/core 0.16.0 → 0.17.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,4 @@
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 { A as CompleteRequestSchema, At as string, B as InitializedNotificationSchema, C as assertToolsCallTaskCapability, Ct as resolveProjectConfigPath, D as toJsonSchemaCompat, Dt as ZodOptional, E as mergeCapabilities, Et as SERVER_ID_PATTERN, F as ElicitResultSchema, Ft as toSafeError, G as ListResourcesRequestSchema, H as LATEST_PROTOCOL_VERSION, I as EmptyResultSchema, It as __commonJSMin, J as LoggingLevelSchema, K as ListRootsResultSchema, L as ErrorCode, Lt as __require, M as CreateMessageResultWithToolsSchema, Mt as CAPLETS_ERROR_CODES, N as CreateTaskResultSchema, Nt as CapletsError, O as CallToolRequestSchema, Ot as literal, Pt as redactSecrets, Q as SetLevelRequestSchema, R as GetPromptRequestSchema, Rt as __toESM, S as assertClientRequestTaskCapability, St as resolveProjectCapletsRoot, T as Protocol, Tt as validateCapletFile, U as ListPromptsRequestSchema, V as JSONRPCMessageSchema, W as ListResourceTemplatesRequestSchema, X as ReadResourceRequestSchema, Y as McpError, Z as SUPPORTED_PROTOCOL_VERSIONS, _ as readTokenBundle, _t as loadConfigWithSources, a as resolveCapletsServer, at as isJSONRPCResultResponse, b as serializeMessage, bt as resolveCapletsRoot, c as handleServerTool, ct as getParseErrorMessage, d as runGenericOAuthFlow, dt as isZ4Schema, et as assertCompleteRequestPrompt, f as runOAuthFlow, ft as normalizeObjectSchema, g as isTokenBundleExpired, gt as loadConfig, h as deleteTokenBundle, ht as safeParseAsync, i as resolveCapletsMode, it as isJSONRPCRequest, j as CreateMessageResultSchema, jt as url, k as CallToolResultSchema, kt as object, l as ServerRegistry, lt as getSchemaDescription, m as startOAuthFlow, mt as safeParse, nt as isInitializeRequest, o as CapletsEngine, ot as getLiteralValue, p as startGenericOAuthFlow, pt as objectFromShape, q as ListToolsRequestSchema, r as parseServerBaseUrl, rt as isJSONRPCErrorResponse, s as generatedToolInputSchema, st as getObjectShape, t as controlUrlForBase, tt as assertCompleteRequestResourceTemplate, u as capabilityDescription, ut as isSchemaOptional, vt as parseConfig, w as AjvJsonSchemaValidator, wt as discoverCapletFiles, xt as resolveConfigPath, y as ReadBuffer, yt as DEFAULT_AUTH_DIR, z as InitializeRequestSchema } from "./options-CJEOqS87.js";
2
2
  import { accessSync, chmodSync, closeSync, constants, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
3
3
  import { basename, dirname, join, parse, relative, resolve } from "node:path";
4
4
  import { execFileSync } from "node:child_process";
@@ -1319,7 +1319,7 @@ const EMPTY_COMPLETION_RESULT = { completion: {
1319
1319
  } };
1320
1320
  //#endregion
1321
1321
  //#region package.json
1322
- var version = "0.16.0";
1322
+ var version = "0.17.0";
1323
1323
  //#endregion
1324
1324
  //#region src/serve/session.ts
1325
1325
  var CapletsMcpSession = class {
@@ -4921,49 +4921,56 @@ async function loginAuth(serverId, options) {
4921
4921
  }
4922
4922
  }
4923
4923
  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`);
4924
+ if (logoutAuthResult(serverId, options).deleted) options.writeOut(`Deleted OAuth credentials for \`${serverId}\`.\n`);
4926
4925
  else options.writeOut(`No OAuth credentials found for \`${serverId}\`.\n`);
4927
4926
  }
4927
+ function logoutAuthResult(serverId, options) {
4928
+ assertLoginTarget(findAuthTarget(serverId, loadConfig(options.configPath)), serverId);
4929
+ return {
4930
+ server: serverId,
4931
+ deleted: deleteTokenBundle(serverId, options.authDir)
4932
+ };
4933
+ }
4928
4934
  function listAuth(options) {
4929
- const servers = authTargets(loadConfig(options.configPath)).sort((left, right) => left.server.localeCompare(right.server));
4935
+ const rows = listAuthRows(options);
4930
4936
  const format = options.format ?? "plain";
4931
4937
  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
4938
  options.writeOut(`${JSON.stringify(rows, null, 2)}\n`);
4943
4939
  return;
4944
4940
  }
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) {
4941
+ options.writeOut(formatAuthRows(rows, format));
4942
+ }
4943
+ function listAuthRows(options) {
4944
+ return authTargets(loadConfig(options.configPath)).sort((left, right) => left.server.localeCompare(right.server)).map((server) => {
4952
4945
  const bundle = readTokenBundle(server.server, options.authDir);
4953
4946
  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("; ");
4947
+ return {
4948
+ server: server.server,
4949
+ status,
4950
+ ...bundle?.expiresAt ? { expiresAt: bundle.expiresAt } : {},
4951
+ ...bundle?.scope ? { scope: bundle.scope } : {}
4952
+ };
4953
+ });
4954
+ }
4955
+ function formatAuthRows(rows, format) {
4956
+ if (rows.length === 0) return format === "markdown" ? "## OAuth credentials\n\nNo configured remote OAuth servers found.\n" : "No configured remote OAuth servers found.\n";
4957
+ let output = "";
4958
+ if (format === "markdown") output += "## OAuth credentials\n\n";
4959
+ else output += "OAuth credentials\n\n";
4960
+ for (const row of rows) {
4961
+ const details = [row.expiresAt ? `expires ${row.expiresAt}` : void 0, row.scope ? `scope ${row.scope}` : void 0].filter(Boolean).join("; ");
4955
4962
  if (format === "markdown") {
4956
- options.writeOut(`- \`${server.server}\` — ${status}${details ? ` (${details})` : ""}\n`);
4963
+ output += `- \`${row.server}\` — ${row.status}${details ? ` (${details})` : ""}\n`;
4957
4964
  continue;
4958
4965
  }
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");
4966
+ output += [
4967
+ row.server,
4968
+ ` Status: ${row.status}`,
4969
+ ...row.expiresAt ? [` Expires: ${row.expiresAt}`] : [],
4970
+ ...row.scope ? [` Scope: ${row.scope}`] : []
4971
+ ].join("\n") + "\n\n";
4966
4972
  }
4973
+ return output;
4967
4974
  }
4968
4975
  function findAuthTarget(serverId, config = loadConfig()) {
4969
4976
  return authTargets(config).find((server) => server.server === serverId);
@@ -5390,6 +5397,96 @@ function nearestExistingParent(path) {
5390
5397
  return nearestExistingParent(parent);
5391
5398
  }
5392
5399
  //#endregion
5400
+ //#region src/remote-control/client.ts
5401
+ var RemoteControlClient = class {
5402
+ #baseUrl;
5403
+ #requestInit;
5404
+ #fetch;
5405
+ constructor(options) {
5406
+ this.#baseUrl = options.baseUrl;
5407
+ this.#requestInit = options.requestInit;
5408
+ this.#fetch = options.fetch ?? fetch;
5409
+ }
5410
+ async request(command, args) {
5411
+ const controlUrl = controlUrlForBase(this.#baseUrl);
5412
+ let response;
5413
+ try {
5414
+ response = await this.#fetch(controlUrl, {
5415
+ ...this.#requestInit,
5416
+ method: "POST",
5417
+ headers: mergeJsonHeaders(this.#requestInit.headers),
5418
+ body: JSON.stringify({
5419
+ command,
5420
+ arguments: args
5421
+ })
5422
+ });
5423
+ } catch (error) {
5424
+ throw new CapletsError("SERVER_UNAVAILABLE", `Could not connect to Caplets server at ${safeBaseUrl(this.#baseUrl)}.`, toSafeError(error, "SERVER_UNAVAILABLE"));
5425
+ }
5426
+ if (response.status === 401 || response.status === 403) throw new CapletsError("AUTH_FAILED", "Caplets server authentication failed. Check CAPLETS_SERVER_USER and CAPLETS_SERVER_PASSWORD.");
5427
+ if (!response.ok) throw new CapletsError("SERVER_UNAVAILABLE", `Caplets server at ${safeBaseUrl(this.#baseUrl)} returned HTTP ${response.status}.`);
5428
+ const payload = await parseRemoteCliResponse(response);
5429
+ if (!payload.ok) throw new CapletsError(payload.error.code, redactRemoteMessage(payload.error.message), payload.error.nextAction === void 0 ? void 0 : { nextAction: payload.error.nextAction });
5430
+ return payload.result;
5431
+ }
5432
+ };
5433
+ function mergeJsonHeaders(headers) {
5434
+ const merged = new Headers(headers);
5435
+ merged.set("content-type", "application/json");
5436
+ return merged;
5437
+ }
5438
+ function safeBaseUrl(baseUrl) {
5439
+ const safe = new URL(baseUrl.href);
5440
+ safe.username = "";
5441
+ safe.password = "";
5442
+ safe.search = "";
5443
+ safe.hash = "";
5444
+ return safe.toString();
5445
+ }
5446
+ async function parseRemoteCliResponse(response) {
5447
+ let payload;
5448
+ try {
5449
+ payload = await response.json();
5450
+ } catch (error) {
5451
+ throw invalidRemoteControlResponse(error);
5452
+ }
5453
+ if (!isRecord(payload)) throw invalidRemoteControlResponse();
5454
+ if (payload.ok === true) {
5455
+ if (!("result" in payload)) throw invalidRemoteControlResponse();
5456
+ return {
5457
+ ok: true,
5458
+ result: payload.result
5459
+ };
5460
+ }
5461
+ if (payload.ok === false) {
5462
+ const error = payload.error;
5463
+ if (!isRecord(error) || typeof error.code !== "string" || typeof error.message !== "string") throw invalidRemoteControlResponse();
5464
+ if ("nextAction" in error && error.nextAction !== void 0 && typeof error.nextAction !== "string") throw invalidRemoteControlResponse();
5465
+ const errorResponse = {
5466
+ ok: false,
5467
+ error: {
5468
+ code: isCapletsErrorCode(error.code) ? error.code : "DOWNSTREAM_TOOL_ERROR",
5469
+ message: error.message
5470
+ }
5471
+ };
5472
+ if (typeof error.nextAction === "string") errorResponse.error.nextAction = error.nextAction;
5473
+ return errorResponse;
5474
+ }
5475
+ throw invalidRemoteControlResponse();
5476
+ }
5477
+ function invalidRemoteControlResponse(cause) {
5478
+ return new CapletsError("DOWNSTREAM_PROTOCOL_ERROR", "Caplets server returned an invalid remote control response.", cause === void 0 ? void 0 : toSafeError(cause, "DOWNSTREAM_PROTOCOL_ERROR"));
5479
+ }
5480
+ function isRecord(value) {
5481
+ return value !== null && typeof value === "object" && !Array.isArray(value);
5482
+ }
5483
+ function isCapletsErrorCode(value) {
5484
+ return CAPLETS_ERROR_CODES.includes(value);
5485
+ }
5486
+ function redactRemoteMessage(message) {
5487
+ 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]");
5488
+ }
5489
+ //#endregion
5393
5490
  //#region ../../node_modules/.pnpm/hono@4.12.19/node_modules/hono/dist/compose.js
5394
5491
  var compose = (middleware, onError, onNotFound) => {
5395
5492
  return (context, next) => {
@@ -8309,30 +8406,348 @@ var logger = (fn = console.log) => {
8309
8406
  };
8310
8407
  };
8311
8408
  //#endregion
8409
+ //#region src/remote-control/dispatch.ts
8410
+ const ENGINE_COMMANDS = new Set([
8411
+ "get_caplet",
8412
+ "check_backend",
8413
+ "list_tools",
8414
+ "search_tools",
8415
+ "get_tool",
8416
+ "call_tool"
8417
+ ]);
8418
+ async function dispatchRemoteCliRequest(request, context) {
8419
+ try {
8420
+ return {
8421
+ ok: true,
8422
+ result: await dispatch(request, context)
8423
+ };
8424
+ } catch (error) {
8425
+ const safe = toSafeError(error);
8426
+ const action = nextAction(safe.details);
8427
+ return {
8428
+ ok: false,
8429
+ error: {
8430
+ code: safe.code,
8431
+ message: redactControlErrorMessage(safe.message),
8432
+ ...action ? { nextAction: action } : {}
8433
+ }
8434
+ };
8435
+ }
8436
+ }
8437
+ async function dispatch(request, context) {
8438
+ assertObject(request, "remote control request");
8439
+ assertObject(request.arguments, "remote control request arguments");
8440
+ if (request.command === "list") return listCaplets(loadConfigWithSources(context.configPath, context.projectConfigPath), { includeDisabled: optionalBoolean(request.arguments, "includeDisabled") ?? false });
8441
+ if (ENGINE_COMMANDS.has(request.command)) {
8442
+ const caplet = requiredString(request.arguments, "caplet");
8443
+ const toolRequest = requiredEngineRequest(request.arguments, request.command);
8444
+ const engine = new CapletsEngine(context);
8445
+ try {
8446
+ return await engine.execute(caplet, toolRequest);
8447
+ } finally {
8448
+ await engine.close();
8449
+ }
8450
+ }
8451
+ if (request.command === "init") return {
8452
+ remote: true,
8453
+ path: initConfig({
8454
+ ...optionalProp("path", context.configPath),
8455
+ ...optionalProp("force", optionalBoolean(request.arguments, "force"))
8456
+ })
8457
+ };
8458
+ if (request.command === "add") return dispatchAdd(request.arguments, context);
8459
+ if (request.command === "install") return {
8460
+ remote: true,
8461
+ ...installCaplets(requiredString(request.arguments, "repo"), {
8462
+ ...optionalProp("capletIds", optionalStringArray(request.arguments, "capletIds")),
8463
+ destinationRoot: context.projectCapletsRoot,
8464
+ ...optionalProp("force", optionalBoolean(request.arguments, "force"))
8465
+ })
8466
+ };
8467
+ if (request.command === "auth_list") return listAuthRows({
8468
+ ...optionalProp("configPath", context.configPath),
8469
+ ...optionalProp("authDir", context.authDir)
8470
+ });
8471
+ if (request.command === "auth_logout") return logoutAuthResult(requiredString(request.arguments, "server"), {
8472
+ ...optionalProp("configPath", context.configPath),
8473
+ ...optionalProp("authDir", context.authDir)
8474
+ });
8475
+ if (request.command === "auth_login_start") return startRemoteAuthLogin(requiredString(request.arguments, "server"), context);
8476
+ if (request.command === "auth_login_complete") return completeRemoteAuthLogin(requiredString(request.arguments, "flowId"), requiredString(request.arguments, "callbackUrl"), context);
8477
+ throw new CapletsError("UNKNOWN_OPERATION", `Unsupported remote control command ${request.command}`);
8478
+ }
8479
+ async function startRemoteAuthLogin(serverId, context) {
8480
+ if (!context.authFlowStore || !context.controlCallbackBaseUrl) throw new CapletsError("REQUEST_INVALID", "Remote auth login is not available on this server");
8481
+ const config = loadConfigWithSources(context.configPath, context.projectConfigPath).config;
8482
+ const target = findAuthTarget(serverId, config);
8483
+ assertLoginTarget(target, serverId);
8484
+ const flowId = randomUUID();
8485
+ const baseUrl = context.controlCallbackBaseUrl.endsWith("/") ? context.controlCallbackBaseUrl : `${context.controlCallbackBaseUrl}/`;
8486
+ const redirectUri = new URL(`auth/callback/${flowId}`, baseUrl).toString();
8487
+ const started = target.backend === "mcp" ? await startOAuthFlow(target, {
8488
+ redirectUri,
8489
+ ...optionalProp("authDir", context.authDir)
8490
+ }) : await startGenericOAuthFlow(target, {
8491
+ redirectUri,
8492
+ ...optionalProp("authDir", context.authDir)
8493
+ });
8494
+ if (!started.authorizationUrl) return {
8495
+ server: serverId,
8496
+ authenticated: true
8497
+ };
8498
+ const flow = context.authFlowStore.create({
8499
+ server: serverId,
8500
+ authorizationUrl: started.authorizationUrl,
8501
+ complete: started.complete
8502
+ }, flowId);
8503
+ return {
8504
+ server: serverId,
8505
+ flowId: flow.id,
8506
+ authorizationUrl: flow.authorizationUrl
8507
+ };
8508
+ }
8509
+ async function completeRemoteAuthLogin(flowId, callbackUrl, context) {
8510
+ const flow = context.authFlowStore?.get(flowId);
8511
+ if (!flow) throw new CapletsError("REQUEST_INVALID", `Unknown auth flow ${flowId}`);
8512
+ context.authFlowStore?.delete(flowId);
8513
+ await flow.complete(callbackUrl);
8514
+ return {
8515
+ server: flow.server,
8516
+ authenticated: true
8517
+ };
8518
+ }
8519
+ function dispatchAdd(args, context) {
8520
+ const kind = requiredString(args, "kind");
8521
+ const id = requiredString(args, "id");
8522
+ const options = remoteAddOptions$1(kind, optionalObject(args, "options"));
8523
+ switch (kind) {
8524
+ case "cli": return {
8525
+ remote: true,
8526
+ label: "CLI",
8527
+ ...addCliCaplet(id, {
8528
+ ...options,
8529
+ destinationRoot: context.projectCapletsRoot,
8530
+ print: false
8531
+ })
8532
+ };
8533
+ case "mcp": return {
8534
+ remote: true,
8535
+ label: "MCP",
8536
+ ...addMcpCaplet(id, {
8537
+ ...options,
8538
+ destinationRoot: context.projectCapletsRoot,
8539
+ print: false
8540
+ })
8541
+ };
8542
+ case "openapi": return {
8543
+ remote: true,
8544
+ label: "OpenAPI",
8545
+ ...addOpenApiCaplet(id, {
8546
+ ...options,
8547
+ destinationRoot: context.projectCapletsRoot,
8548
+ print: false
8549
+ })
8550
+ };
8551
+ case "graphql": return {
8552
+ remote: true,
8553
+ label: "GraphQL",
8554
+ ...addGraphqlCaplet(id, {
8555
+ ...options,
8556
+ destinationRoot: context.projectCapletsRoot,
8557
+ print: false
8558
+ })
8559
+ };
8560
+ case "http": return {
8561
+ remote: true,
8562
+ label: "HTTP",
8563
+ ...addHttpCaplet(id, {
8564
+ ...options,
8565
+ destinationRoot: context.projectCapletsRoot,
8566
+ print: false
8567
+ })
8568
+ };
8569
+ default: throw new CapletsError("REQUEST_INVALID", "add.kind must be cli, mcp, openapi, graphql, or http");
8570
+ }
8571
+ }
8572
+ function optionalProp(key, value) {
8573
+ return value === void 0 ? {} : { [key]: value };
8574
+ }
8575
+ function assertObject(value, label) {
8576
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw new CapletsError("REQUEST_INVALID", `${label} must be an object`);
8577
+ }
8578
+ function requiredString(args, key) {
8579
+ const value = args[key];
8580
+ if (typeof value !== "string" || value.length === 0) throw new CapletsError("REQUEST_INVALID", `${key} must be a non-empty string`);
8581
+ return value;
8582
+ }
8583
+ function optionalObject(args, key) {
8584
+ const value = args[key];
8585
+ if (value === void 0) return {};
8586
+ assertObject(value, key);
8587
+ return value;
8588
+ }
8589
+ function requiredEngineRequest(args, command) {
8590
+ const toolRequest = optionalObject(args, "request");
8591
+ if (typeof toolRequest.operation !== "string") throw new CapletsError("REQUEST_INVALID", "request.operation must be a string");
8592
+ if (toolRequest.operation !== command) throw new CapletsError("REQUEST_INVALID", `request.operation must match remote command ${command}`);
8593
+ return toolRequest;
8594
+ }
8595
+ function remoteAddOptions$1(kind, options) {
8596
+ rejectServerOwnedAddOptions(options);
8597
+ switch (kind) {
8598
+ case "cli": return pickOptions(options, {
8599
+ repo: "string",
8600
+ include: "string",
8601
+ command: "string",
8602
+ force: "boolean"
8603
+ });
8604
+ case "mcp": return pickOptions(options, {
8605
+ command: "string",
8606
+ arg: "string-array",
8607
+ cwd: "string",
8608
+ env: "string-array",
8609
+ url: "string",
8610
+ transport: "string",
8611
+ tokenEnv: "string",
8612
+ force: "boolean"
8613
+ });
8614
+ case "openapi": return pickOptions(options, {
8615
+ spec: "string",
8616
+ baseUrl: "string",
8617
+ tokenEnv: "string",
8618
+ force: "boolean"
8619
+ });
8620
+ case "graphql": return pickOptions(options, {
8621
+ endpointUrl: "string",
8622
+ schema: "string",
8623
+ introspection: "boolean",
8624
+ tokenEnv: "string",
8625
+ force: "boolean"
8626
+ });
8627
+ case "http": return pickOptions(options, {
8628
+ baseUrl: "string",
8629
+ action: "string-array",
8630
+ tokenEnv: "string",
8631
+ force: "boolean"
8632
+ });
8633
+ default: return options;
8634
+ }
8635
+ }
8636
+ function pickOptions(options, schema) {
8637
+ const next = {};
8638
+ for (const [key, type] of Object.entries(schema)) {
8639
+ const value = options[key];
8640
+ if (value === void 0) continue;
8641
+ validateOptionType(key, value, type);
8642
+ next[key] = value;
8643
+ }
8644
+ return next;
8645
+ }
8646
+ function rejectServerOwnedAddOptions(options) {
8647
+ if ("output" in options) throw new CapletsError("REQUEST_INVALID", "Remote add output is not supported remotely; the server owns destinationRoot and output path selection");
8648
+ 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`);
8649
+ }
8650
+ function validateOptionType(key, value, type) {
8651
+ if (type === "string" && typeof value !== "string") throw new CapletsError("REQUEST_INVALID", `add.options.${key} must be a string`);
8652
+ if (type === "boolean" && typeof value !== "boolean") throw new CapletsError("REQUEST_INVALID", `add.options.${key} must be a boolean`);
8653
+ if (type === "string-array") {
8654
+ if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) throw new CapletsError("REQUEST_INVALID", `add.options.${key} must be an array of strings`);
8655
+ }
8656
+ }
8657
+ function optionalBoolean(args, key) {
8658
+ const value = args[key];
8659
+ if (value === void 0) return;
8660
+ if (typeof value !== "boolean") throw new CapletsError("REQUEST_INVALID", `${key} must be a boolean`);
8661
+ return value;
8662
+ }
8663
+ function optionalStringArray(args, key) {
8664
+ const value = args[key];
8665
+ if (value === void 0) return;
8666
+ if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) throw new CapletsError("REQUEST_INVALID", `${key} must be an array of strings`);
8667
+ return value;
8668
+ }
8669
+ function nextAction(details) {
8670
+ if (details && typeof details === "object" && "nextAction" in details) {
8671
+ const value = details.nextAction;
8672
+ return typeof value === "string" ? value : void 0;
8673
+ }
8674
+ }
8675
+ function redactControlErrorMessage(message) {
8676
+ 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]");
8677
+ }
8678
+ //#endregion
8679
+ //#region src/remote-control/auth-flow.ts
8680
+ const DEFAULT_AUTH_FLOW_TTL_MS = 600 * 1e3;
8681
+ var RemoteAuthFlowStore = class {
8682
+ options;
8683
+ flows = /* @__PURE__ */ new Map();
8684
+ constructor(options = {}) {
8685
+ this.options = options;
8686
+ }
8687
+ create(flow, id = randomUUID()) {
8688
+ this.pruneExpired();
8689
+ const created = {
8690
+ id,
8691
+ createdAt: this.now(),
8692
+ ...flow
8693
+ };
8694
+ this.flows.set(created.id, created);
8695
+ return { ...created };
8696
+ }
8697
+ get(id) {
8698
+ this.pruneExpired();
8699
+ const flow = this.flows.get(id);
8700
+ if (flow && this.isExpired(flow)) {
8701
+ this.flows.delete(id);
8702
+ return;
8703
+ }
8704
+ return flow;
8705
+ }
8706
+ delete(id) {
8707
+ this.flows.delete(id);
8708
+ }
8709
+ pruneExpired() {
8710
+ for (const [id, flow] of this.flows) if (this.isExpired(flow)) this.flows.delete(id);
8711
+ }
8712
+ isExpired(flow) {
8713
+ return this.now() - flow.createdAt > (this.options.ttlMs ?? DEFAULT_AUTH_FLOW_TTL_MS);
8714
+ }
8715
+ now() {
8716
+ return this.options.now?.() ?? Date.now();
8717
+ }
8718
+ };
8719
+ //#endregion
8312
8720
  //#region src/serve/http.ts
8313
8721
  function createHttpServeApp(options, engine, io = {}) {
8314
8722
  const app = new Hono();
8315
8723
  const sessions = /* @__PURE__ */ new Map();
8316
8724
  const writeErr = io.writeErr ?? process.stderr.write.bind(process.stderr);
8725
+ const paths = servicePaths(options.path);
8726
+ const authFlowStore = io.authFlowStore ?? new RemoteAuthFlowStore();
8317
8727
  app.use("*", logger((message, ...rest) => {
8318
8728
  writeErr(`${[message, ...rest].join(" ")}\n`);
8319
8729
  }));
8320
- app.get("/", (c) => c.json({
8730
+ app.get(paths.base, (c) => c.json({
8321
8731
  name: "caplets",
8322
8732
  transport: "http",
8323
- mcp: options.path,
8324
- health: "/healthz",
8733
+ base: paths.base,
8734
+ mcp: paths.mcp,
8735
+ control: paths.control,
8736
+ health: paths.health,
8325
8737
  auth: {
8326
8738
  type: "basic",
8327
8739
  enabled: options.auth.enabled
8328
8740
  }
8329
8741
  }));
8330
- app.get("/healthz", (c) => c.json({
8742
+ app.get(paths.health, (c) => c.json({
8331
8743
  status: "ok",
8332
8744
  transport: "http",
8333
- mcpPath: options.path
8745
+ base: paths.base,
8746
+ mcpPath: paths.mcp,
8747
+ controlPath: paths.control,
8748
+ healthPath: paths.health
8334
8749
  }));
8335
- app.all(options.path, basicAuth(options.auth), async (c) => {
8750
+ app.all(paths.mcp, basicAuth(options.auth), async (c) => {
8336
8751
  const sessionId = c.req.header("mcp-session-id");
8337
8752
  if (sessionId) {
8338
8753
  const existing = sessions.get(sessionId);
@@ -8363,6 +8778,36 @@ function createHttpServeApp(options, engine, io = {}) {
8363
8778
  sessions.set(nextSessionId, session);
8364
8779
  return session.transport.handleRequest(c);
8365
8780
  });
8781
+ app.post(paths.control, basicAuth(options.auth), async (c) => {
8782
+ let request;
8783
+ try {
8784
+ const parsed = await c.req.json();
8785
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new CapletsError("REQUEST_INVALID", "Control request JSON must be an object");
8786
+ request = parsed;
8787
+ } catch (error) {
8788
+ const safe = toSafeError(error instanceof CapletsError ? error : new CapletsError("REQUEST_INVALID", "Control request body must be valid JSON", error), "REQUEST_INVALID");
8789
+ return c.json({
8790
+ ok: false,
8791
+ error: {
8792
+ code: safe.code,
8793
+ message: safe.message
8794
+ }
8795
+ });
8796
+ }
8797
+ return c.json(await dispatchRemoteCliRequest(request, controlContext(io, writeErr, authFlowStore, c.req.url, paths.control, options.trustProxy, (name) => c.req.header(name))));
8798
+ });
8799
+ app.get(routePath(paths.control, "auth/callback/:flowId"), async (c) => {
8800
+ const flowId = c.req.param("flowId");
8801
+ const result = await dispatchRemoteCliRequest({
8802
+ command: "auth_login_complete",
8803
+ arguments: {
8804
+ flowId,
8805
+ callbackUrl: c.req.url
8806
+ }
8807
+ }, controlContext(io, writeErr, authFlowStore, c.req.url, paths.control, options.trustProxy, (name) => c.req.header(name)));
8808
+ if (!result.ok) writeErr(`Caplets authentication failed for flow ${flowId}: ${result.error.message}\n`);
8809
+ 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);
8810
+ });
8366
8811
  app.notFound((c) => c.json({ error: "not_found" }, 404));
8367
8812
  app.closeCapletsSessions = async () => {
8368
8813
  await Promise.allSettled([...sessions.values()].map(async (session) => {
@@ -8373,19 +8818,66 @@ function createHttpServeApp(options, engine, io = {}) {
8373
8818
  if (options.warnUnauthenticatedNetwork) writeErr(`Warning: Caplets MCP HTTP server is listening on ${options.host} without authentication.\n`);
8374
8819
  return app;
8375
8820
  }
8821
+ function controlContext(io, writeErr, authFlowStore, requestUrl, controlPath, trustProxy, header) {
8822
+ return {
8823
+ ...io.control,
8824
+ projectCapletsRoot: io.control?.projectCapletsRoot ?? resolveProjectCapletsRoot(),
8825
+ authFlowStore,
8826
+ controlCallbackBaseUrl: new URL(controlPath, publicRequestOrigin(requestUrl, trustProxy, header)).toString(),
8827
+ writeErr
8828
+ };
8829
+ }
8830
+ function publicRequestOrigin(requestUrl, trustProxy, header) {
8831
+ const url = new URL(requestUrl);
8832
+ if (!trustProxy) return `${url.protocol.slice(0, -1)}://${header("host") ?? url.host}`;
8833
+ const forwardedProto = firstForwardedValue(header("x-forwarded-proto"));
8834
+ const forwardedHost = firstForwardedValue(header("x-forwarded-host"));
8835
+ return `${forwardedProto === "http" || forwardedProto === "https" ? forwardedProto : url.protocol.slice(0, -1)}://${forwardedHost ?? header("host") ?? url.host}`;
8836
+ }
8837
+ function firstForwardedValue(value) {
8838
+ return value?.split(",", 1)[0]?.trim() || void 0;
8839
+ }
8376
8840
  async function serveHttp(options, engineOptions = {}, writeErr = (value) => process.stderr.write(value)) {
8377
8841
  const engine = new CapletsEngine(engineOptions);
8378
- const app = createHttpServeApp(options, engine, { writeErr });
8842
+ const app = createHttpServeApp(options, engine, {
8843
+ writeErr,
8844
+ control: {
8845
+ ...engineOptions,
8846
+ projectCapletsRoot: projectCapletsRootForEngineOptions(engineOptions)
8847
+ }
8848
+ });
8849
+ const paths = servicePaths(options.path);
8850
+ const origin = `http://${formatHost(options.host)}:${options.port}`;
8851
+ const baseUrl = `${origin}${paths.base === "/" ? "" : paths.base}`;
8379
8852
  installHttpSignalHandlers(serve({
8380
8853
  fetch: app.fetch,
8381
8854
  hostname: options.host,
8382
8855
  port: options.port
8383
8856
  }, () => {
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`);
8857
+ writeErr(`Caplets HTTP service listening on ${baseUrl}\n`);
8858
+ writeErr(`MCP endpoint: ${origin}${paths.mcp}\n`);
8859
+ writeErr(`Control endpoint: ${origin}${paths.control}\n`);
8860
+ writeErr(`Health check: ${origin}${paths.health}\n`);
8386
8861
  writeErr(`Basic Auth: ${options.auth.enabled ? `enabled (user: ${options.auth.user})` : "disabled"}\n`);
8387
8862
  }), app, engine, writeErr);
8388
8863
  }
8864
+ function projectCapletsRootForEngineOptions(engineOptions) {
8865
+ return engineOptions.projectConfigPath ? resolveProjectCapletsRootForConfigPath(engineOptions.projectConfigPath) : resolveProjectCapletsRoot();
8866
+ }
8867
+ function resolveProjectCapletsRootForConfigPath(projectConfigPath) {
8868
+ return dirname(projectConfigPath);
8869
+ }
8870
+ function routePath(base, path) {
8871
+ return base === "/" ? `/${path}` : `${base}/${path}`;
8872
+ }
8873
+ function servicePaths(base) {
8874
+ return {
8875
+ base,
8876
+ mcp: routePath(base, "mcp"),
8877
+ control: routePath(base, "control"),
8878
+ health: routePath(base, "healthz")
8879
+ };
8880
+ }
8389
8881
  async function createHttpSession(engine, sessionId, options, onClose) {
8390
8882
  const transport = new StreamableHTTPTransport({
8391
8883
  sessionIdGenerator: () => sessionId,
@@ -8465,7 +8957,8 @@ const HTTP_ONLY_OPTIONS = [
8465
8957
  "path",
8466
8958
  "user",
8467
8959
  "password",
8468
- "allowUnauthenticatedHttp"
8960
+ "allowUnauthenticatedHttp",
8961
+ "trustProxy"
8469
8962
  ];
8470
8963
  function resolveServeOptions(raw, env = process.env) {
8471
8964
  const transport = parseTransport(raw.transport ?? "stdio");
@@ -8474,9 +8967,10 @@ function resolveServeOptions(raw, env = process.env) {
8474
8967
  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
8968
  return { transport };
8476
8969
  }
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");
8970
+ const serverUrl = env.CAPLETS_SERVER_URL ? parseServeServerUrl(nonEmpty(env.CAPLETS_SERVER_URL, "CAPLETS_SERVER_URL")) : void 0;
8971
+ const host = nonEmpty(raw.host, "--host") ?? serverUrlHost(serverUrl) ?? "127.0.0.1";
8972
+ const port = parsePort(raw.port ?? (serverUrl?.port ? Number(serverUrl.port) : 5387));
8973
+ const path = normalizeHttpPath(raw.path ?? serverUrl?.pathname ?? "/");
8480
8974
  const userWasExplicit = raw.user !== void 0 || hasEnv(env.CAPLETS_SERVER_USER);
8481
8975
  const user = nonEmpty(raw.user, "--user") ?? nonEmpty(env.CAPLETS_SERVER_USER, "CAPLETS_SERVER_USER") ?? "caplets";
8482
8976
  const password = nonEmpty(raw.password, "--password") ?? nonEmpty(env.CAPLETS_SERVER_PASSWORD, "CAPLETS_SERVER_PASSWORD");
@@ -8498,13 +8992,22 @@ function resolveServeOptions(raw, env = process.env) {
8498
8992
  path,
8499
8993
  auth,
8500
8994
  warnUnauthenticatedNetwork: !loopback && !auth.enabled,
8501
- loopback
8995
+ loopback,
8996
+ trustProxy: raw.trustProxy === true
8502
8997
  };
8503
8998
  }
8504
8999
  function isLoopbackHost(host) {
8505
9000
  const normalized = host.toLocaleLowerCase();
8506
9001
  return normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1";
8507
9002
  }
9003
+ function parseServeServerUrl(value) {
9004
+ try {
9005
+ return parseServerBaseUrl(value);
9006
+ } catch (error) {
9007
+ 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.");
9008
+ throw error;
9009
+ }
9010
+ }
8508
9011
  function parseTransport(value) {
8509
9012
  if (value === "stdio" || value === "http") return value;
8510
9013
  throw new CapletsError("REQUEST_INVALID", `Expected --transport to be stdio or http, got ${value}`);
@@ -8519,6 +9022,9 @@ function normalizeHttpPath(value) {
8519
9022
  if (value.includes("?") || value.includes("#")) throw new CapletsError("REQUEST_INVALID", "HTTP --path must not include a query string or fragment");
8520
9023
  return value === "/" ? value : value.replace(/\/+$/u, "");
8521
9024
  }
9025
+ function serverUrlHost(url) {
9026
+ return url?.hostname.replace(/^\[(.*)\]$/u, "$1");
9027
+ }
8522
9028
  function nonEmpty(value, label) {
8523
9029
  if (value === void 0) return;
8524
9030
  const trimmed = value.trim();
@@ -8656,6 +9162,8 @@ async function runCli(args, io = {}) {
8656
9162
  function createProgram(io = {}) {
8657
9163
  const writeOut = io.writeOut ?? ((value) => process.stdout.write(value));
8658
9164
  const writeErr = io.writeErr ?? ((value) => process.stderr.write(value));
9165
+ const env = io.env ?? process.env;
9166
+ const currentConfigPath = () => envConfigPath(env);
8659
9167
  const setExitCode = io.setExitCode ?? ((code) => {
8660
9168
  process.exitCode = code;
8661
9169
  });
@@ -8665,42 +9173,78 @@ function createProgram(io = {}) {
8665
9173
  writeErr,
8666
9174
  outputError: (value, write) => write(value)
8667
9175
  });
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) => {
9176
+ 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 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
9177
  const resolved = resolveServeOptions(options);
8670
- const configPath = envConfigPath();
9178
+ const configPath = currentConfigPath();
8671
9179
  await (io.serve ?? ((serveOptions) => serveResolvedCaplets(serveOptions, {
8672
9180
  ...configPath ? { configPath } : {},
8673
9181
  ...io.authDir ? { authDir: io.authDir } : {}
8674
9182
  }, writeErr)))(resolved);
8675
9183
  });
8676
- program.command("init").description("Create a starter Caplets config file.").option("--force", "overwrite an existing config file").action((options) => {
8677
- const configPath = envConfigPath();
9184
+ program.command("init").description("Create a starter Caplets config file.").option("--force", "overwrite an existing config file").action(async (options) => {
9185
+ const remote = remoteClientForCli(io);
9186
+ if (remote) {
9187
+ writeOut(`Created remote Caplets config at ${(await remote.request("init", { force: Boolean(options.force) })).path}\n`);
9188
+ return;
9189
+ }
9190
+ const configPath = currentConfigPath();
8678
9191
  writeOut(`Created Caplets config at ${initConfig({
8679
9192
  ...configPath ? { path: configPath } : {},
8680
9193
  force: Boolean(options.force)
8681
9194
  })}\n`);
8682
9195
  });
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) });
9196
+ 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(async (options) => {
9197
+ const includeDisabled = Boolean(options.all);
9198
+ const remote = remoteClientForCli(io);
9199
+ if (remote) {
9200
+ const rows = await remote.request("list", { includeDisabled });
9201
+ if (options.json || options.format === "json") {
9202
+ writeOut(`${JSON.stringify(rows, null, 2)}\n`);
9203
+ return;
9204
+ }
9205
+ writeOut(formatCapletList(rows, options.format ?? "plain"));
9206
+ return;
9207
+ }
9208
+ const rows = listCaplets(loadConfigWithSources(currentConfigPath()), { includeDisabled });
8685
9209
  if (options.json || options.format === "json") {
8686
9210
  writeOut(`${JSON.stringify(rows, null, 2)}\n`);
8687
9211
  return;
8688
9212
  }
8689
9213
  writeOut(formatCapletList(rows, options.format ?? "plain"));
8690
9214
  });
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) => {
9215
+ 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(async (repo, capletIds, options) => {
9216
+ const remote = remoteClientForCli(io);
9217
+ if (remote) {
9218
+ if (options.global) writeErr("Warning: --global is not supported in remote mode; the server controls the installation destination.\n");
9219
+ const result = await remote.request("install", {
9220
+ repo,
9221
+ capletIds,
9222
+ force: Boolean(options.force)
9223
+ });
9224
+ for (const caplet of result.installed) writeOut(`Installed ${caplet.id} to remote ${caplet.destination}\n`);
9225
+ return;
9226
+ }
8692
9227
  const result = installCaplets(repo, {
8693
9228
  capletIds,
8694
9229
  force: Boolean(options.force),
8695
- destinationRoot: options.global ? resolveCapletsRoot(resolveConfigPath(envConfigPath())) : resolveProjectCapletsRoot()
9230
+ destinationRoot: options.global ? resolveCapletsRoot(resolveConfigPath(currentConfigPath())) : resolveProjectCapletsRoot()
8696
9231
  });
8697
9232
  for (const caplet of result.installed) writeOut(`Installed ${caplet.id} to ${caplet.destination}\n`);
8698
9233
  });
8699
9234
  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) => {
9235
+ 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) => {
9236
+ const remote = remoteClientForCli(io);
9237
+ if (remote) {
9238
+ writeAddResult(writeOut, "CLI", await remote.request("add", {
9239
+ kind: "cli",
9240
+ id,
9241
+ options: remoteAddOptions(options)
9242
+ }));
9243
+ return;
9244
+ }
8701
9245
  const result = addCliCaplet(id, {
8702
9246
  ...options,
8703
- destinationRoot: options.global ? resolveCapletsRoot(resolveConfigPath(envConfigPath())) : resolveProjectCapletsRoot()
9247
+ destinationRoot: options.global ? resolveCapletsRoot(resolveConfigPath(currentConfigPath())) : resolveProjectCapletsRoot()
8704
9248
  });
8705
9249
  if (result.path) {
8706
9250
  writeOut(`Wrote CLI Caplet to ${result.path}\n`);
@@ -8708,28 +9252,64 @@ function createProgram(io = {}) {
8708
9252
  }
8709
9253
  writeOut(result.text);
8710
9254
  });
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) => {
9255
+ 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) => {
9256
+ const remote = remoteClientForCli(io);
9257
+ if (remote) {
9258
+ writeAddResult(writeOut, "MCP", await remote.request("add", {
9259
+ kind: "mcp",
9260
+ id,
9261
+ options: remoteAddOptions(options)
9262
+ }));
9263
+ return;
9264
+ }
8712
9265
  writeAddResult(writeOut, "MCP", addMcpCaplet(id, {
8713
9266
  ...options,
8714
- destinationRoot: addDestinationRoot(options)
9267
+ destinationRoot: addDestinationRoot(options, currentConfigPath())
8715
9268
  }));
8716
9269
  });
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) => {
9270
+ 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) => {
9271
+ const remote = remoteClientForCli(io);
9272
+ if (remote) {
9273
+ writeAddResult(writeOut, "OpenAPI", await remote.request("add", {
9274
+ kind: "openapi",
9275
+ id,
9276
+ options: remoteAddOptions(options)
9277
+ }));
9278
+ return;
9279
+ }
8718
9280
  writeAddResult(writeOut, "OpenAPI", addOpenApiCaplet(id, {
8719
9281
  ...options,
8720
- destinationRoot: addDestinationRoot(options)
9282
+ destinationRoot: addDestinationRoot(options, currentConfigPath())
8721
9283
  }));
8722
9284
  });
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) => {
9285
+ 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) => {
9286
+ const remote = remoteClientForCli(io);
9287
+ if (remote) {
9288
+ writeAddResult(writeOut, "GraphQL", await remote.request("add", {
9289
+ kind: "graphql",
9290
+ id,
9291
+ options: remoteAddOptions(options)
9292
+ }));
9293
+ return;
9294
+ }
8724
9295
  writeAddResult(writeOut, "GraphQL", addGraphqlCaplet(id, {
8725
9296
  ...options,
8726
- destinationRoot: addDestinationRoot(options)
9297
+ destinationRoot: addDestinationRoot(options, currentConfigPath())
8727
9298
  }));
8728
9299
  });
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) => {
9300
+ 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) => {
9301
+ const remote = remoteClientForCli(io);
9302
+ if (remote) {
9303
+ writeAddResult(writeOut, "HTTP", await remote.request("add", {
9304
+ kind: "http",
9305
+ id,
9306
+ options: remoteAddOptions(options)
9307
+ }));
9308
+ return;
9309
+ }
8730
9310
  writeAddResult(writeOut, "HTTP", addHttpCaplet(id, {
8731
9311
  ...options,
8732
- destinationRoot: addDestinationRoot(options)
9312
+ destinationRoot: addDestinationRoot(options, currentConfigPath())
8733
9313
  }));
8734
9314
  });
8735
9315
  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) => {
@@ -8738,6 +9318,8 @@ function createProgram(io = {}) {
8738
9318
  writeErr,
8739
9319
  setExitCode,
8740
9320
  authDir: io.authDir,
9321
+ env,
9322
+ remote: remoteClientForCli(io),
8741
9323
  format: options.format
8742
9324
  });
8743
9325
  });
@@ -8747,6 +9329,8 @@ function createProgram(io = {}) {
8747
9329
  writeErr,
8748
9330
  setExitCode,
8749
9331
  authDir: io.authDir,
9332
+ env,
9333
+ remote: remoteClientForCli(io),
8750
9334
  format: options.format
8751
9335
  });
8752
9336
  });
@@ -8756,6 +9340,8 @@ function createProgram(io = {}) {
8756
9340
  writeErr,
8757
9341
  setExitCode,
8758
9342
  authDir: io.authDir,
9343
+ env,
9344
+ remote: remoteClientForCli(io),
8759
9345
  format: options.format
8760
9346
  });
8761
9347
  });
@@ -8772,6 +9358,8 @@ function createProgram(io = {}) {
8772
9358
  writeErr,
8773
9359
  setExitCode,
8774
9360
  authDir: io.authDir,
9361
+ env,
9362
+ remote: remoteClientForCli(io),
8775
9363
  format: options.format
8776
9364
  });
8777
9365
  });
@@ -8785,6 +9373,8 @@ function createProgram(io = {}) {
8785
9373
  writeErr,
8786
9374
  setExitCode,
8787
9375
  authDir: io.authDir,
9376
+ env,
9377
+ remote: remoteClientForCli(io),
8788
9378
  format: options.format
8789
9379
  });
8790
9380
  });
@@ -8800,15 +9390,17 @@ function createProgram(io = {}) {
8800
9390
  writeErr,
8801
9391
  setExitCode,
8802
9392
  authDir: io.authDir,
9393
+ env,
9394
+ remote: remoteClientForCli(io),
8803
9395
  format: options.format
8804
9396
  });
8805
9397
  });
8806
9398
  const config = program.command("config").description("Inspect Caplets config locations.");
8807
9399
  config.command("path").description("Print the effective user config path.").action(() => {
8808
- writeOut(`${resolveConfigPath(envConfigPath())}\n`);
9400
+ writeOut(`${resolveConfigPath(currentConfigPath())}\n`);
8809
9401
  });
8810
9402
  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);
9403
+ const paths = resolveCliConfigPaths(currentConfigPath(), io.authDir);
8812
9404
  if (options.json || options.format === "json") {
8813
9405
  writeOut(`${JSON.stringify(paths, null, 2)}\n`);
8814
9406
  return;
@@ -8817,7 +9409,19 @@ function createProgram(io = {}) {
8817
9409
  });
8818
9410
  const auth = program.command("auth").description("Manage OAuth credentials for remote servers.");
8819
9411
  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();
9412
+ const remote = remoteClientForCli(io);
9413
+ if (remote) {
9414
+ const started = await remote.request("auth_login_start", { server: serverId });
9415
+ if (started.authorizationUrl) {
9416
+ writeOut(`Open this URL to authorize ${serverId}:\n${started.authorizationUrl}\n`);
9417
+ if (options.open !== false) await openBrowser(started.authorizationUrl);
9418
+ writeOut("Complete authentication in your browser. The server callback will store credentials.\n");
9419
+ return;
9420
+ }
9421
+ if (started.authenticated) writeOut(`Authenticated \`${serverId}\`.\n`);
9422
+ return;
9423
+ }
9424
+ const configPath = currentConfigPath();
8821
9425
  await loginAuth(serverId, {
8822
9426
  noOpen: options.open === false,
8823
9427
  writeOut,
@@ -8826,27 +9430,78 @@ function createProgram(io = {}) {
8826
9430
  ...io.authDir ? { authDir: io.authDir } : {}
8827
9431
  });
8828
9432
  });
8829
- auth.command("logout").description("Delete stored OAuth credentials for a server.").argument("<server>", "configured server ID").action((serverId) => {
8830
- const configPath = envConfigPath();
9433
+ auth.command("logout").description("Delete stored OAuth credentials for a server.").argument("<server>", "configured server ID").action(async (serverId) => {
9434
+ const remote = remoteClientForCli(io);
9435
+ if (remote) {
9436
+ writeOut((await remote.request("auth_logout", { server: serverId })).deleted ? `Deleted remote OAuth credentials for \`${serverId}\`.\n` : `No remote OAuth credentials found for \`${serverId}\`.\n`);
9437
+ return;
9438
+ }
9439
+ const configPath = currentConfigPath();
8831
9440
  logoutAuth(serverId, {
8832
9441
  writeOut,
8833
9442
  ...configPath ? { configPath } : {},
8834
9443
  ...io.authDir ? { authDir: io.authDir } : {}
8835
9444
  });
8836
9445
  });
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();
9446
+ 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) => {
9447
+ const configPath = currentConfigPath();
9448
+ const format = options.json || options.format === "json" ? "json" : options.format ?? "plain";
9449
+ const remote = remoteClientForCli(io);
9450
+ if (remote) {
9451
+ const rows = await remote.request("auth_list", {});
9452
+ if (format === "json") {
9453
+ writeOut(`${JSON.stringify(rows, null, 2)}\n`);
9454
+ return;
9455
+ }
9456
+ writeOut(formatAuthRows(rows, format));
9457
+ return;
9458
+ }
8839
9459
  listAuth({
8840
9460
  writeOut,
8841
- format: options.json || options.format === "json" ? "json" : options.format ?? "plain",
9461
+ format,
8842
9462
  ...configPath ? { configPath } : {},
8843
9463
  ...io.authDir ? { authDir: io.authDir } : {}
8844
9464
  });
8845
9465
  });
8846
9466
  return program;
8847
9467
  }
8848
- function envConfigPath() {
8849
- return process.env.CAPLETS_CONFIG?.trim() || void 0;
9468
+ function envConfigPath(env) {
9469
+ return env.CAPLETS_CONFIG?.trim() || void 0;
9470
+ }
9471
+ function remoteClientForCli(io) {
9472
+ const env = io.env ?? process.env;
9473
+ if (resolveCapletsMode({}, env).mode !== "remote") return;
9474
+ return new RemoteControlClient(resolveCapletsServer(io.fetch ? { fetch: io.fetch } : {}, env));
9475
+ }
9476
+ async function openBrowser(url) {
9477
+ const { spawn } = await import("node:child_process");
9478
+ spawn(process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open", process.platform === "win32" ? [
9479
+ "/c",
9480
+ "start",
9481
+ "",
9482
+ url
9483
+ ] : [url], {
9484
+ stdio: "ignore",
9485
+ detached: true
9486
+ }).unref();
9487
+ }
9488
+ function remoteCommandForOperation(operation) {
9489
+ switch (operation) {
9490
+ case "get_caplet":
9491
+ case "check_backend":
9492
+ case "list_tools":
9493
+ case "search_tools":
9494
+ case "get_tool":
9495
+ case "call_tool": return operation;
9496
+ default: return;
9497
+ }
9498
+ }
9499
+ function remoteAddOptions(options) {
9500
+ const { output, print, global, destinationRoot, ...remoteOptions } = options;
9501
+ if (global) throw new CapletsError("REQUEST_INVALID", "--global is not supported in remote mode; the server controls the add destination.");
9502
+ if (print) throw new CapletsError("REQUEST_INVALID", "--print is not supported in remote mode; the server controls add output.");
9503
+ if (output !== void 0) throw new CapletsError("REQUEST_INVALID", "--output is not supported in remote mode; the server controls the add destination.");
9504
+ return remoteOptions;
8850
9505
  }
8851
9506
  function collect(value, previous) {
8852
9507
  previous.push(value);
@@ -8889,7 +9544,21 @@ function isPlainObject(value) {
8889
9544
  return value !== null && typeof value === "object" && !Array.isArray(value);
8890
9545
  }
8891
9546
  async function executeOperation(caplet, request, io) {
8892
- const configPath = envConfigPath();
9547
+ const command = remoteCommandForOperation(request.operation);
9548
+ if (io.remote && command) {
9549
+ const result = await io.remote.request(command, {
9550
+ caplet,
9551
+ request
9552
+ });
9553
+ const output = cliOutputForOperation(result, {
9554
+ ...request,
9555
+ caplet
9556
+ }, io.format ?? "markdown");
9557
+ io.writeOut(typeof output === "string" ? `${output}\n` : `${JSON.stringify(output, null, 2)}\n`);
9558
+ if (isPlainObject(result) && result.isError === true) io.setExitCode(1);
9559
+ return;
9560
+ }
9561
+ const configPath = envConfigPath(io.env ?? process.env);
8893
9562
  const engine = new CapletsEngine({
8894
9563
  ...configPath ? { configPath } : {},
8895
9564
  ...io.authDir ? { authDir: io.authDir } : {},
@@ -9135,12 +9804,12 @@ function schemaSummary(schema) {
9135
9804
  required.length > 0 ? `required ${required.join(", ")}` : "no required fields"
9136
9805
  ].filter((part) => Boolean(part)).join("; ");
9137
9806
  }
9138
- function addDestinationRoot(options) {
9139
- return options.global ? resolveCapletsRoot(resolveConfigPath(envConfigPath())) : resolveProjectCapletsRoot();
9807
+ function addDestinationRoot(options, configPath) {
9808
+ return options.global ? resolveCapletsRoot(resolveConfigPath(configPath)) : resolveProjectCapletsRoot();
9140
9809
  }
9141
9810
  function writeAddResult(writeOut, label, result) {
9142
9811
  if (result.path) {
9143
- writeOut(`Wrote ${label} Caplet to ${result.path}\n`);
9812
+ writeOut(`Wrote ${result.remote ? "remote " : ""}${label} Caplet to ${result.path}\n`);
9144
9813
  return;
9145
9814
  }
9146
9815
  writeOut(result.text);