@caplets/core 0.26.1 → 0.28.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 (43) hide show
  1. package/dist/attach/api.d.ts +10 -1
  2. package/dist/caplet-files-bundle.d.ts +1 -0
  3. package/dist/caplet-source.js +39 -3
  4. package/dist/cli/code-mode.d.ts +3 -2
  5. package/dist/cli/commands.d.ts +3 -1
  6. package/dist/cli/inspection.d.ts +2 -1
  7. package/dist/cli.d.ts +4 -0
  8. package/dist/code-mode/types.d.ts +2 -1
  9. package/dist/{completion-CFOJucl5.js → completion-CjE0EnbF.js} +16 -4
  10. package/dist/config/paths.d.ts +9 -0
  11. package/dist/config/validation.d.ts +2 -0
  12. package/dist/config-runtime.d.ts +6 -1
  13. package/dist/config-runtime.js +34 -3
  14. package/dist/config.d.ts +13 -2
  15. package/dist/engine.d.ts +18 -0
  16. package/dist/errors.d.ts +1 -1
  17. package/dist/esm-Db9dhnIG.js +7488 -0
  18. package/dist/exposure/namespace.d.ts +39 -0
  19. package/dist/index.js +840 -483
  20. package/dist/native/options.d.ts +1 -0
  21. package/dist/native/remote.d.ts +4 -2
  22. package/dist/native/service.d.ts +16 -2
  23. package/dist/native.js +1 -1
  24. package/dist/node-BgWIvSVP.js +17214 -0
  25. package/dist/rolldown-runtime-CE-6LUnI.js +44 -0
  26. package/dist/serve/http.d.ts +16 -1
  27. package/dist/serve/options.d.ts +2 -0
  28. package/dist/{service-aBIn4nrw.js → service-BfPCRxQ9.js} +2043 -154
  29. package/dist/src-Cd2QIUm1.js +813 -0
  30. package/dist/telemetry/context.d.ts +13 -0
  31. package/dist/telemetry/debug.d.ts +10 -0
  32. package/dist/telemetry/delivery.d.ts +1 -0
  33. package/dist/telemetry/events.d.ts +70 -0
  34. package/dist/telemetry/identity.d.ts +1 -0
  35. package/dist/telemetry/index.d.ts +8 -0
  36. package/dist/telemetry/intake.generated.d.ts +2 -0
  37. package/dist/telemetry/notice.d.ts +8 -0
  38. package/dist/telemetry/privacy.d.ts +3 -0
  39. package/dist/telemetry/providers.d.ts +21 -0
  40. package/dist/telemetry/runtime.d.ts +40 -0
  41. package/dist/telemetry/state.d.ts +53 -0
  42. package/dist/{validation-GD2x5HW1.js → validation-CWzd2gtn.js} +3 -1
  43. package/package.json +3 -1
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
- import { $ as resolveExposure, $t as mergeCapabilities, A as nativeCapletPromptGuidance, An as isJSONRPCRequest, At as runOAuthFlow, B as CodeModeSessionManager, Bn as safeParse, Bt as defaultConfigBaseDir, C as resolveHostedCloudRemote, Cn as ReadResourceRequestSchema, Ct as validateCapletFile, D as isLoopbackHost, Dn as assertCompleteRequestResourceTemplate, Dt as markdownStructuredContent, E as controlUrlForBase, En as assertCompleteRequestPrompt, Et as markdownCallToolResultContent, Fn as getSchemaDescription, Ft as readTokenBundle, G as CodeModeJournalStore, Gt as resolveProjectCapletsRoot, H as diagnoseCodeModeTypeScript, Ht as defaultStateBaseDir, I as codeModeRunInputSchema, In as isSchemaOptional, It as DEFAULT_AUTH_DIR, J as codeModeDeclarationHash, Jt as serializeMessage, K as CodeModeLogStore, Kt as resolveProjectConfigPath, L as codeModeRunParamsSchema, Ln as isZ4Schema, M as nativeCapletToolName, Mn as getLiteralValue, Mt as startOAuthFlow, Nn as getObjectShape, Nt as deleteTokenBundle, O as parseServerBaseUrl, On as isInitializeRequest, Ot as refreshOAuthTokenBundle, Pn as getParseErrorMessage, Pt as isTokenBundleExpired, Q as CapletsEngine, Qt as Protocol, R as emptyCodeModeRunMeta, Rn as normalizeObjectSchema, Rt as DEFAULT_OBSERVED_OUTPUT_SHAPE_CACHE_DIR, S as resolveCapletsRemote, Sn as McpError, St as discoverCapletFiles, T as appendBasePath, Tn as SetLevelRequestSchema, Tt as hasRenderableStructuredContent, U as createCodeModeCapletsApi, Ut as resolveCapletsRoot, V as QuickJsCodeModeSandbox, Vn as safeParseAsync, Vt as defaultConfigPath, W as listCodeModeCallableCaplets, Wt as resolveConfigPath, X as generateCodeModeRunToolDescription, Xt as assertToolsCallTaskCapability, Y as generateCodeModeDeclarations, Yt as assertClientRequestTaskCapability, Z as minifyCodeModeDeclarationText, Zt as AjvJsonSchemaValidator, _ as CapletsCloudClient, _n as ListResourceTemplatesRequestSchema, _t as FileVaultStore, a as CloudAuthStore, an as CreateMessageResultWithToolsSchema, at as ServerRegistry, b as isCapletsCloudUrl, bn as ListToolsRequestSchema, bt as decryptVaultValue, c as redactedCloudAuthStatus, cn as ElicitResultSchema, ct as loadConfig, d as projectBindingError, dn as GetPromptRequestSchema, dt as loadLocalOverlayConfigWithSources, en as toJsonSchemaCompat, et as decodeDirectResourceUri, f as projectBindingRecovery, fn as InitializeRequestSchema, ft as loadProjectConfig, g as buildProjectSyncManifest, gn as ListPromptsRequestSchema, gt as vaultStoreForAuthDir, hn as LATEST_PROTOCOL_VERSION, ht as vaultResolverForAuthDir, i as createRemoteProfileStore, in as CreateMessageResultSchema, it as handleServerTool, j as nativeCapletToolDescription, jn as isJSONRPCResultResponse, jt as startGenericOAuthFlow, k as resolveCapletsServer, kn as isJSONRPCErrorResponse, kt as runGenericOAuthFlow, l as PROJECT_BINDING_ERROR_CODES, ln as EmptyResultSchema, lt as loadConfigWithSources, mn as JSONRPCMessageSchema, mt as vaultBootstrapResolver, n as resolveRemoteSelection, nn as CallToolResultSchema, nt as findProjectRoot, o as cloudAuthPath, on as CreateTaskResultSchema, ot as capabilityDescription, p as CloudAuthClient, pn as InitializedNotificationSchema, pt as parseConfig, q as redactCodeModeLogText, qt as ReadBuffer, r as cloudCredentialsFromRemoteProfile, rn as CompleteRequestSchema, rt as fingerprintProjectRoot, s as migrateCredentials, st as GoogleDiscoveryManager, t as createNativeCapletsService, tn as CallToolRequestSchema, tt as directResourceUriMatchesTemplate, u as ProjectBindingError, un as ErrorCode, ut as loadGlobalConfig, vn as ListResourcesRequestSchema, vt as VAULT_MAX_VALUE_BYTES, w as resolveRemoteMode, wn as SUPPORTED_PROTOCOL_VERSIONS, wt as loadCapletFilesFromMap, x as normalizeRemoteProfileHostUrl, xn as LoggingLevelSchema, xt as encryptVaultValue, y as hostedCloudWorkspaceFromRemoteUrl, yn as ListRootsResultSchema, yt as validateVaultKeyName, z as runCodeMode, zn as objectFromShape, zt as defaultCacheBaseDir } from "./service-aBIn4nrw.js";
1
+ import { $ as redactCodeModeLogText, $n as getParseErrorMessage, $t as readTokenBundle, A as resolveRemoteMode, An as GetPromptRequestSchema, At as loadProjectConfig, Bn as LoggingLevelSchema, Bt as discoverCapletFiles, C as CapletsCloudClient, Cn as CreateMessageResultSchema, Ct as ServerRegistry, D as normalizeRemoteProfileHostUrl, Dn as ElicitResultSchema, Dt as loadConfigWithSources, E as isCapletsCloudUrl, Et as loadConfig, F as resolveCapletsServer, Fn as ListPromptsRequestSchema, Ft as FileVaultStore, G as runCodeMode, Gn as assertCompleteRequestPrompt, Gt as markdownStructuredContent, H as codeModeRunInputSchema, Hn as ReadResourceRequestSchema, Ht as loadCapletFilesFromMap, I as nativeCapletPromptGuidance, In as ListResourceTemplatesRequestSchema, It as VAULT_MAX_VALUE_BYTES, J as diagnoseCodeModeTypeScript, Jn as isJSONRPCErrorResponse, Jt as runOAuthFlow, K as CodeModeSessionManager, Kn as assertCompleteRequestResourceTemplate, Kt as refreshOAuthTokenBundle, L as nativeCapletToolDescription, Ln as ListResourcesRequestSchema, Lt as validateVaultKeyName, M as controlUrlForBase, Mn as InitializedNotificationSchema, Mt as vaultBootstrapResolver, N as isLoopbackHost, Nn as JSONRPCMessageSchema, Nt as vaultResolverForAuthDir, O as resolveCapletsRemote, On as EmptyResultSchema, Ot as loadGlobalConfig, P as parseServerBaseUrl, Pn as LATEST_PROTOCOL_VERSION, Pt as vaultStoreForAuthDir, Q as CodeModeLogStore, Qn as getObjectShape, Qt as isTokenBundleExpired, R as nativeCapletToolName, Rn as ListRootsResultSchema, Rt as decryptVaultValue, S as buildProjectSyncManifest, Sn as CompleteRequestSchema, St as handleServerTool, T as hostedCloudWorkspaceFromRemoteUrl, Tn as CreateTaskResultSchema, Tt as GoogleDiscoveryManager, U as codeModeRunParamsSchema, Un as SUPPORTED_PROTOCOL_VERSIONS, Ut as hasRenderableStructuredContent, Vn as McpError, Vt as validateCapletFile, W as emptyCodeModeRunMeta, Wn as SetLevelRequestSchema, Wt as markdownCallToolResultContent, X as listCodeModeCallableCaplets, Xn as isJSONRPCResultResponse, Xt as startOAuthFlow, Y as createCodeModeCapletsApi, Yn as isJSONRPCRequest, Yt as startGenericOAuthFlow, Z as CodeModeJournalStore, Zn as getLiteralValue, Zt as deleteTokenBundle, _ as attachErrorResponse, _n as Protocol, _t as rotateTelemetryIdentity, a as CloudAuthStore, an as defaultConfigPath, ar as safeParse, at as version, b as invokeAttachExport, bn as CallToolRequestSchema, bt as findProjectRoot, c as redactedCloudAuthStatus, cn as resolveCapletsRoot, ct as buildProductTelemetryEvent, d as projectBindingError, dn as resolveProjectConfigPath, dt as maybePrintTelemetryNotice, en as DEFAULT_AUTH_DIR, er as getSchemaDescription, et as codeModeDeclarationHash, f as projectBindingRecovery, fn as ReadBuffer, ft as resolveTelemetryState, g as CAPLETS_ATTACH_SESSION_HEADER, gn as AjvJsonSchemaValidator, gt as readTelemetryNotice, hn as assertToolsCallTaskCapability, ht as readTelemetryIdentity, i as createRemoteProfileStore, in as defaultConfigBaseDir, ir as objectFromShape, it as CapletsEngine, j as appendBasePath, jn as InitializeRequestSchema, jt as parseConfig, k as resolveHostedCloudRemote, kn as ErrorCode, kt as loadLocalOverlayConfigWithSources, l as PROJECT_BINDING_ERROR_CODES, ln as resolveConfigPath, lt as buildReliabilityTelemetryEvent, mn as assertClientRequestTaskCapability, mt as readTelemetryDeliveryHealth, n as resolveRemoteSelection, nn as DEFAULT_OBSERVED_OUTPUT_SHAPE_CACHE_DIR, nr as isZ4Schema, nt as generateCodeModeRunToolDescription, o as cloudAuthPath, on as defaultStateBaseDir, or as safeParseAsync, ot as createTelemetryDispatcher, p as CloudAuthClient, pn as serializeMessage, pt as deleteTelemetryIdentity, q as QuickJsCodeModeSandbox, qn as isInitializeRequest, qt as runGenericOAuthFlow, r as cloudCredentialsFromRemoteProfile, rn as defaultCacheBaseDir, rr as normalizeObjectSchema, rt as minifyCodeModeDeclarationText, s as migrateCredentials, sn as defaultTelemetryStateDir, st as TelemetryDebugSink, t as createNativeCapletsService, tr as isSchemaOptional, tt as generateCodeModeDeclarations, u as ProjectBindingError, un as resolveProjectCapletsRoot, ut as durationBucket, v as buildAttachProjection, vn as mergeCapabilities, vt as resolveExposure, wn as CreateMessageResultWithToolsSchema, wt as capabilityDescription, x as invokeNativeAttachExport, xn as CallToolResultSchema, xt as fingerprintProjectRoot, y as buildNativeAttachProjection, yn as toJsonSchemaCompat, yt as decodeDirectResourceUri, zn as ListToolsRequestSchema, zt as encryptVaultValue } from "./service-BfPCRxQ9.js";
2
2
  import { _ as record, b as unknown, d as literal, m as object, n as ZodOptional, o as array, p as number, r as _enum, s as boolean, v as string, x as url } from "./schemas-BoqMu4MG.js";
3
- import { f as redactSecrets$1, i as SERVER_ID_PATTERN, l as CAPLETS_ERROR_CODES, p as toSafeError, u as CapletsError } from "./validation-GD2x5HW1.js";
4
- import { generatedToolInputJsonSchemaForCaplet, generatedToolInputSchema, generatedToolInputSchemaForCaplet } from "./generated-tool-input-schema.js";
5
- import { f as observedOutputShapeKey, g as stableJsonStringify, h as schemaHash, i as observeOutputShape, u as FileObservedOutputShapeStore } from "./observed-output-shapes-DuP7mJQf.js";
6
- import { a as formatCapletList, c as resolveCliConfigPaths, l as cliCommands$1, n as completionScript, o as formatConfigPaths, s as listCaplets, t as completeCliWords, u as completionShells } from "./completion-CFOJucl5.js";
3
+ import { a as SERVER_ID_PATTERN, d as CapletsError, m as toSafeError, p as redactSecrets$1, u as CAPLETS_ERROR_CODES } from "./validation-CWzd2gtn.js";
4
+ import { generatedToolInputSchema, generatedToolInputSchemaForCaplet } from "./generated-tool-input-schema.js";
5
+ import { f as observedOutputShapeKey, i as observeOutputShape, u as FileObservedOutputShapeStore } from "./observed-output-shapes-DuP7mJQf.js";
6
+ import { a as formatCapletList, c as resolveCliConfigPaths, l as cliCommands$1, n as completionScript, o as formatConfigPaths, s as listCaplets, t as completeCliWords, u as completionShells } from "./completion-CjE0EnbF.js";
7
7
  import { n as normalizeCapletSourcePath, t as FilesystemCapletSource } from "./filesystem-Kkg32TOJ.js";
8
8
  import { parseConfig as parseConfig$1 } from "./config-runtime.js";
9
9
  import fs, { accessSync, chmodSync, closeSync, constants, copyFileSync, cpSync, existsSync, fstatSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, watch, writeFileSync, writeSync } from "node:fs";
@@ -13,7 +13,7 @@ import process$1, { stdin, stdout } from "node:process";
13
13
  import { Readable, Writable } from "node:stream";
14
14
  import { STATUS_CODES, createServer } from "node:http";
15
15
  import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
16
- import { homedir, tmpdir, userInfo } from "node:os";
16
+ import { arch, homedir, platform, tmpdir, userInfo } from "node:os";
17
17
  import { Buffer as Buffer$1 } from "node:buffer";
18
18
  import { EventEmitter } from "node:events";
19
19
  import { promisify, stripVTControlCharacters } from "node:util";
@@ -1552,9 +1552,6 @@ const EMPTY_COMPLETION_RESULT = { completion: {
1552
1552
  hasMore: false
1553
1553
  } };
1554
1554
  //#endregion
1555
- //#region package.json
1556
- var version = "0.26.1";
1557
- //#endregion
1558
1555
  //#region src/serve/session.ts
1559
1556
  var CapletsMcpSession = class {
1560
1557
  engine;
@@ -1634,7 +1631,7 @@ var CapletsMcpSession = class {
1634
1631
  downstreamName: entry.downstreamName,
1635
1632
  exposure: "direct"
1636
1633
  } },
1637
- callback: async (request) => this.engine.executeDirectTool(entry.caplet.server, entry.downstreamName, isRecord$4(request) ? request : {}),
1634
+ callback: async (request) => this.engine.executeDirectTool(entry.caplet.server, entry.downstreamName, isRecord$3(request) ? request : {}),
1638
1635
  enabled: true
1639
1636
  })
1640
1637
  });
@@ -1672,6 +1669,7 @@ var CapletsMcpSession = class {
1672
1669
  }, async (request) => this.handleCodeModeRunTool(request));
1673
1670
  }
1674
1671
  async handleCodeModeRunTool(request) {
1672
+ const started = Date.now();
1675
1673
  const parsed = codeModeRunInputSchema.safeParse(request);
1676
1674
  const envelope = parsed.success ? await runCodeMode({
1677
1675
  code: parsed.data.code,
@@ -1697,6 +1695,10 @@ var CapletsMcpSession = class {
1697
1695
  },
1698
1696
  meta: emptyCodeModeRunMeta()
1699
1697
  };
1698
+ this.engine.captureCodeModeOutcome(envelope, {
1699
+ started,
1700
+ ...parsed.success && parsed.data.timeoutMs !== void 0 ? { timeoutMs: parsed.data.timeoutMs } : {}
1701
+ }).catch(() => void 0);
1700
1702
  return {
1701
1703
  content: [{
1702
1704
  type: "text",
@@ -1725,7 +1727,7 @@ var CapletsMcpSession = class {
1725
1727
  downstreamName: entry.downstreamName,
1726
1728
  exposure: "direct"
1727
1729
  } }
1728
- }, async (request) => this.engine.executeDirectTool(entry.caplet.server, entry.downstreamName, isRecord$4(request) ? request : {}));
1730
+ }, async (request) => this.engine.executeDirectTool(entry.caplet.server, entry.downstreamName, isRecord$3(request) ? request : {}));
1729
1731
  }
1730
1732
  registerDirectResource(entry) {
1731
1733
  if (!this.server.registerResource) throw new Error("MCP server does not support resource registration");
@@ -1744,11 +1746,11 @@ var CapletsMcpSession = class {
1744
1746
  title: entry.prompt.name,
1745
1747
  ...entry.prompt.description ? { description: entry.prompt.description } : {},
1746
1748
  argsSchema: promptArgsSchema(entry.prompt.arguments)
1747
- }, async (args) => await this.engine.getDirectPrompt(entry.caplet.server, entry.downstreamName, isRecord$4(args) ? stringifyRecord$1(args) : {}));
1749
+ }, async (args) => await this.engine.getDirectPrompt(entry.caplet.server, entry.downstreamName, isRecord$3(args) ? stringifyRecord(args) : {}));
1748
1750
  }
1749
1751
  async directResourceResult(serverId, downstreamUri) {
1750
1752
  const result = await this.engine.readDirectResource(serverId, downstreamUri);
1751
- if (isRecord$4(result) && "contents" in result) return result;
1753
+ if (isRecord$3(result) && "contents" in result) return result;
1752
1754
  return { contents: [{
1753
1755
  uri: downstreamUri,
1754
1756
  mimeType: "application/json",
@@ -1819,10 +1821,10 @@ function promptArgsSchema(args) {
1819
1821
  for (const arg of args ?? []) shape[arg.name] = string().optional();
1820
1822
  return shape;
1821
1823
  }
1822
- function stringifyRecord$1(value) {
1824
+ function stringifyRecord(value) {
1823
1825
  return Object.fromEntries(Object.entries(value).map(([key, nested]) => [key, nested === void 0 ? "" : String(nested)]));
1824
1826
  }
1825
- function isRecord$4(value) {
1827
+ function isRecord$3(value) {
1826
1828
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1827
1829
  }
1828
1830
  function staticExposureSnapshot(config, caplets) {
@@ -5524,7 +5526,12 @@ async function runCodeModeCli(options) {
5524
5526
  mode: "local",
5525
5527
  ...options.configPath ? { configPath: options.configPath } : {},
5526
5528
  ...options.projectConfigPath ? { projectConfigPath: options.projectConfigPath } : {},
5527
- ...options.authDir ? { authDir: options.authDir } : {}
5529
+ ...options.authDir ? { authDir: options.authDir } : {},
5530
+ telemetryEnv: options.env,
5531
+ telemetryStateDir: options.telemetryStateDir ?? defaultTelemetryStateDir(options.env),
5532
+ telemetrySurface: "code_mode",
5533
+ telemetryVisibility: "visible",
5534
+ telemetryRuntimeMode: runtimeScope(options.env) === "local" ? "local" : "unknown"
5528
5535
  });
5529
5536
  try {
5530
5537
  if (options.sessionId !== void 0) {
@@ -5547,12 +5554,18 @@ async function runCodeModeCli(options) {
5547
5554
  options.setExitCode(1);
5548
5555
  return;
5549
5556
  }
5557
+ const code = await readCodeModeCliCode(options);
5558
+ const started = Date.now();
5550
5559
  const result = await runCodeMode({
5551
- code: await readCodeModeCliCode(options),
5552
- service: service.codeModeService?.() ?? service,
5560
+ code,
5553
5561
  ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs },
5562
+ service: service.codeModeService?.() ?? service,
5554
5563
  runtimeScope: "cli-one-shot"
5555
5564
  });
5565
+ await service.captureCodeModeOutcome?.(result, {
5566
+ started,
5567
+ ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
5568
+ }).catch(() => void 0);
5556
5569
  if (options.json) options.writeOut(`${JSON.stringify(result, null, 2)}\n`);
5557
5570
  else if (result.ok) options.writeOut(`${formatHumanValue(result.value)}\n`);
5558
5571
  else {
@@ -5587,7 +5600,8 @@ async function codeModeTypesCli(options) {
5587
5600
  const engine = new CapletsEngine({
5588
5601
  ...options.configPath ? { configPath: options.configPath } : {},
5589
5602
  ...options.projectConfigPath ? { projectConfigPath: options.projectConfigPath } : {},
5590
- ...options.authDir ? { authDir: options.authDir } : {}
5603
+ ...options.authDir ? { authDir: options.authDir } : {},
5604
+ telemetryStateDir: options.telemetryStateDir ?? defaultTelemetryStateDir(options.env)
5591
5605
  });
5592
5606
  try {
5593
5607
  const caplets = listCodeModeCallableCaplets$1(engine);
@@ -6919,14 +6933,24 @@ const HTTP_ONLY_OPTIONS = [
6919
6933
  "port",
6920
6934
  "path",
6921
6935
  "remoteStatePath",
6936
+ "upstreamUrl",
6922
6937
  "allowUnauthenticatedHttp",
6923
6938
  "trustProxy"
6924
6939
  ];
6940
+ const HTTP_ONLY_OPTION_FLAGS = {
6941
+ host: "--host",
6942
+ port: "--port",
6943
+ path: "--path",
6944
+ remoteStatePath: "--remote-state-path",
6945
+ upstreamUrl: "--upstream-url",
6946
+ allowUnauthenticatedHttp: "--allow-unauthenticated-http",
6947
+ trustProxy: "--trust-proxy"
6948
+ };
6925
6949
  function resolveServeOptions(raw, env = process.env) {
6926
6950
  const transport = parseTransport(raw.transport ?? "stdio");
6927
6951
  if (transport === "stdio") {
6928
6952
  const invalid = HTTP_ONLY_OPTIONS.filter((key) => raw[key] !== void 0);
6929
- if (invalid.length > 0) throw new CapletsError("REQUEST_INVALID", `${invalid.map((key) => `--${key}`).join(", ")} ${invalid.length === 1 ? "is" : "are"} only valid with --transport http`);
6953
+ if (invalid.length > 0) throw new CapletsError("REQUEST_INVALID", `${invalid.map((key) => HTTP_ONLY_OPTION_FLAGS[key]).join(", ")} ${invalid.length === 1 ? "is" : "are"} only valid with --transport http`);
6930
6954
  return { transport };
6931
6955
  }
6932
6956
  const serverUrl = env.CAPLETS_SERVER_URL ? parseServeServerUrl(nonEmpty$2(env.CAPLETS_SERVER_URL, "CAPLETS_SERVER_URL")) : void 0;
@@ -6934,6 +6958,13 @@ function resolveServeOptions(raw, env = process.env) {
6934
6958
  const port = parsePort(raw.port ?? (serverUrl?.port ? Number(serverUrl.port) : 5387));
6935
6959
  const path = normalizeHttpPath(raw.path ?? serverUrl?.pathname ?? "/");
6936
6960
  const remoteCredentialStateDir = nonEmpty$2(raw.remoteStatePath, "--remote-state-path") ?? nonEmpty$2(env.CAPLETS_REMOTE_SERVER_STATE_DIR, "CAPLETS_REMOTE_SERVER_STATE_DIR") ?? join(DEFAULT_AUTH_DIR, "remote-server");
6961
+ const upstreamUrl = nonEmpty$2(raw.upstreamUrl, "--upstream-url");
6962
+ if (upstreamUrl) rejectSelfReferentialUpstream(upstreamUrl, {
6963
+ ...serverUrl ? { origin: serverUrl.origin } : {},
6964
+ host,
6965
+ port,
6966
+ path
6967
+ });
6937
6968
  const loopback = isLoopbackHost(host);
6938
6969
  const auth = raw.allowUnauthenticatedHttp === true ? { type: "development_unauthenticated" } : { type: "remote_credentials" };
6939
6970
  return {
@@ -6944,6 +6975,7 @@ function resolveServeOptions(raw, env = process.env) {
6944
6975
  ...serverUrl ? { publicOrigin: serverUrl.origin } : {},
6945
6976
  auth,
6946
6977
  ...auth.type === "remote_credentials" ? { remoteCredentialStateDir } : {},
6978
+ ...upstreamUrl ? { upstreamUrl } : {},
6947
6979
  allowUnauthenticatedHttp: raw.allowUnauthenticatedHttp === true,
6948
6980
  warnUnauthenticatedNetwork: !loopback && auth.type === "development_unauthenticated",
6949
6981
  loopback,
@@ -6975,6 +7007,42 @@ function normalizeHttpPath(value) {
6975
7007
  function serverUrlHost(url) {
6976
7008
  return url?.hostname.replace(/^\[(.*)\]$/u, "$1");
6977
7009
  }
7010
+ function rejectSelfReferentialUpstream(upstreamUrl, local) {
7011
+ if (sameServerBase(parseServerBaseUrl(upstreamUrl), localServeBaseUrl(local))) throw new CapletsError("REQUEST_INVALID", "--upstream-url must not point back to this runtime.");
7012
+ }
7013
+ function localServeBaseUrl(local) {
7014
+ const origin = local.origin ?? `http://${formatHost$3(local.host)}:${local.port}`;
7015
+ const url = new URL(origin);
7016
+ url.pathname = local.path;
7017
+ url.search = "";
7018
+ url.hash = "";
7019
+ return url;
7020
+ }
7021
+ function sameServerBase(left, right) {
7022
+ return left.protocol === right.protocol && sameHost(left.hostname, right.hostname) && effectivePort(left) === effectivePort(right) && normalizePath(left.pathname) === normalizePath(right.pathname);
7023
+ }
7024
+ function sameHost(left, right) {
7025
+ if (left === right) return true;
7026
+ const normalizedLeft = normalizeLoopbackHost(left);
7027
+ const normalizedRight = normalizeLoopbackHost(right);
7028
+ return normalizedLeft !== void 0 && normalizedLeft === normalizedRight;
7029
+ }
7030
+ function normalizeLoopbackHost(host) {
7031
+ const normalized = host.toLowerCase().replace(/^\[(.*)\]$/u, "$1");
7032
+ if (normalized === "localhost" || normalized === "::1") return "loopback";
7033
+ if (normalized === "0.0.0.0" || normalized === "::") return "loopback";
7034
+ if (/^127(?:\.\d{1,3}){3}$/u.test(normalized)) return "loopback";
7035
+ }
7036
+ function effectivePort(url) {
7037
+ if (url.port) return url.port;
7038
+ return url.protocol === "https:" ? "443" : "80";
7039
+ }
7040
+ function normalizePath(path) {
7041
+ return path === "/" ? "/" : path.replace(/\/+$/u, "");
7042
+ }
7043
+ function formatHost$3(host) {
7044
+ return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
7045
+ }
6978
7046
  function nonEmpty$2(value, label) {
6979
7047
  if (value === void 0) return;
6980
7048
  const trimmed = value.trim();
@@ -10481,291 +10549,6 @@ var logger = (fn = console.log) => {
10481
10549
  };
10482
10550
  };
10483
10551
  //#endregion
10484
- //#region src/attach/api.ts
10485
- async function buildAttachProjection(engine) {
10486
- const snapshot = await engine.exposureSnapshot();
10487
- const partial = sortAttachProjectionInput({
10488
- caplets: snapshot.progressiveCaplets.map(progressiveCapletExport),
10489
- tools: snapshot.directTools.map(toolExport),
10490
- resources: snapshot.directResources.map(resourceExport),
10491
- resourceTemplates: snapshot.directResourceTemplates.map(resourceTemplateExport),
10492
- prompts: snapshot.directPrompts.map(promptExport),
10493
- completions: completionExports(snapshot),
10494
- codeModeCaplets: snapshot.codeModeCaplets.map(codeModeCapletExport),
10495
- diagnostics: snapshot.hiddenCaplets.map((hidden) => ({
10496
- code: `ATTACH_CAPLET_${hidden.reason.toUpperCase()}`,
10497
- message: `Caplet ${hidden.capletId} is not exported: ${hidden.reason}.`,
10498
- capletId: hidden.capletId,
10499
- ...hidden.error ? { details: hidden.error } : {}
10500
- }))
10501
- });
10502
- const revision = revisionFor(partial);
10503
- const manifest = {
10504
- version: 1,
10505
- revision,
10506
- generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
10507
- ...withRevisionExportIds(revision, partial)
10508
- };
10509
- return {
10510
- manifest,
10511
- routes: routesFor(manifest)
10512
- };
10513
- }
10514
- async function invokeAttachExport(engine, projection, request) {
10515
- if (request.revision !== projection.manifest.revision) throw new CapletsError("ATTACH_MANIFEST_STALE", "Attach manifest revision is stale.");
10516
- const route = projection.routes.get(request.exportId);
10517
- if (!route || route.kind !== request.kind) throw new CapletsError("ATTACH_EXPORT_NOT_FOUND", "Attach export was not found.");
10518
- if (route.kind === "caplet") return await engine.execute(route.capletId, request.input);
10519
- if (route.kind === "tool") return await engine.executeDirectTool(route.capletId, route.downstreamName, isRecord$3(request.input) ? request.input : {});
10520
- if (route.kind === "resource") return await engine.readDirectResource(route.capletId, route.downstreamUri);
10521
- if (route.kind === "resourceTemplate") {
10522
- const uri = isRecord$3(request.input) && typeof request.input.uri === "string" ? request.input.uri : void 0;
10523
- if (!uri) throw new CapletsError("REQUEST_INVALID", "Attach resource template invoke requires input.uri.");
10524
- const downstreamUri = downstreamResourceUri(route.capletId, uri);
10525
- if (!directResourceUriMatchesTemplate(downstreamUri, route.downstreamUriTemplate)) throw new CapletsError("ATTACH_EXPORT_NOT_FOUND", "Attach resource URI does not match the exported resource template.");
10526
- return await engine.readDirectResource(route.capletId, downstreamUri);
10527
- }
10528
- if (route.kind === "prompt") return await engine.getDirectPrompt(route.capletId, route.downstreamName, isRecord$3(request.input) ? stringifyRecord(request.input) : {});
10529
- if (route.kind === "completion") return await engine.execute(route.capletId, {
10530
- ...normalizeCompletionInput(projection.manifest, route.capletId, request.input),
10531
- operation: "complete"
10532
- });
10533
- throw new CapletsError("REQUEST_INVALID", "Attach export kind is not invokable via /v1/attach/invoke.");
10534
- }
10535
- function attachErrorResponse(error) {
10536
- const safe = toSafeError(error, "INTERNAL_ERROR");
10537
- return {
10538
- status: safe.code === "ATTACH_MANIFEST_STALE" ? 409 : safe.code === "ATTACH_EXPORT_NOT_FOUND" ? 404 : safe.code === "REQUEST_INVALID" ? 400 : 500,
10539
- body: {
10540
- ok: false,
10541
- error: safe
10542
- }
10543
- };
10544
- }
10545
- function progressiveCapletExport(entry) {
10546
- const inputSchema = generatedToolInputJsonSchemaForCaplet(entry.caplet);
10547
- return {
10548
- stableId: `progressive:${entry.caplet.server}`,
10549
- kind: "caplet",
10550
- name: entry.caplet.server,
10551
- title: entry.caplet.name,
10552
- description: entry.caplet.description,
10553
- inputSchema,
10554
- schemaHash: schemaHash(inputSchema),
10555
- capletId: entry.caplet.server,
10556
- shadowing: shadowingPolicy(entry.caplet)
10557
- };
10558
- }
10559
- function codeModeCapletExport(entry) {
10560
- return {
10561
- stableId: `code_mode:${entry.caplet.server}`,
10562
- kind: "caplet",
10563
- name: entry.caplet.name,
10564
- title: entry.caplet.name,
10565
- description: entry.caplet.description,
10566
- schemaHash: null,
10567
- capletId: entry.caplet.server,
10568
- shadowing: shadowingPolicy(entry.caplet)
10569
- };
10570
- }
10571
- function toolExport(entry) {
10572
- return {
10573
- stableId: `tool:${entry.caplet.server}:${entry.downstreamName}`,
10574
- kind: "tool",
10575
- name: entry.name,
10576
- downstreamName: entry.downstreamName,
10577
- title: entry.tool.name,
10578
- description: entry.tool.description,
10579
- inputSchema: entry.tool.inputSchema,
10580
- outputSchema: entry.tool.outputSchema,
10581
- annotations: entry.tool.annotations,
10582
- schemaHash: schemaHash({
10583
- input: entry.tool.inputSchema,
10584
- output: entry.tool.outputSchema
10585
- }),
10586
- capletId: entry.caplet.server,
10587
- shadowing: shadowingPolicy(entry.caplet)
10588
- };
10589
- }
10590
- function resourceExport(entry) {
10591
- return {
10592
- stableId: `resource:${entry.caplet.server}:${entry.downstreamUri}`,
10593
- kind: "resource",
10594
- uri: entry.uri,
10595
- downstreamUri: entry.downstreamUri,
10596
- title: entry.resource.name,
10597
- description: entry.resource.description,
10598
- ...entry.resource.mimeType ? { mimeType: entry.resource.mimeType } : {},
10599
- ...typeof entry.resource.size === "number" ? { size: entry.resource.size } : {},
10600
- schemaHash: null,
10601
- capletId: entry.caplet.server,
10602
- shadowing: shadowingPolicy(entry.caplet)
10603
- };
10604
- }
10605
- function resourceTemplateExport(entry) {
10606
- return {
10607
- stableId: `resourceTemplate:${entry.caplet.server}:${entry.downstreamUriTemplate}`,
10608
- kind: "resourceTemplate",
10609
- uriTemplate: entry.uriTemplate,
10610
- downstreamUriTemplate: entry.downstreamUriTemplate,
10611
- title: entry.resourceTemplate.name,
10612
- description: entry.resourceTemplate.description,
10613
- ...entry.resourceTemplate.mimeType ? { mimeType: entry.resourceTemplate.mimeType } : {},
10614
- schemaHash: null,
10615
- capletId: entry.caplet.server,
10616
- shadowing: shadowingPolicy(entry.caplet)
10617
- };
10618
- }
10619
- function promptExport(entry) {
10620
- const inputSchema = { arguments: entry.prompt.arguments ?? [] };
10621
- return {
10622
- stableId: `prompt:${entry.caplet.server}:${entry.downstreamName}`,
10623
- kind: "prompt",
10624
- name: entry.name,
10625
- downstreamName: entry.downstreamName,
10626
- title: entry.prompt.name,
10627
- description: entry.prompt.description,
10628
- inputSchema,
10629
- schemaHash: schemaHash(inputSchema),
10630
- capletId: entry.caplet.server,
10631
- shadowing: shadowingPolicy(entry.caplet)
10632
- };
10633
- }
10634
- function completionExports(snapshot) {
10635
- return [...new Map([...snapshot.directPrompts, ...snapshot.directResourceTemplates].map((entry) => [entry.caplet.server, entry.caplet])).entries()].sort(([left], [right]) => left.localeCompare(right)).map(([capletId, caplet]) => ({
10636
- stableId: `completion:${capletId}`,
10637
- kind: "completion",
10638
- name: `${capletId}:complete`,
10639
- title: "Complete",
10640
- description: `MCP completion for ${capletId}.`,
10641
- schemaHash: null,
10642
- capletId,
10643
- shadowing: shadowingPolicy(caplet)
10644
- }));
10645
- }
10646
- function shadowingPolicy(caplet) {
10647
- return caplet.shadowing ?? "forbid";
10648
- }
10649
- function sortAttachProjectionInput(partial) {
10650
- return {
10651
- caplets: sortByStableId(partial.caplets),
10652
- tools: sortByStableId(partial.tools),
10653
- resources: sortByStableId(partial.resources),
10654
- resourceTemplates: sortByStableId(partial.resourceTemplates),
10655
- prompts: sortByStableId(partial.prompts),
10656
- completions: sortByStableId(partial.completions),
10657
- codeModeCaplets: sortByStableId(partial.codeModeCaplets),
10658
- diagnostics: [...partial.diagnostics].sort((left, right) => diagnosticSortKey(left).localeCompare(diagnosticSortKey(right)))
10659
- };
10660
- }
10661
- function sortByStableId(entries) {
10662
- return [...entries].sort((left, right) => left.stableId.localeCompare(right.stableId));
10663
- }
10664
- function diagnosticSortKey(diagnostic) {
10665
- return stableJsonStringify({
10666
- code: diagnostic.code,
10667
- capletId: diagnostic.capletId ?? "",
10668
- message: diagnostic.message
10669
- });
10670
- }
10671
- function revisionFor(value) {
10672
- return `sha256:${createHash("sha256").update(stableJsonStringify(value)).digest("hex")}`;
10673
- }
10674
- function withRevisionExportIds(revision, partial) {
10675
- return {
10676
- ...partial,
10677
- caplets: partial.caplets.map((entry) => withExportId(revision, entry)),
10678
- tools: partial.tools.map((entry) => withExportId(revision, entry)),
10679
- resources: partial.resources.map((entry) => withExportId(revision, entry)),
10680
- resourceTemplates: partial.resourceTemplates.map((entry) => withExportId(revision, entry)),
10681
- prompts: partial.prompts.map((entry) => withExportId(revision, entry)),
10682
- completions: partial.completions.map((entry) => withExportId(revision, entry)),
10683
- codeModeCaplets: partial.codeModeCaplets.map((entry) => withExportId(revision, entry))
10684
- };
10685
- }
10686
- function withExportId(revision, entry) {
10687
- return {
10688
- ...entry,
10689
- exportId: `${revision}:${entry.stableId}`
10690
- };
10691
- }
10692
- function routesFor(manifest) {
10693
- const routes = /* @__PURE__ */ new Map();
10694
- for (const entry of manifest.caplets) routes.set(entry.exportId, {
10695
- kind: "caplet",
10696
- capletId: entry.capletId
10697
- });
10698
- for (const entry of manifest.tools) routes.set(entry.exportId, {
10699
- kind: "tool",
10700
- capletId: entry.capletId,
10701
- downstreamName: entry.downstreamName
10702
- });
10703
- for (const entry of manifest.resources) routes.set(entry.exportId, {
10704
- kind: "resource",
10705
- capletId: entry.capletId,
10706
- downstreamUri: entry.downstreamUri
10707
- });
10708
- for (const entry of manifest.resourceTemplates) routes.set(entry.exportId, {
10709
- kind: "resourceTemplate",
10710
- capletId: entry.capletId,
10711
- downstreamUriTemplate: entry.downstreamUriTemplate
10712
- });
10713
- for (const entry of manifest.prompts) routes.set(entry.exportId, {
10714
- kind: "prompt",
10715
- capletId: entry.capletId,
10716
- downstreamName: entry.downstreamName
10717
- });
10718
- for (const entry of manifest.completions) routes.set(entry.exportId, {
10719
- kind: "completion",
10720
- capletId: entry.capletId
10721
- });
10722
- for (const entry of manifest.codeModeCaplets) routes.set(entry.exportId, {
10723
- kind: "caplet",
10724
- capletId: entry.capletId
10725
- });
10726
- return routes;
10727
- }
10728
- function normalizeCompletionInput(manifest, capletId, input) {
10729
- if (!isRecord$3(input)) return {};
10730
- const ref = input.ref;
10731
- if (!isRecord$3(ref)) return input;
10732
- if (ref.type === "prompt" && typeof ref.name === "string") {
10733
- const prompt = manifest.prompts.find((entry) => entry.capletId === capletId && (entry.name === ref.name || entry.downstreamName === ref.name));
10734
- if (!prompt) return input;
10735
- return {
10736
- ...input,
10737
- ref: {
10738
- ...ref,
10739
- name: prompt.downstreamName
10740
- }
10741
- };
10742
- }
10743
- if (ref.type === "resourceTemplate" && typeof ref.uri === "string") {
10744
- const resourceTemplate = manifest.resourceTemplates.find((entry) => entry.capletId === capletId && (entry.uriTemplate === ref.uri || entry.downstreamUriTemplate === ref.uri));
10745
- if (!resourceTemplate) return input;
10746
- return {
10747
- ...input,
10748
- ref: {
10749
- ...ref,
10750
- uri: resourceTemplate.downstreamUriTemplate
10751
- }
10752
- };
10753
- }
10754
- return input;
10755
- }
10756
- function isRecord$3(value) {
10757
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
10758
- }
10759
- function stringifyRecord(record) {
10760
- return Object.fromEntries(Object.entries(record).map(([key, value]) => [key, typeof value === "string" ? value : JSON.stringify(value)]));
10761
- }
10762
- function downstreamResourceUri(capletId, uri) {
10763
- if (!uri.startsWith("caplets://")) return uri;
10764
- const decoded = decodeDirectResourceUri(uri);
10765
- if (decoded.capletId !== capletId) throw new CapletsError("ATTACH_EXPORT_NOT_FOUND", "Attach resource template URI belongs to a different Caplet.");
10766
- return decoded.downstreamUri;
10767
- }
10768
- //#endregion
10769
10552
  //#region src/cli/install.ts
10770
10553
  function installCaplets(repo, options = {}) {
10771
10554
  const source = resolveInstallSource(repo);
@@ -12109,14 +11892,24 @@ function parseSupersededRefreshTokens(value) {
12109
11892
  }
12110
11893
  //#endregion
12111
11894
  //#region src/serve/http.ts
11895
+ const CAPLETS_STACK_CHAIN_HEADER = "caplets-stack-chain";
11896
+ const ATTACH_SESSION_IDLE_TIMEOUT_MS = 10 * 6e4;
11897
+ const ATTACH_SESSION_PRUNE_INTERVAL_MS = 6e4;
12112
11898
  function createHttpServeApp(options, engine, io = {}) {
12113
11899
  const app = new Hono();
12114
11900
  const sessions = /* @__PURE__ */ new Map();
11901
+ const attachSessions = /* @__PURE__ */ new Map();
11902
+ const defaultAttachSessions = /* @__PURE__ */ new Map();
11903
+ const defaultAttachSessionPromises = /* @__PURE__ */ new Map();
12115
11904
  const attachEventStreams = /* @__PURE__ */ new Set();
11905
+ const attachSessionPruneTimer = setInterval(() => pruneIdleAttachSessions(), ATTACH_SESSION_PRUNE_INTERVAL_MS);
11906
+ attachSessionPruneTimer.unref?.();
12116
11907
  const writeErr = io.writeErr ?? process.stderr.write.bind(process.stderr);
12117
11908
  const paths = servicePaths(options.path);
11909
+ const stackIdentity = httpStackIdentity(options);
12118
11910
  const authFlowStore = io.authFlowStore ?? new RemoteAuthFlowStore();
12119
11911
  const exposeAttach = io.exposeAttach ?? true;
11912
+ const exposeAttachSessions = exposeAttach && Boolean(io.attachSessionFactory);
12120
11913
  const remoteCredentialStore = remoteCredentialStoreForOptions(options, io.remoteCredentialStore);
12121
11914
  if (options.auth.type === "remote_credentials" && options.trustProxy === true && options.publicOrigin === void 0) throw new CapletsError("REQUEST_INVALID", "Remote credential auth with --trust-proxy requires CAPLETS_SERVER_URL.");
12122
11915
  const protectedRouteAuth = routeAuth(options, remoteCredentialStore, paths.base);
@@ -12130,14 +11923,20 @@ function createHttpServeApp(options, engine, io = {}) {
12130
11923
  name: "caplets",
12131
11924
  transport: "http",
12132
11925
  base: paths.base,
12133
- versions: [versionDiscovery(paths, exposeAttach, remote)],
11926
+ versions: [versionDiscovery(paths, {
11927
+ exposeAttach,
11928
+ exposeAttachSessions
11929
+ }, remote)],
12134
11930
  auth: { type: options.auth.type },
12135
11931
  ...remote ? { remote } : {}
12136
11932
  });
12137
11933
  });
12138
11934
  app.get(paths.version, (c) => {
12139
11935
  const remote = remoteCredentialStore ? remoteHostMetadata(c.req.url, paths.base, options, (name) => c.req.header(name)) : void 0;
12140
- return c.json(versionDiscovery(paths, exposeAttach, remote));
11936
+ return c.json(versionDiscovery(paths, {
11937
+ exposeAttach,
11938
+ exposeAttachSessions
11939
+ }, remote));
12141
11940
  });
12142
11941
  app.get(paths.health, (c) => c.json({ status: "ok" }));
12143
11942
  if (remoteCredentialStore) {
@@ -12272,14 +12071,68 @@ function createHttpServeApp(options, engine, io = {}) {
12272
12071
  return session.transport.handleRequest(c);
12273
12072
  });
12274
12073
  if (exposeAttach) {
12074
+ if (io.attachSessionFactory) {
12075
+ app.post(paths.attachSessions, attachHostProtection, protectedRouteAuth, async (c) => {
12076
+ try {
12077
+ const metadata = parseAttachSessionMetadata(await parseJsonObject(c.req.json(), "Attach session request"), { allowProjectContext: allowAttachSessionProjectContext(options, c.req.url, (name) => c.req.header(name)) });
12078
+ const context = attachSessionContext(c.req.header(CAPLETS_STACK_CHAIN_HEADER));
12079
+ const sessionId = randomUUID();
12080
+ const session = await io.attachSessionFactory(metadata, context);
12081
+ attachSessions.set(sessionId, {
12082
+ session,
12083
+ lastUsedAt: Date.now()
12084
+ });
12085
+ pruneIdleAttachSessions();
12086
+ return c.json({ sessionId }, 201);
12087
+ } catch (error) {
12088
+ const response = attachErrorResponse(error);
12089
+ return c.json(response.body, response.status);
12090
+ }
12091
+ });
12092
+ app.delete(routePath(paths.attachSessions, ":sessionId"), attachHostProtection, protectedRouteAuth, async (c) => {
12093
+ const sessionId = c.req.param("sessionId");
12094
+ if (!sessionId) return c.json({
12095
+ ok: false,
12096
+ error: { code: "REQUEST_INVALID" }
12097
+ }, 400);
12098
+ const record = attachSessions.get(sessionId);
12099
+ attachSessions.delete(sessionId);
12100
+ await record?.session.close();
12101
+ return c.json({ ok: true });
12102
+ });
12103
+ }
12275
12104
  app.get(paths.attachManifest, attachHostProtection, protectedRouteAuth, async (c) => {
12276
- const attachProjection = await buildAttachProjection(engine);
12277
- return c.json(attachProjection.manifest);
12105
+ try {
12106
+ const attachSessionId = c.req.header(CAPLETS_ATTACH_SESSION_HEADER);
12107
+ const attachSession = attachSessionId ? attachSessionForRequest(attachSessionId) : await fallbackAttachSession(attachSessionContext(c.req.header(CAPLETS_STACK_CHAIN_HEADER)));
12108
+ if (attachSession) return c.json(await attachSession.manifest());
12109
+ const attachProjection = await buildAttachProjection(engine);
12110
+ return c.json(attachProjection.manifest);
12111
+ } catch (error) {
12112
+ const response = attachErrorResponse(error);
12113
+ return c.json(response.body, response.status);
12114
+ }
12115
+ });
12116
+ app.get(paths.attachEvents, attachHostProtection, protectedRouteAuth, async (c) => {
12117
+ try {
12118
+ const attachSessionId = c.req.header(CAPLETS_ATTACH_SESSION_HEADER);
12119
+ return attachEventsResponse(attachEventSource(engine, attachSessionId ? attachSessionForRequest(attachSessionId) : await fallbackAttachSession(attachSessionContext(c.req.header(CAPLETS_STACK_CHAIN_HEADER)))), attachEventStreams, { onActivity: () => {
12120
+ if (attachSessionId) touchAttachSession(attachSessionId);
12121
+ } });
12122
+ } catch (error) {
12123
+ const response = attachErrorResponse(error);
12124
+ return c.json(response.body, response.status);
12125
+ }
12278
12126
  });
12279
- app.get(paths.attachEvents, attachHostProtection, protectedRouteAuth, () => attachEventsResponse(engine, attachEventStreams));
12280
12127
  app.post(paths.attachInvoke, attachHostProtection, protectedRouteAuth, async (c) => {
12281
12128
  try {
12282
12129
  const request = await parseAttachInvokeRequest(c.req.json());
12130
+ const attachSessionId = c.req.header(CAPLETS_ATTACH_SESSION_HEADER);
12131
+ const attachSession = attachSessionId ? attachSessionForRequest(attachSessionId) : await fallbackAttachSession(attachSessionContext(c.req.header(CAPLETS_STACK_CHAIN_HEADER)));
12132
+ if (attachSession) return c.json({
12133
+ ok: true,
12134
+ data: await attachSession.invoke(request)
12135
+ });
12283
12136
  const result = await invokeAttachExport(engine, await buildAttachProjection(engine), request);
12284
12137
  return c.json({
12285
12138
  ok: true,
@@ -12291,6 +12144,52 @@ function createHttpServeApp(options, engine, io = {}) {
12291
12144
  }
12292
12145
  });
12293
12146
  }
12147
+ function attachSessionForRequest(sessionId) {
12148
+ pruneIdleAttachSessions();
12149
+ if (!sessionId) return void 0;
12150
+ const record = attachSessions.get(sessionId);
12151
+ if (!record) throw new CapletsError("REQUEST_INVALID", "Attach session was not found.");
12152
+ record.lastUsedAt = Date.now();
12153
+ return record.session;
12154
+ }
12155
+ async function fallbackAttachSession(context) {
12156
+ if (!io.defaultAttachSessionFactory) return void 0;
12157
+ const key = context.stackChain.join("\0");
12158
+ const existing = defaultAttachSessions.get(key);
12159
+ if (existing) return existing;
12160
+ let pending = defaultAttachSessionPromises.get(key);
12161
+ if (!pending) {
12162
+ pending = Promise.resolve(io.defaultAttachSessionFactory({}, context)).then((session) => {
12163
+ defaultAttachSessions.set(key, session);
12164
+ defaultAttachSessionPromises.delete(key);
12165
+ return session;
12166
+ }, (error) => {
12167
+ defaultAttachSessionPromises.delete(key);
12168
+ throw error;
12169
+ });
12170
+ defaultAttachSessionPromises.set(key, pending);
12171
+ }
12172
+ return await pending;
12173
+ }
12174
+ function attachSessionContext(header) {
12175
+ const incoming = stackChainFromHeader(header);
12176
+ if (incoming.includes(stackIdentity)) throw new CapletsError("REQUEST_INVALID", "Stacked runtime upstream cycle detected.");
12177
+ return { stackChain: [...incoming, stackIdentity] };
12178
+ }
12179
+ function touchAttachSession(sessionId) {
12180
+ const record = attachSessions.get(sessionId);
12181
+ if (record) record.lastUsedAt = Date.now();
12182
+ }
12183
+ function pruneIdleAttachSessions() {
12184
+ const expiresBefore = Date.now() - ATTACH_SESSION_IDLE_TIMEOUT_MS;
12185
+ for (const [sessionId, record] of attachSessions) {
12186
+ if (record.lastUsedAt >= expiresBefore) continue;
12187
+ attachSessions.delete(sessionId);
12188
+ record.session.close().catch((error) => {
12189
+ writeErr(`Could not close idle attach session: ${errorMessage$1(error)}\n`);
12190
+ });
12191
+ }
12192
+ }
12294
12193
  app.post(paths.control, protectedRouteAuth, async (c) => {
12295
12194
  let request;
12296
12195
  try {
@@ -12314,31 +12213,9 @@ function createHttpServeApp(options, engine, io = {}) {
12314
12213
  bindingId: c.req.param("bindingId"),
12315
12214
  state: "not_attached"
12316
12215
  }));
12317
- app.post(routePath(paths.projectBindings, "sessions"), protectedRouteAuth, (c) => {
12318
- const bindingId = randomUUID();
12319
- return c.json({
12320
- binding: {
12321
- bindingId,
12322
- state: "attaching",
12323
- syncState: "pending"
12324
- },
12325
- sessionId: randomUUID()
12326
- }, 201);
12327
- });
12328
- app.post(routePath(paths.projectBindings, ":bindingId/heartbeat"), protectedRouteAuth, (c) => c.json({
12329
- ok: true,
12330
- binding: {
12331
- bindingId: c.req.param("bindingId"),
12332
- state: "ready"
12333
- }
12334
- }));
12335
- app.delete(routePath(paths.projectBindings, ":bindingId/session"), protectedRouteAuth, (c) => c.json({
12336
- ok: true,
12337
- binding: {
12338
- bindingId: c.req.param("bindingId"),
12339
- state: "ended"
12340
- }
12341
- }));
12216
+ app.post(routePath(paths.projectBindings, "sessions"), protectedRouteAuth, (c) => c.json(projectBindingUnsupported(), 501));
12217
+ app.post(routePath(paths.projectBindings, ":bindingId/heartbeat"), protectedRouteAuth, (c) => c.json(projectBindingUnsupported(c.req.param("bindingId")), 501));
12218
+ app.delete(routePath(paths.projectBindings, ":bindingId/session"), protectedRouteAuth, (c) => c.json(projectBindingUnsupported(c.req.param("bindingId")), 501));
12342
12219
  app.get(routePath(paths.control, "auth/callback/:flowId"), async (c) => {
12343
12220
  const flowId = c.req.param("flowId");
12344
12221
  const result = await dispatchRemoteCliRequest({
@@ -12353,11 +12230,17 @@ function createHttpServeApp(options, engine, io = {}) {
12353
12230
  });
12354
12231
  app.notFound((c) => c.json({ error: "not_found" }, 404));
12355
12232
  app.closeCapletsSessions = async () => {
12233
+ clearInterval(attachSessionPruneTimer);
12356
12234
  for (const stream of attachEventStreams) stream.close();
12357
12235
  await Promise.allSettled([...sessions.values()].map(async (session) => {
12358
12236
  await session.server.close();
12359
12237
  }));
12360
12238
  sessions.clear();
12239
+ await Promise.allSettled([...attachSessions.values()].map((record) => record.session.close()));
12240
+ attachSessions.clear();
12241
+ await Promise.allSettled([...defaultAttachSessions.values()].map((session) => session.close()));
12242
+ defaultAttachSessions.clear();
12243
+ defaultAttachSessionPromises.clear();
12361
12244
  };
12362
12245
  if (options.warnUnauthenticatedNetwork) writeErr(`Warning: Caplets MCP HTTP server is listening on ${options.host} without authentication.\n`);
12363
12246
  return app;
@@ -12390,9 +12273,36 @@ function remoteCredentialSourceHint(trustProxy, header) {
12390
12273
  const sourceHint = firstForwardedValue(header("x-forwarded-for")) ?? firstForwardedValue(header("x-real-ip")) ?? firstForwardedValue(header("cf-connecting-ip"));
12391
12274
  return sourceHint ? { sourceHint } : {};
12392
12275
  }
12276
+ function projectBindingUnsupported(bindingId) {
12277
+ return {
12278
+ ok: false,
12279
+ error: {
12280
+ code: "UNSUPPORTED_CAPABILITY",
12281
+ message: "Self-hosted Project Binding sessions are not implemented by this runtime."
12282
+ },
12283
+ ...bindingId ? { binding: {
12284
+ bindingId,
12285
+ state: "not_attached"
12286
+ } } : {}
12287
+ };
12288
+ }
12393
12289
  function firstForwardedValue(value) {
12394
12290
  return value?.split(",", 1)[0]?.trim() || void 0;
12395
12291
  }
12292
+ function errorMessage$1(error) {
12293
+ return error instanceof Error ? error.message : String(error);
12294
+ }
12295
+ function httpStackIdentity(options) {
12296
+ const origin = options.publicOrigin ?? `http://${formatHost$2(options.host)}:${options.port}`;
12297
+ const url = new URL(origin);
12298
+ url.pathname = options.path;
12299
+ url.search = "";
12300
+ url.hash = "";
12301
+ return url.toString();
12302
+ }
12303
+ function stackChainFromHeader(header) {
12304
+ return (header ?? "").split(",").map((value) => value.trim()).filter((value) => value.length > 0);
12305
+ }
12396
12306
  function remoteHostMetadata(requestUrl, basePath, options, header) {
12397
12307
  const audience = remoteCredentialHostUrl(requestUrl, basePath, options.publicOrigin, options.trustProxy, header);
12398
12308
  return {
@@ -12400,7 +12310,9 @@ function remoteHostMetadata(requestUrl, basePath, options, header) {
12400
12310
  audience
12401
12311
  };
12402
12312
  }
12403
- function versionDiscovery(paths, exposeAttach = true, remote) {
12313
+ function versionDiscovery(paths, options = {}, remote) {
12314
+ const exposeAttach = options.exposeAttach ?? true;
12315
+ const exposeAttachSessions = options.exposeAttachSessions ?? false;
12404
12316
  return {
12405
12317
  version: 1,
12406
12318
  path: paths.version,
@@ -12409,6 +12321,7 @@ function versionDiscovery(paths, exposeAttach = true, remote) {
12409
12321
  mcp: paths.mcp,
12410
12322
  admin: paths.control,
12411
12323
  ...exposeAttach ? {
12324
+ ...exposeAttachSessions ? { attachSessions: paths.attachSessions } : {},
12412
12325
  attachManifest: paths.attachManifest,
12413
12326
  attachEvents: paths.attachEvents,
12414
12327
  attachInvoke: paths.attachInvoke
@@ -12438,10 +12351,82 @@ async function parseAttachInvokeRequest(input) {
12438
12351
  function isAttachExportKind(value) {
12439
12352
  return value === "caplet" || value === "tool" || value === "resource" || value === "resourceTemplate" || value === "prompt" || value === "completion";
12440
12353
  }
12441
- function attachEventsResponse(engine, activeStreams) {
12354
+ function parseAttachSessionMetadata(input, options) {
12355
+ const rawProjectRoot = optionalStringField(input, "projectRoot");
12356
+ const rawProjectConfigPath = optionalStringField(input, "projectConfigPath");
12357
+ if (!options.allowProjectContext && (rawProjectRoot || rawProjectConfigPath)) throw new CapletsError("REQUEST_INVALID", "Attach session project context is only accepted by loopback runtimes.");
12358
+ const projectRoot = canonicalProjectRoot(rawProjectRoot);
12359
+ const projectConfigPath = canonicalProjectConfigPath(rawProjectConfigPath, projectRoot);
12360
+ return {
12361
+ ...projectRoot ? { projectRoot } : {},
12362
+ ...projectConfigPath ? { projectConfigPath } : {}
12363
+ };
12364
+ }
12365
+ function canonicalProjectRoot(projectRoot) {
12366
+ if (!projectRoot) return void 0;
12367
+ try {
12368
+ const canonical = realpathSync(projectRoot);
12369
+ if (!statSync(canonical).isDirectory()) throw new Error("projectRoot is not a directory.");
12370
+ return canonical;
12371
+ } catch (error) {
12372
+ throw new CapletsError("REQUEST_INVALID", "projectRoot must be an existing directory.", error);
12373
+ }
12374
+ }
12375
+ function canonicalProjectConfigPath(projectConfigPath, projectRoot) {
12376
+ if (!projectRoot) {
12377
+ if (!projectConfigPath) return void 0;
12378
+ throw new CapletsError("REQUEST_INVALID", "projectConfigPath requires projectRoot.");
12379
+ }
12380
+ const expectedProjectConfigPath = resolve(projectRoot, ".caplets", "config.json");
12381
+ const lexicalConfigPath = projectConfigPath === void 0 ? expectedProjectConfigPath : isAbsolute(projectConfigPath) ? projectConfigPath : resolve(projectRoot, projectConfigPath);
12382
+ if (resolve(projectConfigPath === void 0 ? expectedProjectConfigPath : canonicalizeExistingParentPath(lexicalConfigPath)) !== expectedProjectConfigPath) throw new CapletsError("REQUEST_INVALID", "projectConfigPath must be <projectRoot>/.caplets/config.json.");
12383
+ if (!existsSync(expectedProjectConfigPath)) return expectedProjectConfigPath;
12384
+ const expected = realpathSync(expectedProjectConfigPath);
12385
+ if (!pathIsInside(expected, projectRoot)) throw new CapletsError("REQUEST_INVALID", "projectConfigPath must resolve inside projectRoot.");
12386
+ return expected;
12387
+ }
12388
+ function canonicalizeExistingParentPath(path) {
12389
+ const parent = dirname(path);
12390
+ try {
12391
+ return resolve(realpathSync(parent), path.slice(parent.length + 1));
12392
+ } catch {
12393
+ return resolve(path);
12394
+ }
12395
+ }
12396
+ function pathIsInside(candidate, root) {
12397
+ const rel = relative(root, candidate);
12398
+ return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
12399
+ }
12400
+ function allowAttachSessionProjectContext(options, requestUrl, header) {
12401
+ if (!options.loopback) return false;
12402
+ return isLoopbackHost(attachRequestHost(options, requestUrl, header));
12403
+ }
12404
+ function attachRequestHost(options, requestUrl, header) {
12405
+ const fallback = new URL(requestUrl).host;
12406
+ const host = (options.trustProxy ? firstForwardedValue(header("x-forwarded-host")) : void 0) ?? header("host") ?? fallback;
12407
+ try {
12408
+ return new URL(`http://${host}`).hostname;
12409
+ } catch {
12410
+ return host.split(":")[0] ?? host;
12411
+ }
12412
+ }
12413
+ function attachEventSource(engine, session) {
12414
+ if (session) return {
12415
+ manifestRevision: async () => (await session.manifest()).revision,
12416
+ onManifestChanged: (listener) => session.onManifestChanged(listener)
12417
+ };
12418
+ return {
12419
+ manifestRevision: async () => (await buildAttachProjection(engine)).manifest.revision,
12420
+ onManifestChanged: (listener) => engine.onReload(() => {
12421
+ listener();
12422
+ })
12423
+ };
12424
+ }
12425
+ function attachEventsResponse(source, activeStreams, options = {}) {
12442
12426
  const encoder = new TextEncoder();
12443
12427
  let unsubscribe = () => void 0;
12444
12428
  let activeStream;
12429
+ let keepAliveTimer;
12445
12430
  let closed = false;
12446
12431
  const readable = new ReadableStream({
12447
12432
  start(controller) {
@@ -12449,17 +12434,30 @@ function attachEventsResponse(engine, activeStreams) {
12449
12434
  if (closed) return;
12450
12435
  closed = true;
12451
12436
  unsubscribe();
12437
+ if (keepAliveTimer) clearInterval(keepAliveTimer);
12452
12438
  if (activeStream) activeStreams.delete(activeStream);
12453
12439
  try {
12454
12440
  controller.close();
12455
12441
  } catch {}
12456
12442
  } };
12457
12443
  activeStreams.add(activeStream);
12444
+ options.onActivity?.();
12458
12445
  controller.enqueue(encoder.encode(": connected\n\n"));
12459
- unsubscribe = engine.onReload(() => {
12460
- buildAttachProjection(engine).then((projection) => {
12446
+ keepAliveTimer = setInterval(() => {
12447
+ if (closed) return;
12448
+ options.onActivity?.();
12449
+ try {
12450
+ controller.enqueue(encoder.encode(": keepalive\n\n"));
12451
+ } catch {
12452
+ activeStream?.close();
12453
+ }
12454
+ }, 3e4);
12455
+ keepAliveTimer.unref?.();
12456
+ unsubscribe = source.onManifestChanged(() => {
12457
+ options.onActivity?.();
12458
+ source.manifestRevision().then((revision) => {
12461
12459
  if (closed) return;
12462
- controller.enqueue(encoder.encode(`event: manifest_changed\ndata: ${JSON.stringify({ revision: projection.manifest.revision })}\n\n`));
12460
+ controller.enqueue(encoder.encode(`event: manifest_changed\ndata: ${JSON.stringify({ revision })}\n\n`));
12463
12461
  }).catch(() => void 0);
12464
12462
  });
12465
12463
  },
@@ -12489,7 +12487,7 @@ async function serveHttp(options, engineOptions = {}, writeErr = (value) => proc
12489
12487
  }
12490
12488
  });
12491
12489
  const paths = servicePaths(options.path);
12492
- const origin = `http://${formatHost$1(options.host)}:${options.port}`;
12490
+ const origin = `http://${formatHost$2(options.host)}:${options.port}`;
12493
12491
  const baseUrl = `${origin}${paths.base === "/" ? "" : paths.base}`;
12494
12492
  installHttpSignalHandlers(serve({
12495
12493
  fetch: app.fetch,
@@ -12504,20 +12502,26 @@ async function serveHttp(options, engineOptions = {}, writeErr = (value) => proc
12504
12502
  writeErr(`Auth: ${authDescription(options)}\n`);
12505
12503
  }), app, engine, writeErr);
12506
12504
  }
12507
- async function serveHttpWithSessionFactory(options, createSession, writeErr = (value) => process.stderr.write(value)) {
12508
- const resolvedEngineOptions = { exposeLocalArtifactPaths: false };
12505
+ async function serveHttpWithSessionFactory(options, createSession, writeErr = (value) => process.stderr.write(value), io = {}, engineOptions = {}) {
12506
+ const resolvedEngineOptions = {
12507
+ exposeLocalArtifactPaths: false,
12508
+ vaultRecoveryTarget: "remote",
12509
+ ...engineOptions
12510
+ };
12509
12511
  const engine = new CapletsEngine(resolvedEngineOptions);
12510
12512
  const app = createHttpServeApp(options, engine, {
12511
12513
  writeErr,
12512
- exposeAttach: false,
12514
+ exposeAttach: io.exposeAttach ?? false,
12513
12515
  sessionFactory: createSession,
12516
+ ...io.attachSessionFactory ? { attachSessionFactory: io.attachSessionFactory } : {},
12517
+ ...io.defaultAttachSessionFactory ? { defaultAttachSessionFactory: io.defaultAttachSessionFactory } : {},
12514
12518
  control: {
12515
12519
  ...resolvedEngineOptions,
12516
- projectCapletsRoot: resolveProjectCapletsRoot()
12520
+ projectCapletsRoot: projectCapletsRootForEngineOptions(resolvedEngineOptions)
12517
12521
  }
12518
12522
  });
12519
12523
  const paths = servicePaths(options.path);
12520
- const origin = `http://${formatHost$1(options.host)}:${options.port}`;
12524
+ const origin = `http://${formatHost$2(options.host)}:${options.port}`;
12521
12525
  const baseUrl = `${origin}${paths.base === "/" ? "" : paths.base}`;
12522
12526
  installHttpSignalHandlers(serve({
12523
12527
  fetch: app.fetch,
@@ -12549,6 +12553,7 @@ function servicePaths(base) {
12549
12553
  version,
12550
12554
  mcp: routePath(version, "mcp"),
12551
12555
  control: routePath(version, "admin"),
12556
+ attachSessions: routePath(attach, "sessions"),
12552
12557
  attachManifest: routePath(attach, "manifest"),
12553
12558
  attachEvents: routePath(attach, "events"),
12554
12559
  attachInvoke: routePath(attach, "invoke"),
@@ -12692,7 +12697,7 @@ function installHttpSignalHandlers(server, app, engine, writeErr) {
12692
12697
  function closeAllServerConnections(server) {
12693
12698
  server.closeAllConnections?.call(server);
12694
12699
  }
12695
- function formatHost$1(host) {
12700
+ function formatHost$2(host) {
12696
12701
  return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
12697
12702
  }
12698
12703
  //#endregion
@@ -12823,11 +12828,11 @@ async function allocateLoopbackPort() {
12823
12828
  if (!address || typeof address === "string") throw new CapletsError("SERVER_UNAVAILABLE", "Could not allocate a validation port.");
12824
12829
  return address.port;
12825
12830
  }
12826
- function formatHost(host) {
12831
+ function formatHost$1(host) {
12827
12832
  return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
12828
12833
  }
12829
12834
  function healthUrl(config, port = config.serve.port) {
12830
- return `http://${formatHost(config.serve.host)}:${port}${servicePaths(config.serve.path).health}`;
12835
+ return `http://${formatHost$1(config.serve.host)}:${port}${servicePaths(config.serve.path).health}`;
12831
12836
  }
12832
12837
  function validationSpawnCommand(config) {
12833
12838
  const planned = serviceCommand(config);
@@ -14708,6 +14713,7 @@ function createAttachNativeService(options, io) {
14708
14713
  return createNativeCapletsService({
14709
14714
  mode: options.selection.kind === "hosted_cloud" ? "cloud" : "remote",
14710
14715
  configPath: options.configPath,
14716
+ projectRoot: options.projectRoot,
14711
14717
  projectConfigPath: options.projectConfigPath,
14712
14718
  ...options.authDir ? { authDir: options.authDir } : {},
14713
14719
  remote: {
@@ -15325,15 +15331,95 @@ async function serveResolvedCaplets(resolved, engineOptions = {}, writeErr) {
15325
15331
  });
15326
15332
  return;
15327
15333
  }
15334
+ if (resolved.upstreamUrl) {
15335
+ await serveHttpWithUpstream(resolved, resolved.upstreamUrl, engineOptions, writeErr);
15336
+ return;
15337
+ }
15328
15338
  await serveHttp(resolved, {
15329
15339
  ...engineOptions,
15330
15340
  ...writeErr ? { writeErr } : {}
15331
15341
  }, writeErr);
15332
15342
  }
15343
+ async function serveHttpWithUpstream(resolved, upstreamUrl, engineOptions, writeErr) {
15344
+ const stackChain = [serveStackIdentity(resolved)];
15345
+ await serveHttpWithSessionFactory(resolved, async () => new NativeCapletsMcpSession(await createReloadedUpstreamService(upstreamUrl, engineOptions, writeErr, {}, stackChain)), writeErr, {
15346
+ exposeAttach: true,
15347
+ defaultAttachSessionFactory: async (_metadata, context) => nativeAttachSession(await createReloadedUpstreamService(upstreamUrl, engineOptions, writeErr, {}, context.stackChain)),
15348
+ attachSessionFactory: async (metadata, context) => {
15349
+ return nativeAttachSession(await createReloadedUpstreamService(upstreamUrl, engineOptions, writeErr, metadata, context.stackChain));
15350
+ }
15351
+ }, engineOptions);
15352
+ }
15353
+ async function createReloadedUpstreamService(upstreamUrl, engineOptions, writeErr, metadata = {}, stackChain = []) {
15354
+ const service = createUpstreamNativeService(upstreamUrl, engineOptions, writeErr, metadata, stackChain);
15355
+ try {
15356
+ await service.reload();
15357
+ return service;
15358
+ } catch (error) {
15359
+ await service.close().catch(() => void 0);
15360
+ throw error;
15361
+ }
15362
+ }
15363
+ function createUpstreamNativeService(upstreamUrl, engineOptions, writeErr, metadata = {}, stackChain = []) {
15364
+ return createNativeCapletsService({
15365
+ ...engineOptions,
15366
+ ...metadata.projectRoot ? { projectRoot: metadata.projectRoot } : {},
15367
+ ...metadata.projectConfigPath ? { projectConfigPath: metadata.projectConfigPath } : {},
15368
+ mode: isCapletsCloudUrl(upstreamUrl) ? "cloud" : "remote",
15369
+ remote: {
15370
+ url: upstreamUrl,
15371
+ ...stackChain.length > 0 ? { requestHeaders: { [CAPLETS_STACK_CHAIN_HEADER]: stackChain.join(",") } } : {}
15372
+ },
15373
+ ...writeErr ? { writeErr } : {}
15374
+ });
15375
+ }
15376
+ function serveStackIdentity(options) {
15377
+ const origin = options.publicOrigin ?? `http://${formatHost(options.host)}:${options.port}`;
15378
+ const url = new URL(origin);
15379
+ url.pathname = options.path;
15380
+ url.search = "";
15381
+ url.hash = "";
15382
+ return url.toString();
15383
+ }
15384
+ function formatHost(host) {
15385
+ return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
15386
+ }
15387
+ function nativeAttachSession(service) {
15388
+ let cachedProjection;
15389
+ const getProjection = async () => {
15390
+ cachedProjection ??= await buildNativeAttachProjection(service);
15391
+ return cachedProjection;
15392
+ };
15393
+ const unsubscribe = service.onToolsChanged(() => {
15394
+ cachedProjection = void 0;
15395
+ });
15396
+ return {
15397
+ manifest: async () => (await getProjection()).manifest,
15398
+ invoke: async (request) => await invokeNativeAttachExport(service, await getProjection(), request),
15399
+ onManifestChanged: (listener) => service.onToolsChanged(() => listener()),
15400
+ close: async () => {
15401
+ unsubscribe();
15402
+ await service.close();
15403
+ }
15404
+ };
15405
+ }
15333
15406
  //#endregion
15334
15407
  //#region src/cli.ts
15335
15408
  async function runCli(args, io = {}) {
15336
- const program = createProgram(io);
15409
+ let observedExitCode = 0;
15410
+ const wrappedIo = {
15411
+ ...io,
15412
+ setExitCode: (code) => {
15413
+ observedExitCode = code;
15414
+ if (io.setExitCode) io.setExitCode(code);
15415
+ else process.exitCode = code;
15416
+ }
15417
+ };
15418
+ const program = createProgram(wrappedIo);
15419
+ const trackedCommand = telemetryCommandFamilyFromArgs(args);
15420
+ const startedAt = Date.now();
15421
+ const telemetryContext = telemetryContextForIo(wrappedIo);
15422
+ const dispatcher = createTelemetryDispatcher({ stateDir: telemetryContext.stateDir });
15337
15423
  try {
15338
15424
  if (args.length === 0) {
15339
15425
  program.outputHelp();
@@ -15344,12 +15430,35 @@ async function runCli(args, io = {}) {
15344
15430
  "caplets",
15345
15431
  ...args
15346
15432
  ]);
15433
+ if (trackedCommand) await captureCliTelemetry(telemetryContext, {
15434
+ debugSink: wrappedIo.telemetryDebugSink,
15435
+ dispatcher,
15436
+ commandFamily: trackedCommand.commandFamily,
15437
+ surface: trackedCommand.surface,
15438
+ outcome: observedExitCode === 0 ? "success" : "failure",
15439
+ startedAt
15440
+ });
15347
15441
  } catch (error) {
15442
+ let normalizedError = error;
15443
+ let captureProductEvent = true;
15348
15444
  if (error instanceof CommanderError) {
15349
15445
  if (error.code === "commander.helpDisplayed" || error.code === "commander.version" || error.message === "(outputHelp)") return;
15350
- throw new CapletsError("REQUEST_INVALID", error.message);
15351
- }
15352
- throw error;
15446
+ normalizedError = new CapletsError("REQUEST_INVALID", error.message);
15447
+ captureProductEvent = false;
15448
+ }
15449
+ if (trackedCommand) await captureCliTelemetry(telemetryContext, {
15450
+ debugSink: wrappedIo.telemetryDebugSink,
15451
+ dispatcher,
15452
+ commandFamily: trackedCommand.commandFamily,
15453
+ surface: trackedCommand.surface,
15454
+ outcome: "failure",
15455
+ startedAt,
15456
+ error: normalizedError,
15457
+ productEvent: captureProductEvent
15458
+ }).catch(() => void 0);
15459
+ throw normalizedError;
15460
+ } finally {
15461
+ await dispatcher.shutdown();
15353
15462
  }
15354
15463
  }
15355
15464
  function normalizeCompletionWords(words) {
@@ -15370,6 +15479,249 @@ function collectValues(value, previous) {
15370
15479
  return [...previous, value];
15371
15480
  }
15372
15481
  const HIDDEN_INPUT_PROMPT_LABELS = { vaultValue: "Value: " };
15482
+ function telemetryContextForIo(io) {
15483
+ const env = io.env ?? process.env;
15484
+ return {
15485
+ env,
15486
+ configPath: envConfigPath(env),
15487
+ projectConfigPath: envProjectConfigPath(env),
15488
+ stateDir: io.telemetryStateDir ?? defaultTelemetryStateDir(env),
15489
+ stderrIsTTY: io.stderrIsTTY ?? process.stderr.isTTY === true,
15490
+ writeErr: io.writeErr ?? ((value) => process.stderr.write(value))
15491
+ };
15492
+ }
15493
+ function telemetryCommandFamilyFromArgs(args) {
15494
+ const command = args[0];
15495
+ if (command === void 0 || command === "--help" || command === "-h" || command === "--version" || command === "-V" || command === cliCommands$1.telemetry || command === cliCommands$1.completion || command === cliCommands$1.completeHidden) return;
15496
+ if (command === cliCommands$1.serve) return {
15497
+ commandFamily: "serve",
15498
+ surface: "serve"
15499
+ };
15500
+ if (command === cliCommands$1.attach) return {
15501
+ commandFamily: "attach",
15502
+ surface: "attach"
15503
+ };
15504
+ if (command === cliCommands$1.daemon) return {
15505
+ commandFamily: "daemon",
15506
+ surface: "daemon"
15507
+ };
15508
+ if (command === cliCommands$1.codeMode) return {
15509
+ commandFamily: "code_mode",
15510
+ surface: "code_mode"
15511
+ };
15512
+ if (command === cliCommands$1.setup) return {
15513
+ commandFamily: "setup",
15514
+ surface: "cli"
15515
+ };
15516
+ if (command === cliCommands$1.init) return {
15517
+ commandFamily: "init",
15518
+ surface: "cli"
15519
+ };
15520
+ if (command === cliCommands$1.install) return {
15521
+ commandFamily: "install",
15522
+ surface: "cli"
15523
+ };
15524
+ if (command === cliCommands$1.add) return {
15525
+ commandFamily: "add",
15526
+ surface: "cli"
15527
+ };
15528
+ if (command === cliCommands$1.doctor) return {
15529
+ commandFamily: "doctor",
15530
+ surface: "cli"
15531
+ };
15532
+ if (command === cliCommands$1.auth) return {
15533
+ commandFamily: "auth",
15534
+ surface: "cli"
15535
+ };
15536
+ if (command === cliCommands$1.remote || command === cliCommands$1.cloud) return {
15537
+ commandFamily: "remote",
15538
+ surface: "cli"
15539
+ };
15540
+ if (command === cliCommands$1.inspect) return {
15541
+ commandFamily: "inspect",
15542
+ surface: "cli"
15543
+ };
15544
+ if (command === cliCommands$1.checkBackend) return {
15545
+ commandFamily: "check",
15546
+ surface: "cli"
15547
+ };
15548
+ if (command === cliCommands$1.listTools || command === cliCommands$1.searchTools || command === cliCommands$1.getTool || command === cliCommands$1.callTool) return {
15549
+ commandFamily: "tools",
15550
+ surface: "cli"
15551
+ };
15552
+ if (command === cliCommands$1.listResources || command === cliCommands$1.searchResources || command === cliCommands$1.listResourceTemplates || command === cliCommands$1.readResource) return {
15553
+ commandFamily: "resources",
15554
+ surface: "cli"
15555
+ };
15556
+ if (command === cliCommands$1.listPrompts || command === cliCommands$1.searchPrompts || command === cliCommands$1.getPrompt) return {
15557
+ commandFamily: "prompts",
15558
+ surface: "cli"
15559
+ };
15560
+ if (command === cliCommands$1.complete) return {
15561
+ commandFamily: "complete",
15562
+ surface: "cli"
15563
+ };
15564
+ return {
15565
+ commandFamily: "unknown",
15566
+ surface: "cli"
15567
+ };
15568
+ }
15569
+ function telemetryConfigForCli(context) {
15570
+ try {
15571
+ return loadConfigWithSources(context.configPath, context.projectConfigPath, { vaultResolver: vaultBootstrapResolver }).config;
15572
+ } catch (error) {
15573
+ if (error instanceof CapletsError && error.code !== "CONFIG_INVALID") return {};
15574
+ return telemetryOnlyConfigForCli(resolveConfigPath(context.configPath)) ?? { telemetry: false };
15575
+ }
15576
+ }
15577
+ function telemetryOnlyConfigForCli(path) {
15578
+ try {
15579
+ const config = readUserConfigObject(path);
15580
+ return typeof config.telemetry === "boolean" ? { telemetry: config.telemetry } : void 0;
15581
+ } catch {
15582
+ return;
15583
+ }
15584
+ }
15585
+ function maybePrintCliTelemetryNotice(context, surface) {
15586
+ const state = resolveTelemetryState({
15587
+ config: telemetryConfigForCli(context),
15588
+ env: context.env,
15589
+ stateDir: context.stateDir,
15590
+ surface,
15591
+ visibility: "visible",
15592
+ allowWithoutNotice: true,
15593
+ createIdentity: false
15594
+ });
15595
+ if (state.status !== "enabled" || state.notice.shown) return;
15596
+ maybePrintTelemetryNotice({
15597
+ stateDir: context.stateDir,
15598
+ surface,
15599
+ stderrIsTTY: context.stderrIsTTY,
15600
+ writeErr: context.writeErr
15601
+ });
15602
+ }
15603
+ async function captureCliTelemetry(context, options) {
15604
+ try {
15605
+ maybePrintCliTelemetryNotice(context, options.surface ?? "cli");
15606
+ } catch {}
15607
+ const state = resolveTelemetryState({
15608
+ config: telemetryConfigForCli(context),
15609
+ env: context.env,
15610
+ stateDir: context.stateDir,
15611
+ surface: options.surface ?? "cli",
15612
+ visibility: "visible",
15613
+ debug: context.env.CAPLETS_TELEMETRY_DEBUG === "1"
15614
+ });
15615
+ if (state.status !== "enabled" && state.status !== "debug") return;
15616
+ const identity = state.status === "debug" ? readTelemetryIdentity({
15617
+ stateDir: context.stateDir,
15618
+ create: false
15619
+ }) : state.identity ?? readTelemetryIdentity({
15620
+ stateDir: context.stateDir,
15621
+ create: true
15622
+ });
15623
+ if (options.productEvent !== false) {
15624
+ const product = buildProductTelemetryEvent({
15625
+ name: "caplets_cli_command",
15626
+ distinctId: identity.id,
15627
+ properties: {
15628
+ package: "@caplets/core",
15629
+ version,
15630
+ surface: options.surface ?? "cli",
15631
+ runtime_mode: runtimeModeForEnv(context.env),
15632
+ execution_context: state.executionContext,
15633
+ command_family: options.commandFamily,
15634
+ outcome: options.outcome,
15635
+ duration_bucket: durationBucket(Date.now() - options.startedAt)
15636
+ }
15637
+ });
15638
+ if (state.status === "debug") options.debugSink?.capture("debug", product);
15639
+ else await (options.dispatcher ?? createTelemetryDispatcher({ stateDir: context.stateDir })).capture(state, product);
15640
+ }
15641
+ if (options.outcome !== "failure") return;
15642
+ if (options.error === void 0) return;
15643
+ const reliability = buildReliabilityTelemetryEvent({
15644
+ name: "caplets_reliability_error",
15645
+ properties: {
15646
+ package: "@caplets/core",
15647
+ version,
15648
+ surface: options.surface ?? "cli",
15649
+ runtime_mode: runtimeModeForEnv(context.env),
15650
+ command_family: options.commandFamily,
15651
+ error_code: errorCodeForTelemetry(options.error),
15652
+ diagnostic_category: diagnosticCategoryForError(options.error),
15653
+ os_family: platform(),
15654
+ arch: arch(),
15655
+ node_major: Number(process.versions.node.split(".")[0] ?? 0)
15656
+ }
15657
+ });
15658
+ if (state.status === "debug") {
15659
+ options.debugSink?.capture("debug", reliability);
15660
+ return;
15661
+ }
15662
+ await (options.dispatcher ?? createTelemetryDispatcher({ stateDir: context.stateDir })).capture(state, reliability);
15663
+ }
15664
+ function readUserConfigObject(path) {
15665
+ if (!existsSync(path)) return {};
15666
+ try {
15667
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
15668
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
15669
+ } catch (error) {
15670
+ throw new CapletsError("CONFIG_INVALID", `Caplets config at ${path} is not valid JSON`, error);
15671
+ }
15672
+ }
15673
+ function runtimeModeForEnv(env) {
15674
+ const mode = env.CAPLETS_MODE;
15675
+ return mode === "remote" || mode === "cloud" || mode === "local" ? mode : "unknown";
15676
+ }
15677
+ function errorCodeForTelemetry(error) {
15678
+ if (error instanceof CapletsError) return error.code;
15679
+ return "UNKNOWN";
15680
+ }
15681
+ function diagnosticCategoryForError(error) {
15682
+ if (!(error instanceof CapletsError)) return "unknown";
15683
+ if (error.code.startsWith("CONFIG")) return "config";
15684
+ if (error.code.startsWith("AUTH")) return "auth";
15685
+ if (error.code.includes("NETWORK") || error.code.includes("UNAVAILABLE")) return "network";
15686
+ if (error.code.includes("VALID") || error.code.includes("REQUEST")) return "validation";
15687
+ return "runtime";
15688
+ }
15689
+ function writeTelemetryConfig(path, enabled) {
15690
+ const config = {
15691
+ ...readUserConfigObject(path),
15692
+ $schema: "https://caplets.dev/config.schema.json",
15693
+ telemetry: enabled
15694
+ };
15695
+ mkdirSync(dirname(path), { recursive: true });
15696
+ writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`, { mode: 384 });
15697
+ }
15698
+ function formatTelemetryStatus(context) {
15699
+ const config = telemetryConfigForCli(context);
15700
+ const state = resolveTelemetryState({
15701
+ config,
15702
+ env: context.env,
15703
+ stateDir: context.stateDir,
15704
+ surface: "cli",
15705
+ visibility: "visible",
15706
+ createIdentity: false
15707
+ });
15708
+ const notice = readTelemetryNotice({ stateDir: context.stateDir });
15709
+ const identity = readTelemetryIdentity({
15710
+ stateDir: context.stateDir,
15711
+ create: false
15712
+ });
15713
+ const health = readTelemetryDeliveryHealth({ stateDir: context.stateDir });
15714
+ return `${[
15715
+ `Telemetry: ${state.status}`,
15716
+ `Decision: ${state.decider}`,
15717
+ `Config: ${config.telemetry === false ? "disabled" : "enabled"}`,
15718
+ `Environment: ${context.env.CAPLETS_DISABLE_TELEMETRY === "1" ? "disabled" : "enabled"}`,
15719
+ `Notice shown: ${notice.shown ? `yes (${notice.surface})` : "no"}`,
15720
+ `Anonymous ID: ${identity.kind === "stable" ? "present" : "not stored"}`,
15721
+ `Delivery health: ${Object.keys(health).length === 0 ? "none" : JSON.stringify(health)}`,
15722
+ "Disable with CAPLETS_DISABLE_TELEMETRY=1 or `caplets telemetry disable`."
15723
+ ].join("\n")}\n`;
15724
+ }
15373
15725
  function remoteProfileStore(authDir, env) {
15374
15726
  return createRemoteProfileStore({
15375
15727
  authDir,
@@ -15380,6 +15732,21 @@ function attachRemoteUrlFromArgs(positionalUrl, legacyRemoteUrl) {
15380
15732
  if (positionalUrl && legacyRemoteUrl && positionalUrl !== legacyRemoteUrl) throw new CapletsError("REQUEST_INVALID", "Pass either attach URL or --remote-url, not both. Use caplets attach <url> for new configs.");
15381
15733
  return positionalUrl ?? legacyRemoteUrl;
15382
15734
  }
15735
+ function rejectAttachHttpServeFlags(options) {
15736
+ const invalid = [
15737
+ options.transport !== void 0 ? "--transport" : void 0,
15738
+ options.host !== void 0 ? "--host" : void 0,
15739
+ options.port !== void 0 ? "--port" : void 0,
15740
+ options.path !== void 0 ? "--path" : void 0,
15741
+ options.allowUnauthenticatedHttp === true ? "--allow-unauthenticated-http" : void 0,
15742
+ options.trustProxy === true ? "--trust-proxy" : void 0
15743
+ ].filter((value) => value !== void 0);
15744
+ if (invalid.length === 0) return;
15745
+ throw new CapletsError("REQUEST_INVALID", `caplets attach is stdio-only; ${invalid.join(", ")} ${invalid.length === 1 ? "is" : "are"} no longer supported. Use caplets serve --transport http --upstream-url <url> to start an HTTP stacked runtime.`);
15746
+ }
15747
+ function hiddenOption(flags, description) {
15748
+ return new Option(flags, description).hideHelp();
15749
+ }
15383
15750
  function remoteServerCredentialStore(statePath, env) {
15384
15751
  return new RemoteServerCredentialStore({ dir: statePath ?? env.CAPLETS_REMOTE_SERVER_STATE_DIR ?? join(DEFAULT_AUTH_DIR, "remote-server") });
15385
15752
  }
@@ -15801,9 +16168,33 @@ function createProgram(io = {}) {
15801
16168
  const writeErr = io.writeErr ?? ((value) => process.stderr.write(value));
15802
16169
  const env = io.env ?? process.env;
15803
16170
  const currentConfigPath = () => envConfigPath(env);
16171
+ const telemetryContext = () => ({
16172
+ env,
16173
+ configPath: currentConfigPath(),
16174
+ projectConfigPath: envProjectConfigPath(env),
16175
+ stateDir: io.telemetryStateDir ?? defaultTelemetryStateDir(env),
16176
+ stderrIsTTY: io.stderrIsTTY ?? process.stderr.isTTY === true,
16177
+ writeErr
16178
+ });
16179
+ const printTelemetryNotice = (surface) => {
16180
+ try {
16181
+ maybePrintCliTelemetryNotice(telemetryContext(), surface);
16182
+ } catch {}
16183
+ };
15804
16184
  const setExitCode = io.setExitCode ?? ((code) => {
15805
16185
  process.exitCode = code;
15806
16186
  });
16187
+ const executeOperationIo = (format) => ({
16188
+ writeOut,
16189
+ writeErr,
16190
+ setExitCode,
16191
+ authDir: io.authDir,
16192
+ env,
16193
+ remote: remoteClientForCli(io),
16194
+ format,
16195
+ telemetryStateDir: telemetryContext().stateDir,
16196
+ telemetryDebugSink: io.telemetryDebugSink
16197
+ });
15807
16198
  const program = new Command();
15808
16199
  program.name("caplets").description("Progressive-disclosure gateway for MCP servers.").version(io.version ?? version).exitOverride().configureOutput({
15809
16200
  writeOut,
@@ -15847,7 +16238,51 @@ function createProgram(io = {}) {
15847
16238
  }
15848
16239
  if (suggestions.length > 0) writeOut(`${suggestions.join("\n")}\n`);
15849
16240
  });
16241
+ const telemetry = program.command(cliCommands$1.telemetry).description("Inspect and control anonymous Caplets telemetry.");
16242
+ telemetry.command("status").description("Show anonymous telemetry status.").action(() => {
16243
+ writeOut(formatTelemetryStatus(telemetryContext()));
16244
+ });
16245
+ telemetry.command("enable").description("Enable anonymous telemetry in the user config.").action(() => {
16246
+ const path = resolveConfigPath(currentConfigPath());
16247
+ writeTelemetryConfig(path, true);
16248
+ writeOut(`Enabled anonymous telemetry in ${path}.\n`);
16249
+ if (env.CAPLETS_DISABLE_TELEMETRY === "1") writeOut("CAPLETS_DISABLE_TELEMETRY=1 still disables telemetry for this process.\n");
16250
+ });
16251
+ telemetry.command("disable").description("Disable anonymous telemetry in the user config.").action(() => {
16252
+ const path = resolveConfigPath(currentConfigPath());
16253
+ writeTelemetryConfig(path, false);
16254
+ writeOut(`Disabled anonymous telemetry in ${path}.\n`);
16255
+ });
16256
+ telemetry.command("delete-id").description("Delete the local anonymous telemetry ID.").action(() => {
16257
+ deleteTelemetryIdentity({ stateDir: telemetryContext().stateDir });
16258
+ writeOut("Deleted the local anonymous telemetry ID. This does not delete provider-side historical anonymous events; provider retention controls historical data.\n");
16259
+ });
16260
+ telemetry.command("rotate-id").description("Rotate the local anonymous telemetry ID.").action(() => {
16261
+ rotateTelemetryIdentity({ stateDir: telemetryContext().stateDir });
16262
+ writeOut("Rotated the local anonymous telemetry ID. This does not delete provider-side historical anonymous events; provider retention controls historical data.\n");
16263
+ });
16264
+ telemetry.command("debug").description("Run a Caplets command with local telemetry debug output.").allowUnknownOption(true).argument("[args...]", "Caplets command and arguments after --").action(async (args) => {
16265
+ const nestedArgs = args[0] === "--" ? args.slice(1) : args;
16266
+ const sink = new TelemetryDebugSink();
16267
+ try {
16268
+ if (nestedArgs.length > 0) await runCli(nestedArgs, {
16269
+ ...io,
16270
+ env: {
16271
+ ...env,
16272
+ CAPLETS_TELEMETRY_DEBUG: "1"
16273
+ },
16274
+ telemetryDebugSink: sink,
16275
+ writeOut,
16276
+ writeErr
16277
+ });
16278
+ } finally {
16279
+ writeOut(`${JSON.stringify({ telemetryDebug: sink.toJSON() }, null, 2)}\n`);
16280
+ }
16281
+ });
15850
16282
  program.command(cliCommands$1.codeMode).description("Run, inspect, and debug Caplets Code Mode.").argument("[code]", "inline TypeScript code to run").option("--file <path>", "read TypeScript code from a file relative to the current directory").option("--session-id <id>", "optional Code Mode session identifier").option("--recover <ref>", "recover a prior Code Mode REPL session when supported").option("--timeout-ms <ms>", "execution timeout in milliseconds", parsePositiveInteger).option("--json", "print the structured run envelope").action(async (code, options) => {
16283
+ try {
16284
+ maybePrintCliTelemetryNotice(telemetryContext(), "code_mode");
16285
+ } catch {}
15851
16286
  if (code === "repl" && options.file === void 0) {
15852
16287
  await runCodeModeReplCli({
15853
16288
  env,
@@ -15881,6 +16316,7 @@ function createProgram(io = {}) {
15881
16316
  ...currentConfigPath() ? { configPath: currentConfigPath() } : {},
15882
16317
  projectConfigPath: envProjectConfigPath(env),
15883
16318
  ...io.authDir ? { authDir: io.authDir } : {},
16319
+ telemetryStateDir: telemetryContext().stateDir,
15884
16320
  ...code === void 0 ? {} : { inlineCode: code },
15885
16321
  ...options.file === void 0 ? {} : { file: options.file },
15886
16322
  ...options.sessionId === void 0 ? {} : { sessionId: options.sessionId },
@@ -15897,16 +16333,23 @@ function createProgram(io = {}) {
15897
16333
  ...currentConfigPath() ? { configPath: currentConfigPath() } : {},
15898
16334
  projectConfigPath: envProjectConfigPath(env),
15899
16335
  ...io.authDir ? { authDir: io.authDir } : {},
16336
+ telemetryStateDir: telemetryContext().stateDir,
15900
16337
  ...options.json === void 0 && parentOptions.json === void 0 ? {} : { json: options.json ?? parentOptions.json },
15901
16338
  writeOut
15902
16339
  });
15903
16340
  });
15904
- const serve = program.command(cliCommands$1.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("--remote-state-path <path>", "server-owned remote credential state directory").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) => {
16341
+ const serve = program.command(cliCommands$1.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("--remote-state-path <path>", "server-owned remote credential state directory").option("--upstream-url <url>", "upstream Caplets runtime URL to compose with this HTTP service").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) => {
16342
+ printTelemetryNotice("serve");
15905
16343
  const resolved = resolveServeOptions(options);
15906
16344
  const configPath = currentConfigPath();
15907
16345
  await (io.serve ?? ((serveOptions) => serveResolvedCaplets(serveOptions, {
15908
16346
  ...configPath ? { configPath } : {},
15909
- ...io.authDir ? { authDir: io.authDir } : {}
16347
+ ...io.authDir ? { authDir: io.authDir } : {},
16348
+ telemetryEnv: env,
16349
+ telemetryStateDir: io.telemetryStateDir ?? defaultTelemetryStateDir(env),
16350
+ telemetrySurface: "serve",
16351
+ telemetryVisibility: "visible",
16352
+ telemetryRuntimeMode: runtimeModeForEnv(env)
15910
16353
  }, writeErr)))(resolved);
15911
16354
  });
15912
16355
  const daemonOptions = () => ({
@@ -15923,6 +16366,7 @@ function createProgram(io = {}) {
15923
16366
  })) addServeMigrationCommand(serve, name, replacement);
15924
16367
  const daemon = program.command(cliCommands$1.daemon).description("Install and manage the default Caplets daemon.");
15925
16368
  addDaemonInstallOptions(daemon.command("install").description("Install or update the default Caplets daemon.")).action(async (options) => {
16369
+ printTelemetryNotice("daemon");
15926
16370
  const prompt = options.json ? void 0 : createSetupPromptHandle(io, writeOut);
15927
16371
  try {
15928
16372
  const result = await installDaemon(daemonInstallOptions(options), {
@@ -15946,6 +16390,7 @@ function createProgram(io = {}) {
15946
16390
  }
15947
16391
  });
15948
16392
  addJsonOption(daemon.command("uninstall").description("Uninstall the default Caplets daemon.").option("--purge", "remove daemon config, state, and logs").option("--dry-run", "preview uninstall actions without mutation")).action(async (options) => {
16393
+ printTelemetryNotice("daemon");
15949
16394
  const result = await uninstallDaemon({
15950
16395
  purge: options.purge === true,
15951
16396
  dryRun: options.dryRun === true
@@ -15957,6 +16402,7 @@ function createProgram(io = {}) {
15957
16402
  writeOut(result.dryRun ? "Would uninstall Caplets daemon.\n" : "Uninstalled Caplets daemon.\n");
15958
16403
  });
15959
16404
  addJsonOption(daemon.command("start").description("Start the default Caplets daemon.")).action(async (options) => {
16405
+ printTelemetryNotice("daemon");
15960
16406
  const result = await startDaemon(daemonOptions());
15961
16407
  if (options.json) {
15962
16408
  writeOut(`${JSON.stringify(result, null, 2)}\n`);
@@ -15965,6 +16411,7 @@ function createProgram(io = {}) {
15965
16411
  writeOut(result.action === "restart" ? "Restarted Caplets daemon.\n" : "Started Caplets daemon.\n");
15966
16412
  });
15967
16413
  addJsonOption(daemon.command("restart").description("Restart the default Caplets daemon.")).action(async (options) => {
16414
+ printTelemetryNotice("daemon");
15968
16415
  const result = await restartDaemon(daemonOptions());
15969
16416
  if (options.json) {
15970
16417
  writeOut(`${JSON.stringify(result, null, 2)}\n`);
@@ -15973,6 +16420,7 @@ function createProgram(io = {}) {
15973
16420
  writeOut("Restarted Caplets daemon.\n");
15974
16421
  });
15975
16422
  addJsonOption(daemon.command("stop").description("Stop the default Caplets daemon.")).action(async (options) => {
16423
+ printTelemetryNotice("daemon");
15976
16424
  const result = await stopDaemon(daemonOptions());
15977
16425
  if (options.json) {
15978
16426
  writeOut(`${JSON.stringify(result, null, 2)}\n`);
@@ -15981,6 +16429,7 @@ function createProgram(io = {}) {
15981
16429
  writeOut("Stopped Caplets daemon.\n");
15982
16430
  });
15983
16431
  addJsonOption(daemon.command("status").description("Show the default Caplets daemon status.")).action(async (options) => {
16432
+ printTelemetryNotice("daemon");
15984
16433
  const status = await daemonStatus(daemonOptions());
15985
16434
  if (options.json) {
15986
16435
  writeOut(`${JSON.stringify(status, null, 2)}\n`);
@@ -15989,6 +16438,7 @@ function createProgram(io = {}) {
15989
16438
  writeOut(formatDaemonStatus(status));
15990
16439
  });
15991
16440
  addJsonOption(daemon.command("logs").description("Show Caplets daemon logs.").option("--follow", "follow appended log lines").option("--tail <lines>", "show the last number of lines").option("--stream <stream>", "log stream: stdout, stderr, or all")).action(async (options) => {
16441
+ printTelemetryNotice("daemon");
15992
16442
  const tail = options.tail === void 0 ? 10 : parseNonNegativeInteger(options.tail, "--tail");
15993
16443
  const stream = parseLogStream(options.stream);
15994
16444
  if (options.follow) {
@@ -16023,8 +16473,10 @@ function createProgram(io = {}) {
16023
16473
  }
16024
16474
  for (const entry of result.entries) writeOut(stream === "all" ? `[${entry.stream}] ${entry.line}\n` : `${entry.line}\n`);
16025
16475
  });
16026
- program.command(cliCommands$1.attach).description("Start a remote-backed Caplets MCP server.").argument("[url]", "remote Caplets service base URL").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").addOption(new Option("--remote-url <url>", "legacy alias for the remote Caplets service base URL").hideHelp()).option("--workspace <workspace>", "hosted Cloud workspace ID or slug").option("--allow-unauthenticated-http", "allow unauthenticated HTTP serving on non-loopback hosts").option("--trust-proxy", "trust X-Forwarded-* headers from a reverse proxy").option("--json", "print JSON status events").option("--verbose", "print detailed attach diagnostics").option("--once", "validate Project Binding once and exit").option("--project-root <path>", "test-only project root override").action(async (url, options) => {
16476
+ program.command(cliCommands$1.attach).description("Start a remote-backed Caplets MCP server.").argument("[url]", "remote Caplets service base URL").addOption(hiddenOption("--transport <transport>", "server transport: stdio or http")).addOption(hiddenOption("--host <host>", "HTTP bind host")).addOption(hiddenOption("--port <port>", "HTTP bind port")).addOption(hiddenOption("--path <path>", "HTTP service base path")).addOption(new Option("--remote-url <url>", "legacy alias for the remote Caplets service base URL").hideHelp()).option("--workspace <workspace>", "hosted Cloud workspace ID or slug").addOption(hiddenOption("--allow-unauthenticated-http", "allow unauthenticated HTTP serving on non-loopback hosts")).addOption(hiddenOption("--trust-proxy", "trust X-Forwarded-* headers from a reverse proxy")).option("--json", "print JSON status events").option("--verbose", "print detailed attach diagnostics").option("--once", "validate Project Binding once and exit").option("--project-root <path>", "test-only project root override").action(async (url, options) => {
16477
+ printTelemetryNotice("attach");
16027
16478
  try {
16479
+ rejectAttachHttpServeFlags(options);
16028
16480
  const remoteUrl = attachRemoteUrlFromArgs(url, options.remoteUrl);
16029
16481
  const attachOptions = {
16030
16482
  ...options,
@@ -16396,6 +16848,7 @@ function createProgram(io = {}) {
16396
16848
  writeOut(`Created ${localMutationTargetLabel(target, io)}Caplets config at ${path}\n`);
16397
16849
  });
16398
16850
  program.command(cliCommands$1.setup).description("Install or configure an agent integration for Caplets.").argument("[integration]", "integration: codex, claude-code, opencode, pi, or mcp-client").option("--remote", "configure for a remote Caplets server").option("--remote-url <url>", "remote Caplets service base URL").option("--server-url <url>", "remote Caplets service base URL").option("--output <path>", "config path to write for generic MCP setup").option("--dry-run", "print actions without running commands or writing files").option("--yes", "approve Caplet setup commands for the exact current content hash").option("--target <target>", "Caplet setup target: local, remote, or cloud", parseSetupTarget).option("--format <format>", "output format: plain or json", parseSetupFormat).action(async (integration, options) => {
16851
+ printTelemetryNotice("cli");
16399
16852
  const setupOptions = {
16400
16853
  ...options,
16401
16854
  env
@@ -16584,6 +17037,7 @@ function createProgram(io = {}) {
16584
17037
  writeOut(formatCapletList(rows, options.format ?? "plain"));
16585
17038
  });
16586
17039
  program.command(cliCommands$1.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("--project", "install to the project Caplets root").option("-g, --global", "install to the user Caplets root").option("--remote", "install through remote control").option("--force", "overwrite installed Caplets").action(async (repo, capletIds, options) => {
17040
+ printTelemetryNotice("cli");
16587
17041
  const target = parseMutationTarget(options);
16588
17042
  if (target === "remote") {
16589
17043
  const result = await requireRemoteClientForTarget(io).request("install", {
@@ -16703,37 +17157,13 @@ function createProgram(io = {}) {
16703
17157
  writeAddResult(writeOut, `${localMutationTargetLabel(target, io)}HTTP`, result);
16704
17158
  });
16705
17159
  program.command(cliCommands$1.inspect).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) => {
16706
- await executeOperation(caplet, { operation: "inspect" }, {
16707
- writeOut,
16708
- writeErr,
16709
- setExitCode,
16710
- authDir: io.authDir,
16711
- env,
16712
- remote: remoteClientForCli(io),
16713
- format: options.format
16714
- });
17160
+ await executeOperation(caplet, { operation: "inspect" }, executeOperationIo(options.format));
16715
17161
  });
16716
17162
  program.command(cliCommands$1.checkBackend).description("Check backend availability for a configured Caplet.").argument("<caplet>", "configured Caplet ID").option("--format <format>", "output format: markdown, md, plain, or json", parseOutputFormat).action(async (caplet, options) => {
16717
- await executeOperation(caplet, { operation: "check" }, {
16718
- writeOut,
16719
- writeErr,
16720
- setExitCode,
16721
- authDir: io.authDir,
16722
- env,
16723
- remote: remoteClientForCli(io),
16724
- format: options.format
16725
- });
17163
+ await executeOperation(caplet, { operation: "check" }, executeOperationIo(options.format));
16726
17164
  });
16727
17165
  program.command(cliCommands$1.listTools).description("List downstream tools for a configured Caplet.").argument("<caplet>", "configured Caplet ID").option("--format <format>", "output format: markdown, md, plain, or json", parseOutputFormat).action(async (caplet, options) => {
16728
- await executeOperation(caplet, { operation: "tools" }, {
16729
- writeOut,
16730
- writeErr,
16731
- setExitCode,
16732
- authDir: io.authDir,
16733
- env,
16734
- remote: remoteClientForCli(io),
16735
- format: options.format
16736
- });
17166
+ await executeOperation(caplet, { operation: "tools" }, executeOperationIo(options.format));
16737
17167
  });
16738
17168
  program.command(cliCommands$1.searchTools).description("Search downstream tools for a configured Caplet.").argument("<caplet>", "configured Caplet ID").argument("<query>", "search query").option("--limit <n>", "maximum number of tools to return", parsePositiveInteger).option("--format <format>", "output format: markdown, md, plain, or json", parseOutputFormat).action(async (caplet, query, options) => {
16739
17169
  await executeOperation(caplet, options.limit === void 0 ? {
@@ -16743,30 +17173,14 @@ function createProgram(io = {}) {
16743
17173
  operation: "search_tools",
16744
17174
  query,
16745
17175
  limit: options.limit
16746
- }, {
16747
- writeOut,
16748
- writeErr,
16749
- setExitCode,
16750
- authDir: io.authDir,
16751
- env,
16752
- remote: remoteClientForCli(io),
16753
- format: options.format
16754
- });
17176
+ }, executeOperationIo(options.format));
16755
17177
  });
16756
17178
  program.command(cliCommands$1.getTool).description("Print one downstream tool schema.").argument("<caplet-or-target>", "Caplet ID or qualified <caplet.tool> target").argument("[tool]", "downstream tool name when caplet is provided separately").option("--format <format>", "output format: markdown, md, plain, or json", parseOutputFormat).action(async (capletOrTarget, toolArgument, options) => {
16757
17179
  const { caplet, tool } = parseQualifiedTarget(capletOrTarget, toolArgument);
16758
17180
  await executeOperation(caplet, {
16759
17181
  operation: "describe_tool",
16760
17182
  name: tool
16761
- }, {
16762
- writeOut,
16763
- writeErr,
16764
- setExitCode,
16765
- authDir: io.authDir,
16766
- env,
16767
- remote: remoteClientForCli(io),
16768
- format: options.format
16769
- });
17183
+ }, executeOperationIo(options.format));
16770
17184
  });
16771
17185
  program.command(cliCommands$1.callTool).description("Call one downstream tool.").argument("<caplet-or-target>", "Caplet ID or qualified <caplet.tool> target").argument("[tool]", "downstream tool name when caplet is provided separately").option("--args <json-object>", "JSON object of downstream tool arguments").option("--field <path>", "project a field from structured output", collect, []).option("--format <format>", "output format: markdown, md, plain, or json", parseOutputFormat).action(async (capletOrTarget, toolArgument, options) => {
16772
17186
  const { caplet, tool } = parseQualifiedTarget(capletOrTarget, toolArgument);
@@ -16775,28 +17189,12 @@ function createProgram(io = {}) {
16775
17189
  name: tool,
16776
17190
  args: parseCallToolArgs(options.args),
16777
17191
  ...options.field && options.field.length > 0 ? { fields: options.field } : {}
16778
- }, {
16779
- writeOut,
16780
- writeErr,
16781
- setExitCode,
16782
- authDir: io.authDir,
16783
- env,
16784
- remote: remoteClientForCli(io),
16785
- format: options.format
16786
- });
17192
+ }, executeOperationIo(options.format));
16787
17193
  });
16788
17194
  program.command(cliCommands$1.listResources).description("List MCP resources for a configured MCP Caplet.").argument("<caplet>").option("--limit <n>", "maximum number of resources to return", parsePositiveInteger).option("--format <format>", "output format: markdown, md, plain, or json", parseOutputFormat).action(async (caplet, options) => executeOperation(caplet, options.limit === void 0 ? { operation: "resources" } : {
16789
17195
  operation: "resources",
16790
17196
  limit: options.limit
16791
- }, {
16792
- writeOut,
16793
- writeErr,
16794
- setExitCode,
16795
- authDir: io.authDir,
16796
- env,
16797
- remote: remoteClientForCli(io),
16798
- format: options.format
16799
- }));
17197
+ }, executeOperationIo(options.format)));
16800
17198
  program.command(cliCommands$1.searchResources).description("Search MCP resources and resource templates for a configured MCP Caplet.").argument("<caplet>").argument("<query>").option("--limit <n>", "maximum number of matches to return", parsePositiveInteger).option("--format <format>", "output format: markdown, md, plain, or json", parseOutputFormat).action(async (caplet, query, options) => executeOperation(caplet, options.limit === void 0 ? {
16801
17199
  operation: "search_resources",
16802
17200
  query
@@ -16804,51 +17202,19 @@ function createProgram(io = {}) {
16804
17202
  operation: "search_resources",
16805
17203
  query,
16806
17204
  limit: options.limit
16807
- }, {
16808
- writeOut,
16809
- writeErr,
16810
- setExitCode,
16811
- authDir: io.authDir,
16812
- env,
16813
- remote: remoteClientForCli(io),
16814
- format: options.format
16815
- }));
17205
+ }, executeOperationIo(options.format)));
16816
17206
  program.command(cliCommands$1.listResourceTemplates).description("List MCP resource templates for a configured MCP Caplet.").argument("<caplet>").option("--limit <n>", "maximum number of templates to return", parsePositiveInteger).option("--format <format>", "output format: markdown, md, plain, or json", parseOutputFormat).action(async (caplet, options) => executeOperation(caplet, options.limit === void 0 ? { operation: "resource_templates" } : {
16817
17207
  operation: "resource_templates",
16818
17208
  limit: options.limit
16819
- }, {
16820
- writeOut,
16821
- writeErr,
16822
- setExitCode,
16823
- authDir: io.authDir,
16824
- env,
16825
- remote: remoteClientForCli(io),
16826
- format: options.format
16827
- }));
17209
+ }, executeOperationIo(options.format)));
16828
17210
  program.command(cliCommands$1.readResource).description("Read one MCP resource by URI.").argument("<caplet>").argument("<uri>").option("--format <format>", "output format: markdown, md, plain, or json", parseOutputFormat).action(async (caplet, uri, options) => executeOperation(caplet, {
16829
17211
  operation: "read_resource",
16830
17212
  uri
16831
- }, {
16832
- writeOut,
16833
- writeErr,
16834
- setExitCode,
16835
- authDir: io.authDir,
16836
- env,
16837
- remote: remoteClientForCli(io),
16838
- format: options.format
16839
- }));
17213
+ }, executeOperationIo(options.format)));
16840
17214
  program.command(cliCommands$1.listPrompts).description("List MCP prompts for a configured MCP Caplet.").argument("<caplet>").option("--limit <n>", "maximum number of prompts to return", parsePositiveInteger).option("--format <format>", "output format: markdown, md, plain, or json", parseOutputFormat).action(async (caplet, options) => executeOperation(caplet, options.limit === void 0 ? { operation: "prompts" } : {
16841
17215
  operation: "prompts",
16842
17216
  limit: options.limit
16843
- }, {
16844
- writeOut,
16845
- writeErr,
16846
- setExitCode,
16847
- authDir: io.authDir,
16848
- env,
16849
- remote: remoteClientForCli(io),
16850
- format: options.format
16851
- }));
17217
+ }, executeOperationIo(options.format)));
16852
17218
  program.command(cliCommands$1.searchPrompts).description("Search MCP prompts for a configured MCP Caplet.").argument("<caplet>").argument("<query>").option("--limit <n>", "maximum number of prompts to return", parsePositiveInteger).option("--format <format>", "output format: markdown, md, plain, or json", parseOutputFormat).action(async (caplet, query, options) => executeOperation(caplet, options.limit === void 0 ? {
16853
17219
  operation: "search_prompts",
16854
17220
  query
@@ -16856,30 +17222,14 @@ function createProgram(io = {}) {
16856
17222
  operation: "search_prompts",
16857
17223
  query,
16858
17224
  limit: options.limit
16859
- }, {
16860
- writeOut,
16861
- writeErr,
16862
- setExitCode,
16863
- authDir: io.authDir,
16864
- env,
16865
- remote: remoteClientForCli(io),
16866
- format: options.format
16867
- }));
17225
+ }, executeOperationIo(options.format)));
16868
17226
  program.command(cliCommands$1.getPrompt).description("Get one MCP prompt by name.").argument("<caplet-or-target>", "MCP Caplet ID or qualified <caplet.prompt> target").argument("[prompt]", "prompt name when caplet is provided separately").option("--args <json-object>", "JSON object of prompt arguments").option("--format <format>", "output format: markdown, md, plain, or json", parseOutputFormat).action(async (capletOrTarget, promptArgument, options) => {
16869
17227
  const { caplet, tool: prompt } = parseQualifiedTarget(capletOrTarget, promptArgument);
16870
17228
  await executeOperation(caplet, {
16871
17229
  operation: "get_prompt",
16872
17230
  name: prompt,
16873
17231
  args: parseJsonObjectOption(options.args, "get-prompt --args")
16874
- }, {
16875
- writeOut,
16876
- writeErr,
16877
- setExitCode,
16878
- authDir: io.authDir,
16879
- env,
16880
- remote: remoteClientForCli(io),
16881
- format: options.format
16882
- });
17232
+ }, executeOperationIo(options.format));
16883
17233
  });
16884
17234
  program.command(cliCommands$1.complete).description("Complete an MCP prompt or resource-template argument.").argument("<caplet>").requiredOption("--argument <name>", "argument name").option("--value <value>", "argument prefix", "").option("--prompt <name>", "prompt name to complete").option("--resource-template <uri-template>", "resource template URI to complete").option("--format <format>", "output format: markdown, md, plain, or json", parseOutputFormat).action(async (caplet, options) => executeOperation(caplet, {
16885
17235
  operation: "complete",
@@ -16888,15 +17238,7 @@ function createProgram(io = {}) {
16888
17238
  name: options.argument,
16889
17239
  value: options.value
16890
17240
  }
16891
- }, {
16892
- writeOut,
16893
- writeErr,
16894
- setExitCode,
16895
- authDir: io.authDir,
16896
- env,
16897
- remote: remoteClientForCli(io),
16898
- format: options.format
16899
- }));
17241
+ }, executeOperationIo(options.format)));
16900
17242
  const config = program.command(cliCommands$1.config).description("Inspect Caplets config locations.");
16901
17243
  config.command("path").description("Print the effective user config path.").action(() => {
16902
17244
  writeOut(`${resolveConfigPath(currentConfigPath())}\n`);
@@ -17487,8 +17829,17 @@ function mergeRemoteAndLocalRows(remoteRows, localOverlay, options) {
17487
17829
  });
17488
17830
  if (!localOverlay) return [...rows.values()].filter((row) => options.includeDisabled || !row.disabled).sort((left, right) => left.server.localeCompare(right.server));
17489
17831
  for (const row of listCaplets(localOverlay, { includeDisabled: true })) {
17490
- if (rows.get(row.server)) {
17832
+ const remote = rows.get(row.server);
17833
+ if (remote) {
17491
17834
  if (row.disabled) continue;
17835
+ if (remote.shadowing === "namespace") {
17836
+ options.writeErr(`Local Caplet '${row.server}' is exposed under a qualified ID because the remote Caplet uses namespace shadowing for that Caplet ID.\n`);
17837
+ continue;
17838
+ }
17839
+ if (remote.shadowing !== "allow") {
17840
+ options.writeErr(`Local Caplet '${row.server}' is suppressed because the remote Caplet forbids shadowing that Caplet ID.\n`);
17841
+ continue;
17842
+ }
17492
17843
  options.writeErr(`Warning: ${formatOverlaySource(row.source)} Caplet ${row.server} shadows remote Caplet\n`);
17493
17844
  }
17494
17845
  rows.set(row.server, row);
@@ -17512,6 +17863,12 @@ async function executeLocalOperation(caplet, request, io, config) {
17512
17863
  ...io.authDir ? { authDir: io.authDir } : {},
17513
17864
  watch: false,
17514
17865
  writeErr: io.writeErr,
17866
+ telemetryEnv: io.env ?? process.env,
17867
+ telemetryStateDir: io.telemetryStateDir ?? defaultTelemetryStateDir(io.env ?? process.env),
17868
+ telemetrySurface: "cli",
17869
+ telemetryVisibility: "visible",
17870
+ telemetryRuntimeMode: runtimeModeForEnv(io.env ?? process.env),
17871
+ telemetryDebugSink: io.telemetryDebugSink,
17515
17872
  ...config ? { configLoader: () => config } : {}
17516
17873
  });
17517
17874
  try {