caplets 0.15.0 → 0.16.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.
Files changed (3) hide show
  1. package/README.md +25 -16
  2. package/dist/index.js +980 -101
  3. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ import { homedir, tmpdir } from "node:os";
9
9
  import { PassThrough } from "node:stream";
10
10
  import { createServer } from "node:http";
11
11
  import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
12
+ import { Buffer as Buffer$1 } from "node:buffer";
12
13
  import { createInterface } from "node:readline/promises";
13
14
  import { createServer as createServer$1 } from "http";
14
15
  import { Http2ServerRequest, constants as constants$1 } from "http2";
@@ -72,7 +73,7 @@ function generatedToolInputJsonSchema() {
72
73
  };
73
74
  }
74
75
  //#endregion
75
- //#region ../core/dist/engine-Brwid_mq.js
76
+ //#region ../core/dist/options-CJEOqS87.js
76
77
  var __create = Object.create;
77
78
  var __defProp = Object.defineProperty;
78
79
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -95,6 +96,25 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
95
96
  enumerable: true
96
97
  }) : target, mod));
97
98
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
99
+ const CAPLETS_ERROR_CODES = [
100
+ "CONFIG_NOT_FOUND",
101
+ "CONFIG_EXISTS",
102
+ "CONFIG_INVALID",
103
+ "REQUEST_INVALID",
104
+ "SERVER_NOT_FOUND",
105
+ "SERVER_UNAVAILABLE",
106
+ "SERVER_START_TIMEOUT",
107
+ "UNKNOWN_OPERATION",
108
+ "TOOL_NOT_FOUND",
109
+ "TOOL_CALL_TIMEOUT",
110
+ "AUTH_REQUIRED",
111
+ "AUTH_FAILED",
112
+ "AUTH_REFRESH_FAILED",
113
+ "DOWNSTREAM_PROTOCOL_ERROR",
114
+ "DOWNSTREAM_TOOL_ERROR",
115
+ "UNSUPPORTED_TRANSPORT",
116
+ "INTERNAL_ERROR"
117
+ ];
98
118
  var CapletsError = class extends Error {
99
119
  code;
100
120
  details;
@@ -13442,7 +13462,7 @@ function loadConfigWithSources(path = resolveConfigPath(), projectPath = resolve
13442
13462
  const userConfig = hasUserConfig ? readPublicConfigInput(path) : void 0;
13443
13463
  const userCaplets = loadCapletFilesWithPaths(resolveCapletsRoot(path));
13444
13464
  const projectConfig = hasProjectConfig ? rejectProjectConfigExecutableBackendMaps(readPublicConfigInput(projectPath), projectPath) : void 0;
13445
- const projectCapletsRoot = resolveProjectCapletsRootForConfigPath(projectPath);
13465
+ const projectCapletsRoot = resolveProjectCapletsRootForConfigPath$1(projectPath);
13446
13466
  const projectCaplets = projectCapletsRoot ? loadCapletFilesWithPaths(projectCapletsRoot) : void 0;
13447
13467
  if (!hasUserConfig && !hasProjectConfig && !userCaplets && !projectCaplets) throw new CapletsError("CONFIG_NOT_FOUND", `Caplets config not found at ${path} or ${projectPath}`);
13448
13468
  try {
@@ -13496,7 +13516,7 @@ function loadIsolatedConfig(options) {
13496
13516
  if (Object.keys(config.mcpServers).length === 0 && Object.keys(config.openapiEndpoints).length === 0 && Object.keys(config.graphqlEndpoints).length === 0 && Object.keys(config.httpApis).length === 0 && Object.keys(config.cliTools).length === 0 && Object.keys(config.capletSets).length === 0) throw new CapletsError("CONFIG_INVALID", "Nested Caplet set must define at least one Caplet");
13497
13517
  return config;
13498
13518
  }
13499
- function resolveProjectCapletsRootForConfigPath(projectPath) {
13519
+ function resolveProjectCapletsRootForConfigPath$1(projectPath) {
13500
13520
  const root = dirname(projectPath);
13501
13521
  return basename(root) === ".caplets" && basename(projectPath) === "config.json" ? root : void 0;
13502
13522
  }
@@ -30743,8 +30763,45 @@ var FileOAuthProvider = class {
30743
30763
  headers.set("content-type", "application/x-www-form-urlencoded");
30744
30764
  };
30745
30765
  };
30746
- async function runOAuthFlow(server, options = {}) {
30766
+ async function startOAuthFlow(server, options) {
30747
30767
  if (server.transport === "stdio" || !server.url || server.auth?.type !== "oauth2" && server.auth?.type !== "oidc") throw new CapletsError("REQUEST_INVALID", `${server.server} is not a configured OAuth remote server`);
30768
+ let redirectUrl;
30769
+ const provider = new FileOAuthProvider(server, options.redirectUri, (url) => {
30770
+ redirectUrl = url;
30771
+ options.print?.(`Open this URL to authorize ${server.server}:\n${url.toString()}`);
30772
+ }, options.authDir);
30773
+ const scope = scopesFor(server.auth);
30774
+ try {
30775
+ if (await auth(provider, {
30776
+ serverUrl: server.url,
30777
+ ...scope ? { scope } : {}
30778
+ }) === "AUTHORIZED") return {
30779
+ authorizationUrl: "",
30780
+ complete: async () => {}
30781
+ };
30782
+ } catch (error) {
30783
+ throw normalizeMcpOAuthError(server, error);
30784
+ }
30785
+ if (!redirectUrl) throw new CapletsError("AUTH_FAILED", "OAuth authorization URL was not provided");
30786
+ return {
30787
+ authorizationUrl: redirectUrl.toString(),
30788
+ complete: async (callbackUrl) => {
30789
+ assertNoOAuthCallbackError(server, callbackUrl);
30790
+ const completion = extractCompletion(callbackUrl);
30791
+ if (completion.state !== provider.state()) throw new CapletsError("AUTH_FAILED", "OAuth callback state did not match");
30792
+ try {
30793
+ await auth(provider, {
30794
+ serverUrl: server.url,
30795
+ authorizationCode: completion.code,
30796
+ ...scope ? { scope } : {}
30797
+ });
30798
+ } catch (error) {
30799
+ throw normalizeMcpOAuthError(server, error);
30800
+ }
30801
+ }
30802
+ };
30803
+ }
30804
+ async function runOAuthFlow(server, options = {}) {
30748
30805
  let callbackCode;
30749
30806
  let callbackState;
30750
30807
  const callback = await createLoopbackCallback((url) => {
@@ -30752,37 +30809,43 @@ async function runOAuthFlow(server, options = {}) {
30752
30809
  callbackCode = url.searchParams.get("code") ?? void 0;
30753
30810
  callbackState = url.searchParams.get("state") ?? void 0;
30754
30811
  });
30755
- let redirectUrl;
30756
- const provider = new FileOAuthProvider(server, callback.redirectUri, (url) => {
30757
- redirectUrl = url;
30758
- options.print?.(`Open this URL to authorize ${server.server}:\n${url.toString()}`);
30759
- }, options.authDir);
30760
30812
  try {
30761
- const scope = scopesFor(server.auth);
30762
- const first = await auth(provider, {
30763
- serverUrl: server.url,
30764
- ...scope ? { scope } : {}
30813
+ const started = await startOAuthFlow(server, {
30814
+ redirectUri: callback.redirectUri,
30815
+ ...options.authDir ? { authDir: options.authDir } : {},
30816
+ ...options.print ? { print: options.print } : {}
30765
30817
  });
30766
- if (first === "AUTHORIZED") return first;
30767
- if (!options.noOpen && redirectUrl) await (options.open ? options.open(redirectUrl.toString()) : openBrowser(redirectUrl.toString()));
30818
+ if (!started.authorizationUrl) return "AUTHORIZED";
30819
+ if (!options.noOpen) await (options.open ? options.open(started.authorizationUrl) : openBrowser$1(started.authorizationUrl));
30768
30820
  const manualInput = options.manualInput ?? (options.noOpen ? await options.readManualInput?.() : void 0);
30769
30821
  const completion = manualInput ? extractCompletion(manualInput) : await callback.waitForCode(() => callbackCode ? {
30770
30822
  code: callbackCode,
30771
30823
  ...callbackState ? { state: callbackState } : {}
30772
30824
  } : void 0);
30773
- const expectedState = provider.state();
30774
- if (completion.state !== expectedState) throw new CapletsError("AUTH_FAILED", "OAuth callback state did not match");
30775
- return await auth(provider, {
30776
- serverUrl: server.url,
30777
- authorizationCode: completion.code,
30778
- ...scope ? { scope } : {}
30779
- });
30825
+ await started.complete(completion.state ? `${callback.redirectUri}?code=${encodeURIComponent(completion.code)}&state=${encodeURIComponent(completion.state)}` : `${callback.redirectUri}?code=${encodeURIComponent(completion.code)}`);
30826
+ return "AUTHORIZED";
30780
30827
  } catch (error) {
30781
30828
  throw normalizeMcpOAuthError(server, error);
30782
30829
  } finally {
30783
30830
  await callback.close();
30784
30831
  }
30785
30832
  }
30833
+ function assertNoOAuthCallbackError(target, callbackUrl) {
30834
+ let url;
30835
+ try {
30836
+ url = new URL(callbackUrl);
30837
+ } catch {
30838
+ return;
30839
+ }
30840
+ const error = url.searchParams.get("error");
30841
+ if (!error) return;
30842
+ const description = url.searchParams.get("error_description");
30843
+ throw new CapletsError("AUTH_FAILED", description ? `OAuth provider returned an error: ${description}` : "OAuth provider returned an error", redactSecrets({
30844
+ server: target.server,
30845
+ error,
30846
+ error_description: description ?? void 0
30847
+ }));
30848
+ }
30786
30849
  function normalizeMcpOAuthError(server, error) {
30787
30850
  if ((server.auth?.type === "oauth2" || server.auth?.type === "oidc") && !server.auth.clientId && !server.auth.clientMetadataUrl && error instanceof Error && error.message.includes("does not support dynamic client registration")) return new CapletsError("AUTH_FAILED", "OAuth is not available for this server without a host-specific OAuth app or PAT auth", {
30788
30851
  server: server.server,
@@ -30790,6 +30853,75 @@ function normalizeMcpOAuthError(server, error) {
30790
30853
  });
30791
30854
  return error;
30792
30855
  }
30856
+ async function startGenericOAuthFlow(target, options) {
30857
+ if (target.auth?.type !== "oauth2" && target.auth?.type !== "oidc") throw new CapletsError("REQUEST_INVALID", `${target.server} is not configured for OAuth`);
30858
+ const authConfig = target.auth;
30859
+ const redirectUri = authConfig.redirectUri ?? options.redirectUri;
30860
+ const verifier = base64url(randomBytes(32));
30861
+ const state = base64url(randomBytes(24));
30862
+ const allowLoopbackHttp = isLoopbackDevelopmentTarget(target, authConfig);
30863
+ const metadata = await discoverAuthorizationServer(target, authConfig, allowLoopbackHttp);
30864
+ const authorizationEndpoint = authConfig.authorizationUrl ?? metadata.authorization_endpoint;
30865
+ const tokenEndpoint = authConfig.tokenUrl ?? metadata.token_endpoint;
30866
+ if (!authorizationEndpoint || !tokenEndpoint) throw new CapletsError("AUTH_FAILED", "OAuth metadata is missing endpoints", { server: target.server });
30867
+ assertAllowedAuthUrl(authorizationEndpoint, "authorization endpoint", allowLoopbackHttp);
30868
+ assertAllowedAuthUrl(tokenEndpoint, "token endpoint", allowLoopbackHttp);
30869
+ const client = await resolveGenericClient(target, authConfig, metadata, redirectUri, allowLoopbackHttp);
30870
+ const scope = scopesFor(authConfig);
30871
+ const authorizationUrl = new URL(authorizationEndpoint);
30872
+ authorizationUrl.searchParams.set("response_type", "code");
30873
+ authorizationUrl.searchParams.set("client_id", client.clientId);
30874
+ authorizationUrl.searchParams.set("redirect_uri", redirectUri);
30875
+ authorizationUrl.searchParams.set("code_challenge", pkceChallenge(verifier));
30876
+ authorizationUrl.searchParams.set("code_challenge_method", "S256");
30877
+ authorizationUrl.searchParams.set("state", state);
30878
+ if (scope) authorizationUrl.searchParams.set("scope", scope);
30879
+ options.print?.(`Open this URL to authorize ${target.server}:\n${authorizationUrl.toString()}`);
30880
+ return {
30881
+ authorizationUrl: authorizationUrl.toString(),
30882
+ complete: async (callbackUrl) => {
30883
+ assertNoOAuthCallbackError(target, callbackUrl);
30884
+ const completion = extractCompletion(callbackUrl);
30885
+ if (completion.state !== state) throw new CapletsError("AUTH_FAILED", "OAuth callback state did not match");
30886
+ const params = new URLSearchParams({
30887
+ grant_type: "authorization_code",
30888
+ code: completion.code,
30889
+ redirect_uri: redirectUri,
30890
+ client_id: client.clientId,
30891
+ code_verifier: verifier
30892
+ });
30893
+ if (client.clientSecret) params.set("client_secret", client.clientSecret);
30894
+ const tokenResponse = await fetchJson(tokenEndpoint, target.requestTimeoutMs, {
30895
+ method: "POST",
30896
+ headers: { "content-type": "application/x-www-form-urlencoded" },
30897
+ body: params.toString()
30898
+ }, allowLoopbackHttp);
30899
+ const idToken = asString(tokenResponse.id_token);
30900
+ const idClaims = parseJwtPayload(idToken);
30901
+ validateOidcToken(authConfig, metadata, idToken, idClaims, client.clientId);
30902
+ writeTokenBundle(stripUndefined({
30903
+ server: target.server,
30904
+ authType: authConfig.type,
30905
+ accessToken: requireString(tokenResponse.access_token, "access_token"),
30906
+ refreshToken: asString(tokenResponse.refresh_token),
30907
+ tokenType: asString(tokenResponse.token_type),
30908
+ expiresAt: typeof tokenResponse.expires_in === "number" ? new Date(Date.now() + tokenResponse.expires_in * 1e3).toISOString() : void 0,
30909
+ scope: asString(tokenResponse.scope) ?? scope,
30910
+ idToken,
30911
+ issuer: asString(idClaims?.iss) ?? metadata.issuer ?? authConfig.issuer,
30912
+ subject: asString(idClaims?.sub),
30913
+ clientId: client.clientId,
30914
+ clientSecret: client.clientSecret,
30915
+ protectedResourceOrigin: protectedResourceOrigin(target, authConfig),
30916
+ metadata: redactSecrets({
30917
+ protectedResource: target.url ?? target.baseUrl ?? target.specUrl,
30918
+ authorizationServer: metadata,
30919
+ dynamicClient: client.dynamic ? { client_id: client.clientId } : void 0
30920
+ })
30921
+ }), options.authDir);
30922
+ }
30923
+ };
30924
+ }
30793
30925
  async function runGenericOAuthFlow(target, options = {}) {
30794
30926
  if (target.auth?.type !== "oauth2" && target.auth?.type !== "oidc") throw new CapletsError("REQUEST_INVALID", `${target.server} is not configured for OAuth`);
30795
30927
  const authConfig = target.auth;
@@ -30822,7 +30954,7 @@ async function runGenericOAuthFlow(target, options = {}) {
30822
30954
  authorizationUrl.searchParams.set("state", state);
30823
30955
  if (scope) authorizationUrl.searchParams.set("scope", scope);
30824
30956
  options.print?.(`Open this URL to authorize ${target.server}:\n${authorizationUrl.toString()}`);
30825
- if (!options.noOpen) await (options.open ? options.open(authorizationUrl.toString()) : openBrowser(authorizationUrl.toString()));
30957
+ if (!options.noOpen) await (options.open ? options.open(authorizationUrl.toString()) : openBrowser$1(authorizationUrl.toString()));
30826
30958
  const manualInput = options.manualInput ?? (options.noOpen ? await options.readManualInput?.() : void 0);
30827
30959
  const completion = manualInput ? extractCompletion(manualInput) : await callback.waitForCode(() => callbackCode ? {
30828
30960
  code: callbackCode,
@@ -30925,7 +31057,7 @@ async function createLoopbackCallback(onCallback) {
30925
31057
  close: () => new Promise((resolve) => server.close(() => resolve()))
30926
31058
  };
30927
31059
  }
30928
- async function openBrowser(url) {
31060
+ async function openBrowser$1(url) {
30929
31061
  const { spawn } = await import("node:child_process");
30930
31062
  spawn(process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open", process.platform === "win32" ? [
30931
31063
  "/c",
@@ -57259,6 +57391,90 @@ function isDirectory(path) {
57259
57391
  return false;
57260
57392
  }
57261
57393
  }
57394
+ const DEFAULT_SERVER_USER = "caplets";
57395
+ function resolveCapletsMode(input = {}, env = process.env) {
57396
+ const mode = parseCapletsMode(input.mode ?? env.CAPLETS_MODE ?? "auto");
57397
+ if (mode === "local") return { mode: "local" };
57398
+ const rawUrl = nonEmpty$1(input.serverUrl, "serverUrl") ?? nonEmpty$1(env.CAPLETS_SERVER_URL, "CAPLETS_SERVER_URL");
57399
+ if (mode === "remote") {
57400
+ if (rawUrl === void 0) throw new CapletsError("REQUEST_INVALID", "CAPLETS_MODE=remote requires CAPLETS_SERVER_URL or serverUrl.");
57401
+ return { mode: "remote" };
57402
+ }
57403
+ return rawUrl === void 0 ? { mode: "local" } : { mode: "remote" };
57404
+ }
57405
+ function resolveCapletsServer(input = {}, env = process.env) {
57406
+ const rawUrl = nonEmpty$1(input.url, "url") ?? nonEmpty$1(env.CAPLETS_SERVER_URL, "CAPLETS_SERVER_URL");
57407
+ if (rawUrl === void 0) throw new CapletsError("REQUEST_INVALID", "CAPLETS_SERVER_URL or url is required.");
57408
+ const baseUrl = parseServerBaseUrl(rawUrl);
57409
+ const userWasExplicit = input.user !== void 0 || hasEnv$1(env.CAPLETS_SERVER_USER);
57410
+ const user = nonEmpty$1(input.user, "user") ?? nonEmpty$1(env.CAPLETS_SERVER_USER, "CAPLETS_SERVER_USER") ?? DEFAULT_SERVER_USER;
57411
+ const password = nonEmpty$1(input.password, "password") ?? nonEmpty$1(env.CAPLETS_SERVER_PASSWORD, "CAPLETS_SERVER_PASSWORD");
57412
+ if (userWasExplicit && password === void 0) throw new CapletsError("REQUEST_INVALID", "Caplets server Basic Auth requires a password; set CAPLETS_SERVER_PASSWORD or password.");
57413
+ const auth = password === void 0 ? {
57414
+ enabled: false,
57415
+ user
57416
+ } : {
57417
+ enabled: true,
57418
+ user,
57419
+ password
57420
+ };
57421
+ const requestInit = auth.enabled ? { headers: { Authorization: basicAuthHeader(auth.user, auth.password) } } : {};
57422
+ return {
57423
+ baseUrl,
57424
+ mcpUrl: mcpUrlForBase(baseUrl),
57425
+ controlUrl: controlUrlForBase(baseUrl),
57426
+ healthUrl: healthUrlForBase(baseUrl),
57427
+ auth,
57428
+ requestInit,
57429
+ ...input.fetch ? { fetch: input.fetch } : {}
57430
+ };
57431
+ }
57432
+ function mcpUrlForBase(baseUrl) {
57433
+ return appendBasePath(baseUrl, "mcp");
57434
+ }
57435
+ function controlUrlForBase(baseUrl) {
57436
+ return appendBasePath(baseUrl, "control");
57437
+ }
57438
+ function healthUrlForBase(baseUrl) {
57439
+ return appendBasePath(baseUrl, "healthz");
57440
+ }
57441
+ function appendBasePath(baseUrl, path) {
57442
+ const url = new URL(baseUrl.href);
57443
+ url.pathname = `${url.pathname === "/" ? "" : url.pathname}/${path}`;
57444
+ return url;
57445
+ }
57446
+ function parseServerBaseUrl(value) {
57447
+ let url;
57448
+ try {
57449
+ url = new URL(value);
57450
+ } catch {
57451
+ throw new CapletsError("REQUEST_INVALID", "Invalid Caplets server URL.");
57452
+ }
57453
+ if (url.username !== "" || url.password !== "" || url.search !== "" || url.hash !== "") throw new CapletsError("REQUEST_INVALID", "Caplets server URL must not include username, password, query string, or fragment.");
57454
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && isLoopbackHost$1(url.hostname))) throw new CapletsError("REQUEST_INVALID", "Caplets server URL must use https except loopback development URLs.");
57455
+ url.pathname = url.pathname === "/" ? "/" : url.pathname.replace(/\/+$/u, "");
57456
+ return url;
57457
+ }
57458
+ function isLoopbackHost$1(host) {
57459
+ const normalized = host.toLocaleLowerCase();
57460
+ return normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1" || normalized === "[::1]";
57461
+ }
57462
+ function parseCapletsMode(value) {
57463
+ if (value === "auto" || value === "local" || value === "remote") return value;
57464
+ throw new CapletsError("REQUEST_INVALID", `Expected CAPLETS_MODE to be auto, local, or remote, got ${value}`);
57465
+ }
57466
+ function basicAuthHeader(user, password) {
57467
+ return `Basic ${Buffer$1.from(`${user}:${password}`).toString("base64")}`;
57468
+ }
57469
+ function nonEmpty$1(value, label) {
57470
+ if (value === void 0) return;
57471
+ const trimmed = value.trim();
57472
+ if (!trimmed) throw new CapletsError("REQUEST_INVALID", `${label} must not be empty`);
57473
+ return trimmed;
57474
+ }
57475
+ function hasEnv$1(value) {
57476
+ return value !== void 0 && value.trim() !== "";
57477
+ }
57262
57478
  //#endregion
57263
57479
  //#region ../core/dist/index.js
57264
57480
  /**
@@ -58557,7 +58773,7 @@ const EMPTY_COMPLETION_RESULT = { completion: {
58557
58773
  values: [],
58558
58774
  hasMore: false
58559
58775
  } };
58560
- var version$1 = "0.16.0";
58776
+ var version$1 = "0.17.0";
58561
58777
  var CapletsMcpSession = class {
58562
58778
  engine;
58563
58779
  server;
@@ -62090,49 +62306,56 @@ async function loginAuth(serverId, options) {
62090
62306
  }
62091
62307
  }
62092
62308
  function logoutAuth(serverId, options) {
62093
- assertLoginTarget(findAuthTarget(serverId, loadConfig(options.configPath)), serverId);
62094
- if (deleteTokenBundle(serverId, options.authDir)) options.writeOut(`Deleted OAuth credentials for \`${serverId}\`.\n`);
62309
+ if (logoutAuthResult(serverId, options).deleted) options.writeOut(`Deleted OAuth credentials for \`${serverId}\`.\n`);
62095
62310
  else options.writeOut(`No OAuth credentials found for \`${serverId}\`.\n`);
62096
62311
  }
62312
+ function logoutAuthResult(serverId, options) {
62313
+ assertLoginTarget(findAuthTarget(serverId, loadConfig(options.configPath)), serverId);
62314
+ return {
62315
+ server: serverId,
62316
+ deleted: deleteTokenBundle(serverId, options.authDir)
62317
+ };
62318
+ }
62097
62319
  function listAuth(options) {
62098
- const servers = authTargets(loadConfig(options.configPath)).sort((left, right) => left.server.localeCompare(right.server));
62320
+ const rows = listAuthRows(options);
62099
62321
  const format = options.format ?? "plain";
62100
62322
  if (format === "json") {
62101
- const rows = servers.map((server) => {
62102
- const bundle = readTokenBundle(server.server, options.authDir);
62103
- const status = !bundle ? "missing" : isTokenBundleExpired(bundle) ? "expired" : "authenticated";
62104
- return {
62105
- server: server.server,
62106
- status,
62107
- ...bundle?.expiresAt ? { expiresAt: bundle.expiresAt } : {},
62108
- ...bundle?.scope ? { scope: bundle.scope } : {}
62109
- };
62110
- });
62111
62323
  options.writeOut(`${JSON.stringify(rows, null, 2)}\n`);
62112
62324
  return;
62113
62325
  }
62114
- if (servers.length === 0) {
62115
- options.writeOut(format === "markdown" ? "## OAuth credentials\n\nNo configured remote OAuth servers found.\n" : "No configured remote OAuth servers found.\n");
62116
- return;
62117
- }
62118
- if (format === "markdown") options.writeOut("## OAuth credentials\n\n");
62119
- else options.writeOut("OAuth credentials\n\n");
62120
- for (const server of servers) {
62326
+ options.writeOut(formatAuthRows(rows, format));
62327
+ }
62328
+ function listAuthRows(options) {
62329
+ return authTargets(loadConfig(options.configPath)).sort((left, right) => left.server.localeCompare(right.server)).map((server) => {
62121
62330
  const bundle = readTokenBundle(server.server, options.authDir);
62122
62331
  const status = !bundle ? "missing" : isTokenBundleExpired(bundle) ? "expired" : "authenticated";
62123
- const details = [bundle?.expiresAt ? `expires ${bundle.expiresAt}` : void 0, bundle?.scope ? `scope ${bundle.scope}` : void 0].filter(Boolean).join("; ");
62332
+ return {
62333
+ server: server.server,
62334
+ status,
62335
+ ...bundle?.expiresAt ? { expiresAt: bundle.expiresAt } : {},
62336
+ ...bundle?.scope ? { scope: bundle.scope } : {}
62337
+ };
62338
+ });
62339
+ }
62340
+ function formatAuthRows(rows, format) {
62341
+ if (rows.length === 0) return format === "markdown" ? "## OAuth credentials\n\nNo configured remote OAuth servers found.\n" : "No configured remote OAuth servers found.\n";
62342
+ let output = "";
62343
+ if (format === "markdown") output += "## OAuth credentials\n\n";
62344
+ else output += "OAuth credentials\n\n";
62345
+ for (const row of rows) {
62346
+ const details = [row.expiresAt ? `expires ${row.expiresAt}` : void 0, row.scope ? `scope ${row.scope}` : void 0].filter(Boolean).join("; ");
62124
62347
  if (format === "markdown") {
62125
- options.writeOut(`- \`${server.server}\` — ${status}${details ? ` (${details})` : ""}\n`);
62348
+ output += `- \`${row.server}\` — ${row.status}${details ? ` (${details})` : ""}\n`;
62126
62349
  continue;
62127
62350
  }
62128
- options.writeOut([
62129
- server.server,
62130
- ` Status: ${status}`,
62131
- ...bundle?.expiresAt ? [` Expires: ${bundle.expiresAt}`] : [],
62132
- ...bundle?.scope ? [` Scope: ${bundle.scope}`] : []
62133
- ].join("\n"));
62134
- options.writeOut("\n\n");
62351
+ output += [
62352
+ row.server,
62353
+ ` Status: ${row.status}`,
62354
+ ...row.expiresAt ? [` Expires: ${row.expiresAt}`] : [],
62355
+ ...row.scope ? [` Scope: ${row.scope}`] : []
62356
+ ].join("\n") + "\n\n";
62135
62357
  }
62358
+ return output;
62136
62359
  }
62137
62360
  function findAuthTarget(serverId, config = loadConfig()) {
62138
62361
  return authTargets(config).find((server) => server.server === serverId);
@@ -62552,6 +62775,94 @@ function nearestExistingParent(path) {
62552
62775
  if (parent === path) return parent;
62553
62776
  return nearestExistingParent(parent);
62554
62777
  }
62778
+ var RemoteControlClient = class {
62779
+ #baseUrl;
62780
+ #requestInit;
62781
+ #fetch;
62782
+ constructor(options) {
62783
+ this.#baseUrl = options.baseUrl;
62784
+ this.#requestInit = options.requestInit;
62785
+ this.#fetch = options.fetch ?? fetch;
62786
+ }
62787
+ async request(command, args) {
62788
+ const controlUrl = controlUrlForBase(this.#baseUrl);
62789
+ let response;
62790
+ try {
62791
+ response = await this.#fetch(controlUrl, {
62792
+ ...this.#requestInit,
62793
+ method: "POST",
62794
+ headers: mergeJsonHeaders(this.#requestInit.headers),
62795
+ body: JSON.stringify({
62796
+ command,
62797
+ arguments: args
62798
+ })
62799
+ });
62800
+ } catch (error) {
62801
+ throw new CapletsError("SERVER_UNAVAILABLE", `Could not connect to Caplets server at ${safeBaseUrl(this.#baseUrl)}.`, toSafeError(error, "SERVER_UNAVAILABLE"));
62802
+ }
62803
+ if (response.status === 401 || response.status === 403) throw new CapletsError("AUTH_FAILED", "Caplets server authentication failed. Check CAPLETS_SERVER_USER and CAPLETS_SERVER_PASSWORD.");
62804
+ if (!response.ok) throw new CapletsError("SERVER_UNAVAILABLE", `Caplets server at ${safeBaseUrl(this.#baseUrl)} returned HTTP ${response.status}.`);
62805
+ const payload = await parseRemoteCliResponse(response);
62806
+ if (!payload.ok) throw new CapletsError(payload.error.code, redactRemoteMessage(payload.error.message), payload.error.nextAction === void 0 ? void 0 : { nextAction: payload.error.nextAction });
62807
+ return payload.result;
62808
+ }
62809
+ };
62810
+ function mergeJsonHeaders(headers) {
62811
+ const merged = new Headers(headers);
62812
+ merged.set("content-type", "application/json");
62813
+ return merged;
62814
+ }
62815
+ function safeBaseUrl(baseUrl) {
62816
+ const safe = new URL(baseUrl.href);
62817
+ safe.username = "";
62818
+ safe.password = "";
62819
+ safe.search = "";
62820
+ safe.hash = "";
62821
+ return safe.toString();
62822
+ }
62823
+ async function parseRemoteCliResponse(response) {
62824
+ let payload;
62825
+ try {
62826
+ payload = await response.json();
62827
+ } catch (error) {
62828
+ throw invalidRemoteControlResponse(error);
62829
+ }
62830
+ if (!isRecord(payload)) throw invalidRemoteControlResponse();
62831
+ if (payload.ok === true) {
62832
+ if (!("result" in payload)) throw invalidRemoteControlResponse();
62833
+ return {
62834
+ ok: true,
62835
+ result: payload.result
62836
+ };
62837
+ }
62838
+ if (payload.ok === false) {
62839
+ const error = payload.error;
62840
+ if (!isRecord(error) || typeof error.code !== "string" || typeof error.message !== "string") throw invalidRemoteControlResponse();
62841
+ if ("nextAction" in error && error.nextAction !== void 0 && typeof error.nextAction !== "string") throw invalidRemoteControlResponse();
62842
+ const errorResponse = {
62843
+ ok: false,
62844
+ error: {
62845
+ code: isCapletsErrorCode(error.code) ? error.code : "DOWNSTREAM_TOOL_ERROR",
62846
+ message: error.message
62847
+ }
62848
+ };
62849
+ if (typeof error.nextAction === "string") errorResponse.error.nextAction = error.nextAction;
62850
+ return errorResponse;
62851
+ }
62852
+ throw invalidRemoteControlResponse();
62853
+ }
62854
+ function invalidRemoteControlResponse(cause) {
62855
+ return new CapletsError("DOWNSTREAM_PROTOCOL_ERROR", "Caplets server returned an invalid remote control response.", cause === void 0 ? void 0 : toSafeError(cause, "DOWNSTREAM_PROTOCOL_ERROR"));
62856
+ }
62857
+ function isRecord(value) {
62858
+ return value !== null && typeof value === "object" && !Array.isArray(value);
62859
+ }
62860
+ function isCapletsErrorCode(value) {
62861
+ return CAPLETS_ERROR_CODES.includes(value);
62862
+ }
62863
+ function redactRemoteMessage(message) {
62864
+ 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]");
62865
+ }
62555
62866
  var compose = (middleware, onError, onNotFound) => {
62556
62867
  return (context, next) => {
62557
62868
  let index = -1;
@@ -65421,29 +65732,343 @@ var logger = (fn = console.log) => {
65421
65732
  await log(fn, "-->", method, path, c.res.status, time(start));
65422
65733
  };
65423
65734
  };
65735
+ const ENGINE_COMMANDS = new Set([
65736
+ "get_caplet",
65737
+ "check_backend",
65738
+ "list_tools",
65739
+ "search_tools",
65740
+ "get_tool",
65741
+ "call_tool"
65742
+ ]);
65743
+ async function dispatchRemoteCliRequest(request, context) {
65744
+ try {
65745
+ return {
65746
+ ok: true,
65747
+ result: await dispatch(request, context)
65748
+ };
65749
+ } catch (error) {
65750
+ const safe = toSafeError(error);
65751
+ const action = nextAction(safe.details);
65752
+ return {
65753
+ ok: false,
65754
+ error: {
65755
+ code: safe.code,
65756
+ message: redactControlErrorMessage(safe.message),
65757
+ ...action ? { nextAction: action } : {}
65758
+ }
65759
+ };
65760
+ }
65761
+ }
65762
+ async function dispatch(request, context) {
65763
+ assertObject(request, "remote control request");
65764
+ assertObject(request.arguments, "remote control request arguments");
65765
+ if (request.command === "list") return listCaplets(loadConfigWithSources(context.configPath, context.projectConfigPath), { includeDisabled: optionalBoolean(request.arguments, "includeDisabled") ?? false });
65766
+ if (ENGINE_COMMANDS.has(request.command)) {
65767
+ const caplet = requiredString(request.arguments, "caplet");
65768
+ const toolRequest = requiredEngineRequest(request.arguments, request.command);
65769
+ const engine = new CapletsEngine(context);
65770
+ try {
65771
+ return await engine.execute(caplet, toolRequest);
65772
+ } finally {
65773
+ await engine.close();
65774
+ }
65775
+ }
65776
+ if (request.command === "init") return {
65777
+ remote: true,
65778
+ path: initConfig({
65779
+ ...optionalProp("path", context.configPath),
65780
+ ...optionalProp("force", optionalBoolean(request.arguments, "force"))
65781
+ })
65782
+ };
65783
+ if (request.command === "add") return dispatchAdd(request.arguments, context);
65784
+ if (request.command === "install") return {
65785
+ remote: true,
65786
+ ...installCaplets(requiredString(request.arguments, "repo"), {
65787
+ ...optionalProp("capletIds", optionalStringArray(request.arguments, "capletIds")),
65788
+ destinationRoot: context.projectCapletsRoot,
65789
+ ...optionalProp("force", optionalBoolean(request.arguments, "force"))
65790
+ })
65791
+ };
65792
+ if (request.command === "auth_list") return listAuthRows({
65793
+ ...optionalProp("configPath", context.configPath),
65794
+ ...optionalProp("authDir", context.authDir)
65795
+ });
65796
+ if (request.command === "auth_logout") return logoutAuthResult(requiredString(request.arguments, "server"), {
65797
+ ...optionalProp("configPath", context.configPath),
65798
+ ...optionalProp("authDir", context.authDir)
65799
+ });
65800
+ if (request.command === "auth_login_start") return startRemoteAuthLogin(requiredString(request.arguments, "server"), context);
65801
+ if (request.command === "auth_login_complete") return completeRemoteAuthLogin(requiredString(request.arguments, "flowId"), requiredString(request.arguments, "callbackUrl"), context);
65802
+ throw new CapletsError("UNKNOWN_OPERATION", `Unsupported remote control command ${request.command}`);
65803
+ }
65804
+ async function startRemoteAuthLogin(serverId, context) {
65805
+ if (!context.authFlowStore || !context.controlCallbackBaseUrl) throw new CapletsError("REQUEST_INVALID", "Remote auth login is not available on this server");
65806
+ const config = loadConfigWithSources(context.configPath, context.projectConfigPath).config;
65807
+ const target = findAuthTarget(serverId, config);
65808
+ assertLoginTarget(target, serverId);
65809
+ const flowId = randomUUID();
65810
+ const baseUrl = context.controlCallbackBaseUrl.endsWith("/") ? context.controlCallbackBaseUrl : `${context.controlCallbackBaseUrl}/`;
65811
+ const redirectUri = new URL(`auth/callback/${flowId}`, baseUrl).toString();
65812
+ const started = target.backend === "mcp" ? await startOAuthFlow(target, {
65813
+ redirectUri,
65814
+ ...optionalProp("authDir", context.authDir)
65815
+ }) : await startGenericOAuthFlow(target, {
65816
+ redirectUri,
65817
+ ...optionalProp("authDir", context.authDir)
65818
+ });
65819
+ if (!started.authorizationUrl) return {
65820
+ server: serverId,
65821
+ authenticated: true
65822
+ };
65823
+ const flow = context.authFlowStore.create({
65824
+ server: serverId,
65825
+ authorizationUrl: started.authorizationUrl,
65826
+ complete: started.complete
65827
+ }, flowId);
65828
+ return {
65829
+ server: serverId,
65830
+ flowId: flow.id,
65831
+ authorizationUrl: flow.authorizationUrl
65832
+ };
65833
+ }
65834
+ async function completeRemoteAuthLogin(flowId, callbackUrl, context) {
65835
+ const flow = context.authFlowStore?.get(flowId);
65836
+ if (!flow) throw new CapletsError("REQUEST_INVALID", `Unknown auth flow ${flowId}`);
65837
+ context.authFlowStore?.delete(flowId);
65838
+ await flow.complete(callbackUrl);
65839
+ return {
65840
+ server: flow.server,
65841
+ authenticated: true
65842
+ };
65843
+ }
65844
+ function dispatchAdd(args, context) {
65845
+ const kind = requiredString(args, "kind");
65846
+ const id = requiredString(args, "id");
65847
+ const options = remoteAddOptions$1(kind, optionalObject(args, "options"));
65848
+ switch (kind) {
65849
+ case "cli": return {
65850
+ remote: true,
65851
+ label: "CLI",
65852
+ ...addCliCaplet(id, {
65853
+ ...options,
65854
+ destinationRoot: context.projectCapletsRoot,
65855
+ print: false
65856
+ })
65857
+ };
65858
+ case "mcp": return {
65859
+ remote: true,
65860
+ label: "MCP",
65861
+ ...addMcpCaplet(id, {
65862
+ ...options,
65863
+ destinationRoot: context.projectCapletsRoot,
65864
+ print: false
65865
+ })
65866
+ };
65867
+ case "openapi": return {
65868
+ remote: true,
65869
+ label: "OpenAPI",
65870
+ ...addOpenApiCaplet(id, {
65871
+ ...options,
65872
+ destinationRoot: context.projectCapletsRoot,
65873
+ print: false
65874
+ })
65875
+ };
65876
+ case "graphql": return {
65877
+ remote: true,
65878
+ label: "GraphQL",
65879
+ ...addGraphqlCaplet(id, {
65880
+ ...options,
65881
+ destinationRoot: context.projectCapletsRoot,
65882
+ print: false
65883
+ })
65884
+ };
65885
+ case "http": return {
65886
+ remote: true,
65887
+ label: "HTTP",
65888
+ ...addHttpCaplet(id, {
65889
+ ...options,
65890
+ destinationRoot: context.projectCapletsRoot,
65891
+ print: false
65892
+ })
65893
+ };
65894
+ default: throw new CapletsError("REQUEST_INVALID", "add.kind must be cli, mcp, openapi, graphql, or http");
65895
+ }
65896
+ }
65897
+ function optionalProp(key, value) {
65898
+ return value === void 0 ? {} : { [key]: value };
65899
+ }
65900
+ function assertObject(value, label) {
65901
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw new CapletsError("REQUEST_INVALID", `${label} must be an object`);
65902
+ }
65903
+ function requiredString(args, key) {
65904
+ const value = args[key];
65905
+ if (typeof value !== "string" || value.length === 0) throw new CapletsError("REQUEST_INVALID", `${key} must be a non-empty string`);
65906
+ return value;
65907
+ }
65908
+ function optionalObject(args, key) {
65909
+ const value = args[key];
65910
+ if (value === void 0) return {};
65911
+ assertObject(value, key);
65912
+ return value;
65913
+ }
65914
+ function requiredEngineRequest(args, command) {
65915
+ const toolRequest = optionalObject(args, "request");
65916
+ if (typeof toolRequest.operation !== "string") throw new CapletsError("REQUEST_INVALID", "request.operation must be a string");
65917
+ if (toolRequest.operation !== command) throw new CapletsError("REQUEST_INVALID", `request.operation must match remote command ${command}`);
65918
+ return toolRequest;
65919
+ }
65920
+ function remoteAddOptions$1(kind, options) {
65921
+ rejectServerOwnedAddOptions(options);
65922
+ switch (kind) {
65923
+ case "cli": return pickOptions(options, {
65924
+ repo: "string",
65925
+ include: "string",
65926
+ command: "string",
65927
+ force: "boolean"
65928
+ });
65929
+ case "mcp": return pickOptions(options, {
65930
+ command: "string",
65931
+ arg: "string-array",
65932
+ cwd: "string",
65933
+ env: "string-array",
65934
+ url: "string",
65935
+ transport: "string",
65936
+ tokenEnv: "string",
65937
+ force: "boolean"
65938
+ });
65939
+ case "openapi": return pickOptions(options, {
65940
+ spec: "string",
65941
+ baseUrl: "string",
65942
+ tokenEnv: "string",
65943
+ force: "boolean"
65944
+ });
65945
+ case "graphql": return pickOptions(options, {
65946
+ endpointUrl: "string",
65947
+ schema: "string",
65948
+ introspection: "boolean",
65949
+ tokenEnv: "string",
65950
+ force: "boolean"
65951
+ });
65952
+ case "http": return pickOptions(options, {
65953
+ baseUrl: "string",
65954
+ action: "string-array",
65955
+ tokenEnv: "string",
65956
+ force: "boolean"
65957
+ });
65958
+ default: return options;
65959
+ }
65960
+ }
65961
+ function pickOptions(options, schema) {
65962
+ const next = {};
65963
+ for (const [key, type] of Object.entries(schema)) {
65964
+ const value = options[key];
65965
+ if (value === void 0) continue;
65966
+ validateOptionType(key, value, type);
65967
+ next[key] = value;
65968
+ }
65969
+ return next;
65970
+ }
65971
+ function rejectServerOwnedAddOptions(options) {
65972
+ if ("output" in options) throw new CapletsError("REQUEST_INVALID", "Remote add output is not supported remotely; the server owns destinationRoot and output path selection");
65973
+ 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`);
65974
+ }
65975
+ function validateOptionType(key, value, type) {
65976
+ if (type === "string" && typeof value !== "string") throw new CapletsError("REQUEST_INVALID", `add.options.${key} must be a string`);
65977
+ if (type === "boolean" && typeof value !== "boolean") throw new CapletsError("REQUEST_INVALID", `add.options.${key} must be a boolean`);
65978
+ if (type === "string-array") {
65979
+ if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) throw new CapletsError("REQUEST_INVALID", `add.options.${key} must be an array of strings`);
65980
+ }
65981
+ }
65982
+ function optionalBoolean(args, key) {
65983
+ const value = args[key];
65984
+ if (value === void 0) return;
65985
+ if (typeof value !== "boolean") throw new CapletsError("REQUEST_INVALID", `${key} must be a boolean`);
65986
+ return value;
65987
+ }
65988
+ function optionalStringArray(args, key) {
65989
+ const value = args[key];
65990
+ if (value === void 0) return;
65991
+ if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) throw new CapletsError("REQUEST_INVALID", `${key} must be an array of strings`);
65992
+ return value;
65993
+ }
65994
+ function nextAction(details) {
65995
+ if (details && typeof details === "object" && "nextAction" in details) {
65996
+ const value = details.nextAction;
65997
+ return typeof value === "string" ? value : void 0;
65998
+ }
65999
+ }
66000
+ function redactControlErrorMessage(message) {
66001
+ 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]");
66002
+ }
66003
+ const DEFAULT_AUTH_FLOW_TTL_MS = 600 * 1e3;
66004
+ var RemoteAuthFlowStore = class {
66005
+ options;
66006
+ flows = /* @__PURE__ */ new Map();
66007
+ constructor(options = {}) {
66008
+ this.options = options;
66009
+ }
66010
+ create(flow, id = randomUUID()) {
66011
+ this.pruneExpired();
66012
+ const created = {
66013
+ id,
66014
+ createdAt: this.now(),
66015
+ ...flow
66016
+ };
66017
+ this.flows.set(created.id, created);
66018
+ return { ...created };
66019
+ }
66020
+ get(id) {
66021
+ this.pruneExpired();
66022
+ const flow = this.flows.get(id);
66023
+ if (flow && this.isExpired(flow)) {
66024
+ this.flows.delete(id);
66025
+ return;
66026
+ }
66027
+ return flow;
66028
+ }
66029
+ delete(id) {
66030
+ this.flows.delete(id);
66031
+ }
66032
+ pruneExpired() {
66033
+ for (const [id, flow] of this.flows) if (this.isExpired(flow)) this.flows.delete(id);
66034
+ }
66035
+ isExpired(flow) {
66036
+ return this.now() - flow.createdAt > (this.options.ttlMs ?? DEFAULT_AUTH_FLOW_TTL_MS);
66037
+ }
66038
+ now() {
66039
+ return this.options.now?.() ?? Date.now();
66040
+ }
66041
+ };
65424
66042
  function createHttpServeApp(options, engine, io = {}) {
65425
66043
  const app = new Hono();
65426
66044
  const sessions = /* @__PURE__ */ new Map();
65427
66045
  const writeErr = io.writeErr ?? process.stderr.write.bind(process.stderr);
66046
+ const paths = servicePaths(options.path);
66047
+ const authFlowStore = io.authFlowStore ?? new RemoteAuthFlowStore();
65428
66048
  app.use("*", logger((message, ...rest) => {
65429
66049
  writeErr(`${[message, ...rest].join(" ")}\n`);
65430
66050
  }));
65431
- app.get("/", (c) => c.json({
66051
+ app.get(paths.base, (c) => c.json({
65432
66052
  name: "caplets",
65433
66053
  transport: "http",
65434
- mcp: options.path,
65435
- health: "/healthz",
66054
+ base: paths.base,
66055
+ mcp: paths.mcp,
66056
+ control: paths.control,
66057
+ health: paths.health,
65436
66058
  auth: {
65437
66059
  type: "basic",
65438
66060
  enabled: options.auth.enabled
65439
66061
  }
65440
66062
  }));
65441
- app.get("/healthz", (c) => c.json({
66063
+ app.get(paths.health, (c) => c.json({
65442
66064
  status: "ok",
65443
66065
  transport: "http",
65444
- mcpPath: options.path
66066
+ base: paths.base,
66067
+ mcpPath: paths.mcp,
66068
+ controlPath: paths.control,
66069
+ healthPath: paths.health
65445
66070
  }));
65446
- app.all(options.path, basicAuth(options.auth), async (c) => {
66071
+ app.all(paths.mcp, basicAuth(options.auth), async (c) => {
65447
66072
  const sessionId = c.req.header("mcp-session-id");
65448
66073
  if (sessionId) {
65449
66074
  const existing = sessions.get(sessionId);
@@ -65474,6 +66099,36 @@ function createHttpServeApp(options, engine, io = {}) {
65474
66099
  sessions.set(nextSessionId, session);
65475
66100
  return session.transport.handleRequest(c);
65476
66101
  });
66102
+ app.post(paths.control, basicAuth(options.auth), async (c) => {
66103
+ let request;
66104
+ try {
66105
+ const parsed = await c.req.json();
66106
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new CapletsError("REQUEST_INVALID", "Control request JSON must be an object");
66107
+ request = parsed;
66108
+ } catch (error) {
66109
+ const safe = toSafeError(error instanceof CapletsError ? error : new CapletsError("REQUEST_INVALID", "Control request body must be valid JSON", error), "REQUEST_INVALID");
66110
+ return c.json({
66111
+ ok: false,
66112
+ error: {
66113
+ code: safe.code,
66114
+ message: safe.message
66115
+ }
66116
+ });
66117
+ }
66118
+ return c.json(await dispatchRemoteCliRequest(request, controlContext(io, writeErr, authFlowStore, c.req.url, paths.control, options.trustProxy, (name) => c.req.header(name))));
66119
+ });
66120
+ app.get(routePath(paths.control, "auth/callback/:flowId"), async (c) => {
66121
+ const flowId = c.req.param("flowId");
66122
+ const result = await dispatchRemoteCliRequest({
66123
+ command: "auth_login_complete",
66124
+ arguments: {
66125
+ flowId,
66126
+ callbackUrl: c.req.url
66127
+ }
66128
+ }, controlContext(io, writeErr, authFlowStore, c.req.url, paths.control, options.trustProxy, (name) => c.req.header(name)));
66129
+ if (!result.ok) writeErr(`Caplets authentication failed for flow ${flowId}: ${result.error.message}\n`);
66130
+ 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);
66131
+ });
65477
66132
  app.notFound((c) => c.json({ error: "not_found" }, 404));
65478
66133
  app.closeCapletsSessions = async () => {
65479
66134
  await Promise.allSettled([...sessions.values()].map(async (session) => {
@@ -65484,19 +66139,66 @@ function createHttpServeApp(options, engine, io = {}) {
65484
66139
  if (options.warnUnauthenticatedNetwork) writeErr(`Warning: Caplets MCP HTTP server is listening on ${options.host} without authentication.\n`);
65485
66140
  return app;
65486
66141
  }
66142
+ function controlContext(io, writeErr, authFlowStore, requestUrl, controlPath, trustProxy, header) {
66143
+ return {
66144
+ ...io.control,
66145
+ projectCapletsRoot: io.control?.projectCapletsRoot ?? resolveProjectCapletsRoot(),
66146
+ authFlowStore,
66147
+ controlCallbackBaseUrl: new URL(controlPath, publicRequestOrigin(requestUrl, trustProxy, header)).toString(),
66148
+ writeErr
66149
+ };
66150
+ }
66151
+ function publicRequestOrigin(requestUrl, trustProxy, header) {
66152
+ const url = new URL(requestUrl);
66153
+ if (!trustProxy) return `${url.protocol.slice(0, -1)}://${header("host") ?? url.host}`;
66154
+ const forwardedProto = firstForwardedValue(header("x-forwarded-proto"));
66155
+ const forwardedHost = firstForwardedValue(header("x-forwarded-host"));
66156
+ return `${forwardedProto === "http" || forwardedProto === "https" ? forwardedProto : url.protocol.slice(0, -1)}://${forwardedHost ?? header("host") ?? url.host}`;
66157
+ }
66158
+ function firstForwardedValue(value) {
66159
+ return value?.split(",", 1)[0]?.trim() || void 0;
66160
+ }
65487
66161
  async function serveHttp(options, engineOptions = {}, writeErr = (value) => process.stderr.write(value)) {
65488
66162
  const engine = new CapletsEngine(engineOptions);
65489
- const app = createHttpServeApp(options, engine, { writeErr });
66163
+ const app = createHttpServeApp(options, engine, {
66164
+ writeErr,
66165
+ control: {
66166
+ ...engineOptions,
66167
+ projectCapletsRoot: projectCapletsRootForEngineOptions(engineOptions)
66168
+ }
66169
+ });
66170
+ const paths = servicePaths(options.path);
66171
+ const origin = `http://${formatHost(options.host)}:${options.port}`;
66172
+ const baseUrl = `${origin}${paths.base === "/" ? "" : paths.base}`;
65490
66173
  installHttpSignalHandlers(serve({
65491
66174
  fetch: app.fetch,
65492
66175
  hostname: options.host,
65493
66176
  port: options.port
65494
66177
  }, () => {
65495
- writeErr(`Caplets MCP HTTP server listening on http://${formatHost(options.host)}:${options.port}${options.path}\n`);
65496
- writeErr(`Health check: http://${formatHost(options.host)}:${options.port}/healthz\n`);
66178
+ writeErr(`Caplets HTTP service listening on ${baseUrl}\n`);
66179
+ writeErr(`MCP endpoint: ${origin}${paths.mcp}\n`);
66180
+ writeErr(`Control endpoint: ${origin}${paths.control}\n`);
66181
+ writeErr(`Health check: ${origin}${paths.health}\n`);
65497
66182
  writeErr(`Basic Auth: ${options.auth.enabled ? `enabled (user: ${options.auth.user})` : "disabled"}\n`);
65498
66183
  }), app, engine, writeErr);
65499
66184
  }
66185
+ function projectCapletsRootForEngineOptions(engineOptions) {
66186
+ return engineOptions.projectConfigPath ? resolveProjectCapletsRootForConfigPath(engineOptions.projectConfigPath) : resolveProjectCapletsRoot();
66187
+ }
66188
+ function resolveProjectCapletsRootForConfigPath(projectConfigPath) {
66189
+ return dirname(projectConfigPath);
66190
+ }
66191
+ function routePath(base, path) {
66192
+ return base === "/" ? `/${path}` : `${base}/${path}`;
66193
+ }
66194
+ function servicePaths(base) {
66195
+ return {
66196
+ base,
66197
+ mcp: routePath(base, "mcp"),
66198
+ control: routePath(base, "control"),
66199
+ health: routePath(base, "healthz")
66200
+ };
66201
+ }
65500
66202
  async function createHttpSession(engine, sessionId, options, onClose) {
65501
66203
  const transport = new StreamableHTTPTransport({
65502
66204
  sessionIdGenerator: () => sessionId,
@@ -65574,7 +66276,8 @@ const HTTP_ONLY_OPTIONS = [
65574
66276
  "path",
65575
66277
  "user",
65576
66278
  "password",
65577
- "allowUnauthenticatedHttp"
66279
+ "allowUnauthenticatedHttp",
66280
+ "trustProxy"
65578
66281
  ];
65579
66282
  function resolveServeOptions(raw, env = process.env) {
65580
66283
  const transport = parseTransport(raw.transport ?? "stdio");
@@ -65583,9 +66286,10 @@ function resolveServeOptions(raw, env = process.env) {
65583
66286
  if (invalid.length > 0) throw new CapletsError("REQUEST_INVALID", `${invalid.map((key) => `--${key}`).join(", ")} ${invalid.length === 1 ? "is" : "are"} only valid with --transport http`);
65584
66287
  return { transport };
65585
66288
  }
65586
- const host = nonEmpty(raw.host, "--host") ?? "127.0.0.1";
65587
- const port = parsePort(raw.port ?? 5387);
65588
- const path = normalizeHttpPath(raw.path ?? "/mcp");
66289
+ const serverUrl = env.CAPLETS_SERVER_URL ? parseServeServerUrl(nonEmpty(env.CAPLETS_SERVER_URL, "CAPLETS_SERVER_URL")) : void 0;
66290
+ const host = nonEmpty(raw.host, "--host") ?? serverUrlHost(serverUrl) ?? "127.0.0.1";
66291
+ const port = parsePort(raw.port ?? (serverUrl?.port ? Number(serverUrl.port) : 5387));
66292
+ const path = normalizeHttpPath(raw.path ?? serverUrl?.pathname ?? "/");
65589
66293
  const userWasExplicit = raw.user !== void 0 || hasEnv(env.CAPLETS_SERVER_USER);
65590
66294
  const user = nonEmpty(raw.user, "--user") ?? nonEmpty(env.CAPLETS_SERVER_USER, "CAPLETS_SERVER_USER") ?? "caplets";
65591
66295
  const password = nonEmpty(raw.password, "--password") ?? nonEmpty(env.CAPLETS_SERVER_PASSWORD, "CAPLETS_SERVER_PASSWORD");
@@ -65607,13 +66311,22 @@ function resolveServeOptions(raw, env = process.env) {
65607
66311
  path,
65608
66312
  auth,
65609
66313
  warnUnauthenticatedNetwork: !loopback && !auth.enabled,
65610
- loopback
66314
+ loopback,
66315
+ trustProxy: raw.trustProxy === true
65611
66316
  };
65612
66317
  }
65613
66318
  function isLoopbackHost(host) {
65614
66319
  const normalized = host.toLocaleLowerCase();
65615
66320
  return normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1";
65616
66321
  }
66322
+ function parseServeServerUrl(value) {
66323
+ try {
66324
+ return parseServerBaseUrl(value);
66325
+ } catch (error) {
66326
+ 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.");
66327
+ throw error;
66328
+ }
66329
+ }
65617
66330
  function parseTransport(value) {
65618
66331
  if (value === "stdio" || value === "http") return value;
65619
66332
  throw new CapletsError("REQUEST_INVALID", `Expected --transport to be stdio or http, got ${value}`);
@@ -65628,6 +66341,9 @@ function normalizeHttpPath(value) {
65628
66341
  if (value.includes("?") || value.includes("#")) throw new CapletsError("REQUEST_INVALID", "HTTP --path must not include a query string or fragment");
65629
66342
  return value === "/" ? value : value.replace(/\/+$/u, "");
65630
66343
  }
66344
+ function serverUrlHost(url) {
66345
+ return url?.hostname.replace(/^\[(.*)\]$/u, "$1");
66346
+ }
65631
66347
  function nonEmpty(value, label) {
65632
66348
  if (value === void 0) return;
65633
66349
  const trimmed = value.trim();
@@ -65754,6 +66470,8 @@ async function runCli(args, io = {}) {
65754
66470
  function createProgram(io = {}) {
65755
66471
  const writeOut = io.writeOut ?? ((value) => process.stdout.write(value));
65756
66472
  const writeErr = io.writeErr ?? ((value) => process.stderr.write(value));
66473
+ const env = io.env ?? process.env;
66474
+ const currentConfigPath = () => envConfigPath(env);
65757
66475
  const setExitCode = io.setExitCode ?? ((code) => {
65758
66476
  process.exitCode = code;
65759
66477
  });
@@ -65763,42 +66481,78 @@ function createProgram(io = {}) {
65763
66481
  writeErr,
65764
66482
  outputError: (value, write) => write(value)
65765
66483
  });
65766
- 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) => {
66484
+ 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) => {
65767
66485
  const resolved = resolveServeOptions(options);
65768
- const configPath = envConfigPath();
66486
+ const configPath = currentConfigPath();
65769
66487
  await (io.serve ?? ((serveOptions) => serveResolvedCaplets(serveOptions, {
65770
66488
  ...configPath ? { configPath } : {},
65771
66489
  ...io.authDir ? { authDir: io.authDir } : {}
65772
66490
  }, writeErr)))(resolved);
65773
66491
  });
65774
- program.command("init").description("Create a starter Caplets config file.").option("--force", "overwrite an existing config file").action((options) => {
65775
- const configPath = envConfigPath();
66492
+ program.command("init").description("Create a starter Caplets config file.").option("--force", "overwrite an existing config file").action(async (options) => {
66493
+ const remote = remoteClientForCli(io);
66494
+ if (remote) {
66495
+ writeOut(`Created remote Caplets config at ${(await remote.request("init", { force: Boolean(options.force) })).path}\n`);
66496
+ return;
66497
+ }
66498
+ const configPath = currentConfigPath();
65776
66499
  writeOut(`Created Caplets config at ${initConfig({
65777
66500
  ...configPath ? { path: configPath } : {},
65778
66501
  force: Boolean(options.force)
65779
66502
  })}\n`);
65780
66503
  });
65781
- 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) => {
65782
- const rows = listCaplets(loadConfigWithSources(envConfigPath()), { includeDisabled: Boolean(options.all) });
66504
+ 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) => {
66505
+ const includeDisabled = Boolean(options.all);
66506
+ const remote = remoteClientForCli(io);
66507
+ if (remote) {
66508
+ const rows = await remote.request("list", { includeDisabled });
66509
+ if (options.json || options.format === "json") {
66510
+ writeOut(`${JSON.stringify(rows, null, 2)}\n`);
66511
+ return;
66512
+ }
66513
+ writeOut(formatCapletList(rows, options.format ?? "plain"));
66514
+ return;
66515
+ }
66516
+ const rows = listCaplets(loadConfigWithSources(currentConfigPath()), { includeDisabled });
65783
66517
  if (options.json || options.format === "json") {
65784
66518
  writeOut(`${JSON.stringify(rows, null, 2)}\n`);
65785
66519
  return;
65786
66520
  }
65787
66521
  writeOut(formatCapletList(rows, options.format ?? "plain"));
65788
66522
  });
65789
- 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) => {
66523
+ 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) => {
66524
+ const remote = remoteClientForCli(io);
66525
+ if (remote) {
66526
+ if (options.global) writeErr("Warning: --global is not supported in remote mode; the server controls the installation destination.\n");
66527
+ const result = await remote.request("install", {
66528
+ repo,
66529
+ capletIds,
66530
+ force: Boolean(options.force)
66531
+ });
66532
+ for (const caplet of result.installed) writeOut(`Installed ${caplet.id} to remote ${caplet.destination}\n`);
66533
+ return;
66534
+ }
65790
66535
  const result = installCaplets(repo, {
65791
66536
  capletIds,
65792
66537
  force: Boolean(options.force),
65793
- destinationRoot: options.global ? resolveCapletsRoot(resolveConfigPath(envConfigPath())) : resolveProjectCapletsRoot()
66538
+ destinationRoot: options.global ? resolveCapletsRoot(resolveConfigPath(currentConfigPath())) : resolveProjectCapletsRoot()
65794
66539
  });
65795
66540
  for (const caplet of result.installed) writeOut(`Installed ${caplet.id} to ${caplet.destination}\n`);
65796
66541
  });
65797
66542
  const add = program.command("add").description("Add generated Caplet files.");
65798
- 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) => {
66543
+ 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) => {
66544
+ const remote = remoteClientForCli(io);
66545
+ if (remote) {
66546
+ writeAddResult(writeOut, "CLI", await remote.request("add", {
66547
+ kind: "cli",
66548
+ id,
66549
+ options: remoteAddOptions(options)
66550
+ }));
66551
+ return;
66552
+ }
65799
66553
  const result = addCliCaplet(id, {
65800
66554
  ...options,
65801
- destinationRoot: options.global ? resolveCapletsRoot(resolveConfigPath(envConfigPath())) : resolveProjectCapletsRoot()
66555
+ destinationRoot: options.global ? resolveCapletsRoot(resolveConfigPath(currentConfigPath())) : resolveProjectCapletsRoot()
65802
66556
  });
65803
66557
  if (result.path) {
65804
66558
  writeOut(`Wrote CLI Caplet to ${result.path}\n`);
@@ -65806,28 +66560,64 @@ function createProgram(io = {}) {
65806
66560
  }
65807
66561
  writeOut(result.text);
65808
66562
  });
65809
- 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) => {
66563
+ 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) => {
66564
+ const remote = remoteClientForCli(io);
66565
+ if (remote) {
66566
+ writeAddResult(writeOut, "MCP", await remote.request("add", {
66567
+ kind: "mcp",
66568
+ id,
66569
+ options: remoteAddOptions(options)
66570
+ }));
66571
+ return;
66572
+ }
65810
66573
  writeAddResult(writeOut, "MCP", addMcpCaplet(id, {
65811
66574
  ...options,
65812
- destinationRoot: addDestinationRoot(options)
66575
+ destinationRoot: addDestinationRoot(options, currentConfigPath())
65813
66576
  }));
65814
66577
  });
65815
- 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) => {
66578
+ 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) => {
66579
+ const remote = remoteClientForCli(io);
66580
+ if (remote) {
66581
+ writeAddResult(writeOut, "OpenAPI", await remote.request("add", {
66582
+ kind: "openapi",
66583
+ id,
66584
+ options: remoteAddOptions(options)
66585
+ }));
66586
+ return;
66587
+ }
65816
66588
  writeAddResult(writeOut, "OpenAPI", addOpenApiCaplet(id, {
65817
66589
  ...options,
65818
- destinationRoot: addDestinationRoot(options)
66590
+ destinationRoot: addDestinationRoot(options, currentConfigPath())
65819
66591
  }));
65820
66592
  });
65821
- 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) => {
66593
+ 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) => {
66594
+ const remote = remoteClientForCli(io);
66595
+ if (remote) {
66596
+ writeAddResult(writeOut, "GraphQL", await remote.request("add", {
66597
+ kind: "graphql",
66598
+ id,
66599
+ options: remoteAddOptions(options)
66600
+ }));
66601
+ return;
66602
+ }
65822
66603
  writeAddResult(writeOut, "GraphQL", addGraphqlCaplet(id, {
65823
66604
  ...options,
65824
- destinationRoot: addDestinationRoot(options)
66605
+ destinationRoot: addDestinationRoot(options, currentConfigPath())
65825
66606
  }));
65826
66607
  });
65827
- 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) => {
66608
+ 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) => {
66609
+ const remote = remoteClientForCli(io);
66610
+ if (remote) {
66611
+ writeAddResult(writeOut, "HTTP", await remote.request("add", {
66612
+ kind: "http",
66613
+ id,
66614
+ options: remoteAddOptions(options)
66615
+ }));
66616
+ return;
66617
+ }
65828
66618
  writeAddResult(writeOut, "HTTP", addHttpCaplet(id, {
65829
66619
  ...options,
65830
- destinationRoot: addDestinationRoot(options)
66620
+ destinationRoot: addDestinationRoot(options, currentConfigPath())
65831
66621
  }));
65832
66622
  });
65833
66623
  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) => {
@@ -65836,6 +66626,8 @@ function createProgram(io = {}) {
65836
66626
  writeErr,
65837
66627
  setExitCode,
65838
66628
  authDir: io.authDir,
66629
+ env,
66630
+ remote: remoteClientForCli(io),
65839
66631
  format: options.format
65840
66632
  });
65841
66633
  });
@@ -65845,6 +66637,8 @@ function createProgram(io = {}) {
65845
66637
  writeErr,
65846
66638
  setExitCode,
65847
66639
  authDir: io.authDir,
66640
+ env,
66641
+ remote: remoteClientForCli(io),
65848
66642
  format: options.format
65849
66643
  });
65850
66644
  });
@@ -65854,6 +66648,8 @@ function createProgram(io = {}) {
65854
66648
  writeErr,
65855
66649
  setExitCode,
65856
66650
  authDir: io.authDir,
66651
+ env,
66652
+ remote: remoteClientForCli(io),
65857
66653
  format: options.format
65858
66654
  });
65859
66655
  });
@@ -65870,6 +66666,8 @@ function createProgram(io = {}) {
65870
66666
  writeErr,
65871
66667
  setExitCode,
65872
66668
  authDir: io.authDir,
66669
+ env,
66670
+ remote: remoteClientForCli(io),
65873
66671
  format: options.format
65874
66672
  });
65875
66673
  });
@@ -65883,6 +66681,8 @@ function createProgram(io = {}) {
65883
66681
  writeErr,
65884
66682
  setExitCode,
65885
66683
  authDir: io.authDir,
66684
+ env,
66685
+ remote: remoteClientForCli(io),
65886
66686
  format: options.format
65887
66687
  });
65888
66688
  });
@@ -65898,15 +66698,17 @@ function createProgram(io = {}) {
65898
66698
  writeErr,
65899
66699
  setExitCode,
65900
66700
  authDir: io.authDir,
66701
+ env,
66702
+ remote: remoteClientForCli(io),
65901
66703
  format: options.format
65902
66704
  });
65903
66705
  });
65904
66706
  const config = program.command("config").description("Inspect Caplets config locations.");
65905
66707
  config.command("path").description("Print the effective user config path.").action(() => {
65906
- writeOut(`${resolveConfigPath(envConfigPath())}\n`);
66708
+ writeOut(`${resolveConfigPath(currentConfigPath())}\n`);
65907
66709
  });
65908
66710
  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) => {
65909
- const paths = resolveCliConfigPaths(envConfigPath(), io.authDir);
66711
+ const paths = resolveCliConfigPaths(currentConfigPath(), io.authDir);
65910
66712
  if (options.json || options.format === "json") {
65911
66713
  writeOut(`${JSON.stringify(paths, null, 2)}\n`);
65912
66714
  return;
@@ -65915,7 +66717,19 @@ function createProgram(io = {}) {
65915
66717
  });
65916
66718
  const auth = program.command("auth").description("Manage OAuth credentials for remote servers.");
65917
66719
  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) => {
65918
- const configPath = envConfigPath();
66720
+ const remote = remoteClientForCli(io);
66721
+ if (remote) {
66722
+ const started = await remote.request("auth_login_start", { server: serverId });
66723
+ if (started.authorizationUrl) {
66724
+ writeOut(`Open this URL to authorize ${serverId}:\n${started.authorizationUrl}\n`);
66725
+ if (options.open !== false) await openBrowser(started.authorizationUrl);
66726
+ writeOut("Complete authentication in your browser. The server callback will store credentials.\n");
66727
+ return;
66728
+ }
66729
+ if (started.authenticated) writeOut(`Authenticated \`${serverId}\`.\n`);
66730
+ return;
66731
+ }
66732
+ const configPath = currentConfigPath();
65919
66733
  await loginAuth(serverId, {
65920
66734
  noOpen: options.open === false,
65921
66735
  writeOut,
@@ -65924,27 +66738,78 @@ function createProgram(io = {}) {
65924
66738
  ...io.authDir ? { authDir: io.authDir } : {}
65925
66739
  });
65926
66740
  });
65927
- auth.command("logout").description("Delete stored OAuth credentials for a server.").argument("<server>", "configured server ID").action((serverId) => {
65928
- const configPath = envConfigPath();
66741
+ auth.command("logout").description("Delete stored OAuth credentials for a server.").argument("<server>", "configured server ID").action(async (serverId) => {
66742
+ const remote = remoteClientForCli(io);
66743
+ if (remote) {
66744
+ writeOut((await remote.request("auth_logout", { server: serverId })).deleted ? `Deleted remote OAuth credentials for \`${serverId}\`.\n` : `No remote OAuth credentials found for \`${serverId}\`.\n`);
66745
+ return;
66746
+ }
66747
+ const configPath = currentConfigPath();
65929
66748
  logoutAuth(serverId, {
65930
66749
  writeOut,
65931
66750
  ...configPath ? { configPath } : {},
65932
66751
  ...io.authDir ? { authDir: io.authDir } : {}
65933
66752
  });
65934
66753
  });
65935
- 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) => {
65936
- const configPath = envConfigPath();
66754
+ 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) => {
66755
+ const configPath = currentConfigPath();
66756
+ const format = options.json || options.format === "json" ? "json" : options.format ?? "plain";
66757
+ const remote = remoteClientForCli(io);
66758
+ if (remote) {
66759
+ const rows = await remote.request("auth_list", {});
66760
+ if (format === "json") {
66761
+ writeOut(`${JSON.stringify(rows, null, 2)}\n`);
66762
+ return;
66763
+ }
66764
+ writeOut(formatAuthRows(rows, format));
66765
+ return;
66766
+ }
65937
66767
  listAuth({
65938
66768
  writeOut,
65939
- format: options.json || options.format === "json" ? "json" : options.format ?? "plain",
66769
+ format,
65940
66770
  ...configPath ? { configPath } : {},
65941
66771
  ...io.authDir ? { authDir: io.authDir } : {}
65942
66772
  });
65943
66773
  });
65944
66774
  return program;
65945
66775
  }
65946
- function envConfigPath() {
65947
- return process.env.CAPLETS_CONFIG?.trim() || void 0;
66776
+ function envConfigPath(env) {
66777
+ return env.CAPLETS_CONFIG?.trim() || void 0;
66778
+ }
66779
+ function remoteClientForCli(io) {
66780
+ const env = io.env ?? process.env;
66781
+ if (resolveCapletsMode({}, env).mode !== "remote") return;
66782
+ return new RemoteControlClient(resolveCapletsServer(io.fetch ? { fetch: io.fetch } : {}, env));
66783
+ }
66784
+ async function openBrowser(url) {
66785
+ const { spawn } = await import("node:child_process");
66786
+ spawn(process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open", process.platform === "win32" ? [
66787
+ "/c",
66788
+ "start",
66789
+ "",
66790
+ url
66791
+ ] : [url], {
66792
+ stdio: "ignore",
66793
+ detached: true
66794
+ }).unref();
66795
+ }
66796
+ function remoteCommandForOperation(operation) {
66797
+ switch (operation) {
66798
+ case "get_caplet":
66799
+ case "check_backend":
66800
+ case "list_tools":
66801
+ case "search_tools":
66802
+ case "get_tool":
66803
+ case "call_tool": return operation;
66804
+ default: return;
66805
+ }
66806
+ }
66807
+ function remoteAddOptions(options) {
66808
+ const { output, print, global, destinationRoot, ...remoteOptions } = options;
66809
+ if (global) throw new CapletsError("REQUEST_INVALID", "--global is not supported in remote mode; the server controls the add destination.");
66810
+ if (print) throw new CapletsError("REQUEST_INVALID", "--print is not supported in remote mode; the server controls add output.");
66811
+ if (output !== void 0) throw new CapletsError("REQUEST_INVALID", "--output is not supported in remote mode; the server controls the add destination.");
66812
+ return remoteOptions;
65948
66813
  }
65949
66814
  function collect(value, previous) {
65950
66815
  previous.push(value);
@@ -65987,7 +66852,21 @@ function isPlainObject(value) {
65987
66852
  return value !== null && typeof value === "object" && !Array.isArray(value);
65988
66853
  }
65989
66854
  async function executeOperation(caplet, request, io) {
65990
- const configPath = envConfigPath();
66855
+ const command = remoteCommandForOperation(request.operation);
66856
+ if (io.remote && command) {
66857
+ const result = await io.remote.request(command, {
66858
+ caplet,
66859
+ request
66860
+ });
66861
+ const output = cliOutputForOperation(result, {
66862
+ ...request,
66863
+ caplet
66864
+ }, io.format ?? "markdown");
66865
+ io.writeOut(typeof output === "string" ? `${output}\n` : `${JSON.stringify(output, null, 2)}\n`);
66866
+ if (isPlainObject(result) && result.isError === true) io.setExitCode(1);
66867
+ return;
66868
+ }
66869
+ const configPath = envConfigPath(io.env ?? process.env);
65991
66870
  const engine = new CapletsEngine({
65992
66871
  ...configPath ? { configPath } : {},
65993
66872
  ...io.authDir ? { authDir: io.authDir } : {},
@@ -66233,19 +67112,19 @@ function schemaSummary(schema) {
66233
67112
  required.length > 0 ? `required ${required.join(", ")}` : "no required fields"
66234
67113
  ].filter((part) => Boolean(part)).join("; ");
66235
67114
  }
66236
- function addDestinationRoot(options) {
66237
- return options.global ? resolveCapletsRoot(resolveConfigPath(envConfigPath())) : resolveProjectCapletsRoot();
67115
+ function addDestinationRoot(options, configPath) {
67116
+ return options.global ? resolveCapletsRoot(resolveConfigPath(configPath)) : resolveProjectCapletsRoot();
66238
67117
  }
66239
67118
  function writeAddResult(writeOut, label, result) {
66240
67119
  if (result.path) {
66241
- writeOut(`Wrote ${label} Caplet to ${result.path}\n`);
67120
+ writeOut(`Wrote ${result.remote ? "remote " : ""}${label} Caplet to ${result.path}\n`);
66242
67121
  return;
66243
67122
  }
66244
67123
  writeOut(result.text);
66245
67124
  }
66246
67125
  //#endregion
66247
67126
  //#region package.json
66248
- var version = "0.15.0";
67127
+ var version = "0.16.0";
66249
67128
  //#endregion
66250
67129
  //#region src/index.ts
66251
67130
  async function main() {