@hue-run/sdk 0.11.3 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/mcp.js CHANGED
@@ -82,18 +82,21 @@ export const MCP_USAGE = `Usage: hue mcp install --client <claude-code|codex|con
82
82
  [--toolsets NAMES] [--url URL]
83
83
  [--scope project|user] [--dry-run] [--print]
84
84
 
85
- Configure a coding agent to use the Hue MCP server. A key configuration references the
86
- ${ENV_VAR} environment variable; a key value is never written. A sign-in configuration holds
87
- only the URL: the client opens Hue in a browser, where you approve access to the projects of
88
- one organization.
85
+ Configure a coding agent to use the Hue MCP server. A sign-in configuration holds only the
86
+ URL: the client opens Hue in a browser, where you approve access to the projects of one
87
+ organization. A key configuration references the ${ENV_VAR} environment variable; a key value
88
+ is never written.
89
89
 
90
90
  Options:
91
91
  --client NAME Coding agent to configure (required)
92
- --auth MODE key: reference ${ENV_VAR} (the default, except for conductor)
93
- oauth: URL only, sign in with Hue in the client (claude-code, codex and
94
- conductor; the default for conductor)
95
- --read-only key only: add ?read_only=true to the URL so write tools are hidden. A
96
- sign-in connection has Read and write access; use a Read key for read-only
92
+ --auth MODE oauth: URL only, sign in with Hue in the client (claude-code, codex and
93
+ conductor; their default)
94
+ key: reference ${ENV_VAR} (the default for the other clients and with
95
+ --read-only). Without --auth, claude-code and codex keep the choice of
96
+ an existing entry, and use a key for a new one when ${ENV_VAR} is set
97
+ --read-only Add ?read_only=true to a key configuration's URL so write tools are
98
+ hidden; selects a key without --auth. A sign-in connection has Read and
99
+ write access; use a Read key for read-only
97
100
  --project VALUE Pin the connection to one project by id or slug. Its tools omit project_id,
98
101
  and the server is named hue-<value> so it can coexist with an unpinned hue
99
102
  --toolsets NAMES
@@ -114,7 +117,9 @@ Options:
114
117
  Files: claude-code .mcp.json, cursor .cursor/mcp.json, vscode .vscode/mcp.json (relative to the
115
118
  current directory). codex and gemini use their own CLI when it is on PATH; windsurf prints the
116
119
  snippet for its user configuration file. conductor registers the server for its Claude Code
117
- (user scope) and Codex agents with their CLIs, printing the command for a CLI not on PATH.`;
120
+ (user scope) and Codex agents with their CLIs, printing the command for a CLI not on PATH.
121
+ codex mcp add signs in at once and waits up to 5 minutes for the browser, so without --auth
122
+ and outside a terminal the command prints it instead of running it.`;
118
123
  const json = (value) => `${JSON.stringify(value, null, 2)}\n`;
119
124
  /**
120
125
  * A URL as a POSIX shell word for printed commands: `?` is a zsh glob and `&` ends a command, so a
@@ -281,6 +286,10 @@ export function readOnlyMcpUrl(url) {
281
286
  parsed.searchParams.set("read_only", "true");
282
287
  return parsed.href;
283
288
  }
289
+ /** Whether Hue treats the URL as read-only: it honors only `read_only=true` or `read_only=1`. */
290
+ function isReadOnlyUrl(url) {
291
+ return ["true", "1"].includes(new URL(url).searchParams.get("read_only") ?? "");
292
+ }
284
293
  class ConfigError extends Error {
285
294
  constructor(message) {
286
295
  super(message);
@@ -501,9 +510,9 @@ function redactUrlPath(value) {
501
510
  const PARAMETER_NAME = /^[A-Za-z_][\w.-]{0,31}$/u;
502
511
  /**
503
512
  * The URL this command writes, as its output shows it: the selections it validated (`toolsets`,
504
- * `project`, and `read_only` when true or false) are kept; any other query value, and a token-like
505
- * path segment, becomes `[redacted]`. `parseMcpUrl` has already refused userinfo and a fragment.
506
- * Commands printed for a person to run keep the URL as given.
513
+ * `project`, and `read_only` when true, false, 1 or 0) are kept; any other query value, and a
514
+ * token-like path segment, becomes `[redacted]`. `parseMcpUrl` has already refused userinfo and a
515
+ * fragment. Commands printed for a person to run keep the URL as given.
507
516
  */
508
517
  function displayMcpUrl(url) {
509
518
  const parsed = new URL(url);
@@ -515,7 +524,7 @@ function displayMcpUrl(url) {
515
524
  const [name = "", value = ""] = pair.split("=", 2);
516
525
  const selection = name === "toolsets" ||
517
526
  name === "project" ||
518
- (name === "read_only" && /^(?:true|false)$/u.test(value));
527
+ (name === "read_only" && /^(?:true|false|1|0)$/u.test(value));
519
528
  if (selection)
520
529
  return pair;
521
530
  return PARAMETER_NAME.test(name) &&
@@ -573,6 +582,8 @@ function redactConfig(root, serversKey, url, shownUrl) {
573
582
  };
574
583
  return redact(root, false, 0, false);
575
584
  }
585
+ /** Whether a person is likely watching: login.ts opens a browser on the same test. */
586
+ const isTerminal = (stream) => stream.isTTY === true;
576
587
  async function findExecutable(name, env) {
577
588
  const searchPath = env.PATH ?? env.Path ?? "";
578
589
  const extensions = process.platform === "win32" ? (env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";") : [""];
@@ -618,6 +629,102 @@ function runClientCli(executable, args, options) {
618
629
  }
619
630
  });
620
631
  }
632
+ /**
633
+ * A client CLI's exit code and output within ten seconds, none of it shown; null when it could not
634
+ * start or ran out of time.
635
+ */
636
+ function readClientCli(executable, args, options) {
637
+ return new Promise((resolveRun) => {
638
+ try {
639
+ const child = spawn(executable, args, { ...options, stdio: ["ignore", "pipe", "pipe"] });
640
+ const collect = () => {
641
+ const chunks = [];
642
+ let size = 0;
643
+ return {
644
+ add: (chunk) => {
645
+ size += chunk.length;
646
+ if (size <= MAX_CONFIG_BYTES)
647
+ chunks.push(chunk);
648
+ },
649
+ text: () => (size <= MAX_CONFIG_BYTES ? Buffer.concat(chunks).toString() : null),
650
+ };
651
+ };
652
+ const out = collect();
653
+ const err = collect();
654
+ let timedOut = false;
655
+ const timer = setTimeout(() => {
656
+ timedOut = true;
657
+ child.kill();
658
+ }, 10_000);
659
+ child.stdout.on("data", out.add);
660
+ child.stderr.on("data", err.add);
661
+ child.once("error", () => {
662
+ clearTimeout(timer);
663
+ resolveRun(null);
664
+ });
665
+ child.once("close", (code) => {
666
+ clearTimeout(timer);
667
+ const stdout = out.text();
668
+ const stderr = err.text();
669
+ resolveRun(timedOut || code === null || stdout === null || stderr === null
670
+ ? null
671
+ : { code, stdout, stderr });
672
+ });
673
+ }
674
+ catch {
675
+ resolveRun(null);
676
+ }
677
+ });
678
+ }
679
+ const hasAuthorization = (headers) => isRecord(headers) && Object.keys(headers).some((name) => name.toLowerCase() === "authorization");
680
+ /** The server's HTTP entry in `./.mcp.json`; an unreadable file is reported by the write. */
681
+ async function readClaudeProjectEntry(cwd, serverName) {
682
+ let config;
683
+ try {
684
+ ({ config } = await readJsonConfig(resolve(cwd, ".mcp.json"), ".mcp.json"));
685
+ }
686
+ catch {
687
+ return undefined;
688
+ }
689
+ const servers = isRecord(config) ? config.mcpServers : undefined;
690
+ const entry = isRecord(servers) ? servers[serverName] : undefined;
691
+ if (!isRecord(entry) || typeof entry.url !== "string" || !parseMcpUrl(entry.url))
692
+ return undefined;
693
+ const auth = hasAuthorization(entry.headers) ? "key" : "oauth";
694
+ return { url: entry.url, auth, source: ".mcp.json" };
695
+ }
696
+ /**
697
+ * The server's entry in Codex's configuration, as `codex mcp get --json` reports it: `found` with
698
+ * its exact output, `none` when Codex says it has no such server (or holds one that is not an HTTP
699
+ * Hue entry), or `unknown` when Codex could not be asked or answered otherwise, which is never
700
+ * taken for `none`.
701
+ */
702
+ async function readCodexEntry(executable, serverName, options) {
703
+ const result = await readClientCli(executable, ["mcp", "get", serverName, "--json"], options);
704
+ if (!result)
705
+ return { state: "unknown" };
706
+ if (result.code !== 0)
707
+ return result.stderr.includes(`No MCP server named '${serverName}' found`)
708
+ ? { state: "none" }
709
+ : { state: "unknown" };
710
+ let transport;
711
+ try {
712
+ transport = JSON.parse(result.stdout).transport;
713
+ }
714
+ catch {
715
+ return { state: "unknown" };
716
+ }
717
+ if (!isRecord(transport) || typeof transport.url !== "string" || !parseMcpUrl(transport.url))
718
+ return { state: "none" };
719
+ const key = Boolean(transport.bearer_token_env_var) ||
720
+ hasAuthorization(transport.http_headers) ||
721
+ hasAuthorization(transport.env_http_headers);
722
+ return {
723
+ state: "found",
724
+ entry: { url: transport.url, auth: key ? "key" : "oauth", source: "Codex's configuration" },
725
+ raw: result.stdout,
726
+ };
727
+ }
621
728
  /** Validates the MCP endpoint: HTTPS, or HTTP for loopback test servers; no credentials or hash. */
622
729
  export function parseMcpUrl(value) {
623
730
  let url;
@@ -732,20 +839,69 @@ function planFor(client, auth, scope, url, serverName) {
732
839
  }
733
840
  }
734
841
  }
735
- function nextSteps(client, auth, serverName, project) {
842
+ /**
843
+ * The authentication used without `--auth`, and the line saying why. A client that can sign in
844
+ * does, unless a key is asked for: read-only access (`--read-only`, or `read_only=true` in
845
+ * `--url`), which a sign-in connection does not have. For Claude Code and Codex, an existing entry
846
+ * keeps its choice, so rerunning the command, to change `--toolsets` for instance, never swaps a
847
+ * working setup; a key keeps its `read_only`. A new entry uses a key when `HUE_MCP_KEY` is set in
848
+ * `env`; only the variable's presence is checked. Conductor signed in by default before and keeps
849
+ * doing so: its agents read the login-shell environment Conductor captures, not this shell's. The
850
+ * other clients only take a key, so they get no line.
851
+ */
852
+ function defaultAuth(client, readOnly, existing, env) {
853
+ if (!OAUTH_CLIENTS.includes(client))
854
+ return { auth: "key" };
855
+ if (readOnly)
856
+ return {
857
+ auth: "key",
858
+ note: `Using a key: ${readOnly} needs one, since a sign-in connection has Read and write access.`,
859
+ };
860
+ if (existing?.auth === "key") {
861
+ const keepReadOnly = isReadOnlyUrl(existing.url);
862
+ return {
863
+ auth: "key",
864
+ keepReadOnly,
865
+ note: keepReadOnly
866
+ ? `Keeping a read-only key, as the existing entry in ${existing.source} has; pass --auth key to drop read_only, or --auth oauth to sign in with Hue instead.`
867
+ : `Keeping a key, as the existing entry in ${existing.source} has; pass --auth oauth to sign in with Hue instead.`,
868
+ };
869
+ }
870
+ if (existing)
871
+ return {
872
+ auth: "oauth",
873
+ note: `Keeping sign-in with Hue, as the existing entry in ${existing.source} has; pass --auth key to use a key instead.`,
874
+ };
875
+ if (client !== "conductor" && env[ENV_VAR])
876
+ return {
877
+ auth: "key",
878
+ note: `Using a key (${ENV_VAR} is set); pass --auth oauth to sign in with Hue instead.`,
879
+ };
880
+ return {
881
+ auth: "oauth",
882
+ note: `Using sign-in with Hue (${client === "conductor" ? "Conductor's default" : `${ENV_VAR} is not set`}); pass --auth key to use a key instead.`,
883
+ };
884
+ }
885
+ function nextSteps(client, auth, serverName, project, scope, readOnly) {
736
886
  const label = CLIENT_LABELS[client];
737
887
  const approve = project
738
888
  ? `Sign in to Hue and approve the connection. It acts only on the pinned project ${project}, within your role; for read-only access, use a Read project key with --auth key.`
739
- : "Sign in to Hue and approve the connection. It can read and write every active project in the organization you choose, within your role; for read-only access, use a Read project key with --auth key.";
889
+ : "Sign in to Hue and approve the connection. It can read and write every active project in the organization the consent page names, within your role; for read-only access, use a Read project key with --auth key.";
890
+ // A read-only configuration needs a key: plain hue login refuses a Read key.
891
+ const stored = readOnly
892
+ ? "hue login --keys coding-agent stores it in .env.hue and accepts a Read key"
893
+ : "hue login stores it in .env.hue";
740
894
  let lines;
741
895
  if (auth === "oauth")
742
896
  lines =
743
897
  client === "claude-code"
744
- ? [`In Claude Code, run /mcp, select ${serverName} and choose Authenticate. ${approve}`]
898
+ ? [
899
+ `Restart Claude Code (for example claude --continue)${scope === "project" ? ` and approve ${serverName} from .mcp.json` : ""}, then run /mcp, select ${serverName} and choose Authenticate. ${approve}`,
900
+ ]
745
901
  : client === "codex"
746
902
  ? [
747
903
  `If Codex did not open Hue in your browser, run: codex mcp login ${serverName}`,
748
- approve,
904
+ `${approve} Then start a new Codex session, which loads ${serverName}.`,
749
905
  ]
750
906
  : [
751
907
  `In Conductor, open MCP status from the plug icon or /mcp-status, refresh, and use ${serverName}'s authentication action. ${approve}`,
@@ -757,13 +913,13 @@ function nextSteps(client, auth, serverName, project) {
757
913
  ];
758
914
  else if (client === "conductor")
759
915
  lines = [
760
- `Conductor agents read ${ENV_VAR} from the login-shell environment Conductor captures, so export it there, not only in a terminal; hue login stores it in .env.hue. Sign-in (--auth oauth) needs no variable.`,
916
+ `Conductor agents read ${ENV_VAR} from the login-shell environment Conductor captures, so export it there, not only in a terminal; ${stored}.${readOnly ? "" : " Sign-in (--auth oauth) needs no variable."}`,
761
917
  ];
762
918
  else
763
919
  lines = [
764
- `Export ${ENV_VAR} in the shell that starts ${label}; hue login stores it in .env.hue:`,
920
+ `Export ${ENV_VAR} in the shell that starts ${label}; ${stored}:`,
765
921
  " set -a; . ./.env.hue; set +a",
766
- `An app started from the Dock or a launcher does not see that shell's variables; start ${label} from the shell${OAUTH_CLIENTS.includes(client) ? ", or sign in instead with --auth oauth" : ""}.`,
922
+ `An app started from the Dock or a launcher does not see that shell's variables; start ${label} from the shell${OAUTH_CLIENTS.includes(client) && !readOnly ? ", or sign in instead with --auth oauth" : ""}.`,
767
923
  ];
768
924
  // `codex mcp add` has no option for it, so a registered server gets the line by hand.
769
925
  if (client === "codex" || client === "conductor")
@@ -809,10 +965,10 @@ export async function runMcpCommand(argv, io = {}) {
809
965
  if (!client || !CLIENT_IDS.includes(client))
810
966
  return fail(`${client ? `Unknown client: ${client}.` : "--client is required."} Choose one of ${CLIENT_IDS.join(", ")}.\n\n${MCP_USAGE}`, 2);
811
967
  const clientId = client;
812
- const auth = parsed.values.auth ?? (clientId === "conductor" ? "oauth" : "key");
813
- if (auth !== "key" && auth !== "oauth")
968
+ const explicitAuth = parsed.values.auth;
969
+ if (explicitAuth !== undefined && explicitAuth !== "key" && explicitAuth !== "oauth")
814
970
  return fail(`--auth must be key or oauth.\n\n${MCP_USAGE}`, 2);
815
- if (auth === "oauth" && !OAUTH_CLIENTS.includes(clientId))
971
+ if (explicitAuth === "oauth" && !OAUTH_CLIENTS.includes(clientId))
816
972
  return fail(clientId === "cursor"
817
973
  ? "--auth oauth is not available for cursor: Hue does not yet accept Cursor's sign-in callback. Use a key (the default), or sign in from claude-code or codex."
818
974
  : `--auth oauth is available for ${OAUTH_CLIENTS.join(", ")}. Use a key (the default) for ${clientId}.`, 2);
@@ -824,12 +980,6 @@ export async function runMcpCommand(argv, io = {}) {
824
980
  let url = parseMcpUrl(parsed.values.url ?? DEFAULT_MCP_URL);
825
981
  if (!url)
826
982
  return fail("--url must be an HTTPS URL such as https://mcp.hue.run/mcp (plain HTTP is accepted for loopback test servers only).", 2);
827
- // A sign-in connection's access is chosen when it is approved; read_only is not part of it.
828
- if (auth === "oauth" &&
829
- (parsed.values["read-only"] || new URL(url).searchParams.has("read_only")))
830
- return fail("A sign-in connection has Read and write access and cannot be made read-only by its URL. For read-only access, use a Read project key (--auth key), or add --read-only to a key configuration.", 2);
831
- if (parsed.values["read-only"])
832
- url = readOnlyMcpUrl(url);
833
983
  // --project replaces a pin already in --url. Without it, validate and keep the URL's pin.
834
984
  const urlProjects = new URL(url).searchParams.getAll("project");
835
985
  let projectValue = parsed.values.project;
@@ -848,6 +998,40 @@ export async function runMcpCommand(argv, io = {}) {
848
998
  project = selected.project;
849
999
  serverName = selected.serverName;
850
1000
  }
1001
+ const urlReadOnly = new URL(url).searchParams.has("read_only");
1002
+ const readOnly = parsed.values["read-only"]
1003
+ ? "--read-only"
1004
+ : isReadOnlyUrl(url)
1005
+ ? "read_only in --url"
1006
+ : undefined;
1007
+ // Without --auth or read_only, an existing entry keeps its choice. A user-scope `claude mcp add`
1008
+ // refuses a name it already has, and Conductor keeps its default, so only these two are read.
1009
+ let existing;
1010
+ if (explicitAuth === undefined && !parsed.values["read-only"] && !urlReadOnly) {
1011
+ if (clientId === "claude-code" && scope === "project")
1012
+ existing = await readClaudeProjectEntry(cwd, serverName);
1013
+ else if (clientId === "codex") {
1014
+ // Without codex on PATH nothing is run, so nothing Codex holds is replaced. With it, an
1015
+ // entry that cannot be read may hold either a key or a sign-in, so the command does not
1016
+ // guess which to keep.
1017
+ const codex = await findExecutable("codex", env);
1018
+ const lookup = codex ? await readCodexEntry(codex, serverName, { cwd, env }) : undefined;
1019
+ if (lookup?.state === "unknown")
1020
+ return fail(`Could not read Codex's "${serverName}" entry (codex mcp get failed), so the command will not guess whether to keep a key or sign-in. Pass --auth oauth or --auth key.`, 2);
1021
+ if (lookup?.state === "found")
1022
+ existing = lookup.entry;
1023
+ }
1024
+ }
1025
+ const { auth, keepReadOnly, note } = explicitAuth === undefined
1026
+ ? defaultAuth(clientId, readOnly, existing, env)
1027
+ : { auth: explicitAuth };
1028
+ // A sign-in connection's access is chosen when it is approved; read_only is not part of it.
1029
+ if (auth === "oauth" && readOnly)
1030
+ return fail("A sign-in connection has Read and write access and cannot be made read-only by its URL. For read-only access, use a Read project key (--auth key), or add --read-only to a key configuration.", 2);
1031
+ if (auth === "oauth" && urlReadOnly)
1032
+ return fail("Remove read_only from --url: a sign-in URL does not carry it, since a sign-in connection has Read and write access, and Hue reads only read_only=true or 1. For read-only access, use a Read project key (--auth key).", 2);
1033
+ if (parsed.values["read-only"] || keepReadOnly)
1034
+ url = readOnlyMcpUrl(url);
851
1035
  url = projectMcpUrl(url, project);
852
1036
  // --toolsets wins over a selection already in --url, which wins over the client's default.
853
1037
  // Both sources are validated: Hue ignores an unknown name and would list its default.
@@ -860,6 +1044,9 @@ export async function runMcpCommand(argv, io = {}) {
860
1044
  toolsets = selected.toolsets;
861
1045
  }
862
1046
  url = toolsetsMcpUrl(url, toolsets);
1047
+ // On stderr, so --print and --dry-run output stays as it was.
1048
+ if (note)
1049
+ stderr.write(`${note}\n`);
863
1050
  const shownUrl = displayMcpUrl(url);
864
1051
  // What the command reports doing; a command a person is to run is printed as given.
865
1052
  const shownCommand = (command) => command.display.replaceAll(shellWord(url), () => shellWord(shownUrl));
@@ -868,7 +1055,7 @@ export async function runMcpCommand(argv, io = {}) {
868
1055
  stdout.write(plan.snippet);
869
1056
  return 0;
870
1057
  }
871
- const steps = nextSteps(clientId, auth, serverName, project);
1058
+ const steps = nextSteps(clientId, auth, serverName, project, scope, isReadOnlyUrl(url));
872
1059
  if (plan.kind === "manual") {
873
1060
  out(plan.hint);
874
1061
  stdout.write(plan.snippet);
@@ -883,6 +1070,9 @@ export async function runMcpCommand(argv, io = {}) {
883
1070
  return 0;
884
1071
  }
885
1072
  let failed = false;
1073
+ // `codex mcp add` signs in at once and waits up to five minutes for the browser. Outside a
1074
+ // terminal (CI, an agent's shell tool) nobody may finish that, so a default sign-in prints it.
1075
+ const deferCodex = auth === "oauth" && explicitAuth === undefined && !isTerminal(stdout);
886
1076
  for (const command of plan.commands) {
887
1077
  const executable = await findExecutable(command.executable, env);
888
1078
  if (!executable) {
@@ -891,12 +1081,36 @@ export async function runMcpCommand(argv, io = {}) {
891
1081
  out(command.display);
892
1082
  continue;
893
1083
  }
1084
+ if (deferCodex && command.executable === "codex") {
1085
+ out("Not running codex mcp add: it signs in at once and waits up to 5 minutes for the browser, and no terminal is attached. Run it where you can sign in, or pass --auth oauth to run it here and wait:");
1086
+ out(command.display);
1087
+ continue;
1088
+ }
1089
+ // What Codex held before, so a failed add is not mistaken for this run's registration.
1090
+ const codexSignIn = auth === "oauth" && command.executable === "codex";
1091
+ const before = codexSignIn
1092
+ ? await readCodexEntry(executable, serverName, { cwd, env })
1093
+ : undefined;
894
1094
  out(`Running: ${shownCommand(command)}`);
895
1095
  const code = await runClientCli(executable, command.args, { cwd, env, stdout, stderr });
896
1096
  if (code === 0) {
897
1097
  out(`Registered the "${serverName}" MCP server (${shownUrl}) with ${command.label}.`);
898
1098
  continue;
899
1099
  }
1100
+ // Codex writes its entry before signing in, so a sign-in that timed out or was denied
1101
+ // leaves the server registered; `codex mcp login` signs in again. Only an entry this run
1102
+ // wrote counts: one Codex already held unchanged proves nothing about this add.
1103
+ if (code !== null && codexSignIn) {
1104
+ const after = await readCodexEntry(executable, serverName, { cwd, env });
1105
+ const wrote = after.state === "found" &&
1106
+ after.entry.auth === "oauth" &&
1107
+ after.entry.url === url &&
1108
+ (before?.state === "none" || (before?.state === "found" && before.raw !== after.raw));
1109
+ if (wrote) {
1110
+ out(`Registered the "${serverName}" MCP server (${shownUrl}) with Codex, but its sign-in did not finish. Run codex mcp login ${serverName} to sign in.`);
1111
+ continue;
1112
+ }
1113
+ }
900
1114
  failed = true;
901
1115
  stderr.write(`${command.executable} ${code === null ? "could not be started" : `exited with code ${code}`}. Run this command yourself:\n${command.display}\n`);
902
1116
  }
package/dist/client.d.ts CHANGED
@@ -43,6 +43,9 @@ export declare class HueClient {
43
43
  readonly enabled: boolean;
44
44
  private logger;
45
45
  private storage;
46
+ /** Reports a contained capture or instrumentation failure, named to the trace active where
47
+ * it happened when there is one. */
48
+ private instrumentationFailed;
46
49
  private tracerProvider;
47
50
  private loggerProvider;
48
51
  private ownedProviders?;
package/dist/client.js CHANGED
@@ -169,6 +169,20 @@ export class HueClient {
169
169
  enabled;
170
170
  logger;
171
171
  storage = new AsyncLocalStorage();
172
+ /** Reports a contained capture or instrumentation failure, named to the trace active where
173
+ * it happened when there is one. */
174
+ instrumentationFailed(signal = "traces", count = 1) {
175
+ let traceId;
176
+ try {
177
+ traceId = trace
178
+ .getSpan(this.storage.getStore()?.context ?? context.active())
179
+ ?.spanContext().traceId;
180
+ }
181
+ catch {
182
+ // The failure is recorded without a trace.
183
+ }
184
+ this.transport.instrumentationFailure(signal, undefined, count, traceId);
185
+ }
172
186
  tracerProvider;
173
187
  loggerProvider;
174
188
  ownedProviders;
@@ -210,7 +224,7 @@ export class HueClient {
210
224
  }
211
225
  this.captureContent = this.transport.options.captureContent;
212
226
  this.enabled = this.transport.options.enabled !== false;
213
- this.tracer = new ContextualTracer(this.tracerProvider.getTracer(HUE_SCOPE, sdkVersion), this.storage, () => this.enabled && !this.closed, () => this.transport.instrumentationFailure());
227
+ this.tracer = new ContextualTracer(this.tracerProvider.getTracer(HUE_SCOPE, sdkVersion), this.storage, () => this.enabled && !this.closed, () => this.instrumentationFailed());
214
228
  this.logger = this.loggerProvider.getLogger(HUE_SCOPE, sdkVersion);
215
229
  }
216
230
  /**
@@ -232,7 +246,7 @@ export class HueClient {
232
246
  return this.storage.getStore()?.context ?? context.active();
233
247
  }
234
248
  catch {
235
- this.transport.instrumentationFailure();
249
+ this.instrumentationFailed();
236
250
  return ROOT_CONTEXT;
237
251
  }
238
252
  }
@@ -256,7 +270,7 @@ export class HueClient {
256
270
  span = this.storage.run(active, () => this.tracer.startSpan(name, { kind: options.kind ?? SpanKind.INTERNAL, attributes: options.attributes }, active.context));
257
271
  }
258
272
  catch {
259
- this.transport.instrumentationFailure();
273
+ this.instrumentationFailed();
260
274
  }
261
275
  }
262
276
  // A disabled or failed client creates no span; the application's own active span then stays
@@ -268,7 +282,7 @@ export class HueClient {
268
282
  spanContext = trace.setSpan(active.context, span);
269
283
  }
270
284
  catch {
271
- this.transport.instrumentationFailure();
285
+ this.instrumentationFailed();
272
286
  spanContext = trace.setSpan(ROOT_CONTEXT, span);
273
287
  }
274
288
  const handle = {
@@ -287,7 +301,7 @@ export class HueClient {
287
301
  handle.setInput(options.input);
288
302
  }
289
303
  catch {
290
- this.transport.instrumentationFailure();
304
+ this.instrumentationFailed();
291
305
  }
292
306
  try {
293
307
  return await callback(handle);
@@ -303,9 +317,7 @@ export class HueClient {
303
317
  // The span is also OpenTelemetry's active span while the callback runs, so spans from other
304
318
  // instrumentations (HTTP clients, provider SDKs) join this trace when the application has
305
319
  // registered a context manager. Hue still registers none itself.
306
- return this.storage.run({ ...active, context: spanContext }, () => created
307
- ? runInContext(spanContext, execute, () => this.transport.instrumentationFailure())
308
- : execute());
320
+ return this.storage.run({ ...active, context: spanContext }, () => created ? runInContext(spanContext, execute, () => this.instrumentationFailed()) : execute());
309
321
  }
310
322
  /**
311
323
  * Runs `execute` inside an `execute_tool` span named after the tool. `input` and a defined result
@@ -329,7 +341,7 @@ export class HueClient {
329
341
  if (isLabel(value))
330
342
  attributes[key] = value;
331
343
  else if (this.enabled && !this.closed)
332
- this.transport.instrumentationFailure();
344
+ this.instrumentationFailed();
333
345
  };
334
346
  stamp("gen_ai.tool.call.id", options.callId);
335
347
  stamp("mcp.server.name", options.mcp?.name);
@@ -364,7 +376,7 @@ export class HueClient {
364
376
  if (isLabel(value))
365
377
  return value;
366
378
  if (active)
367
- this.transport.instrumentationFailure();
379
+ this.instrumentationFailed();
368
380
  return fallback;
369
381
  };
370
382
  const requestModel = label(model, "unknown");
@@ -413,14 +425,14 @@ export class HueClient {
413
425
  if (value === undefined)
414
426
  continue;
415
427
  if (!Number.isInteger(value) || value < 0) {
416
- this.transport.instrumentationFailure();
428
+ this.instrumentationFailed();
417
429
  continue;
418
430
  }
419
431
  try {
420
432
  span.setAttribute(key, value);
421
433
  }
422
434
  catch {
423
- this.transport.instrumentationFailure();
435
+ this.instrumentationFailed();
424
436
  }
425
437
  }
426
438
  }
@@ -433,7 +445,7 @@ export class HueClient {
433
445
  propagator.inject(activeContext, carrier, defaultTextMapSetter);
434
446
  }
435
447
  catch {
436
- this.transport.instrumentationFailure();
448
+ this.instrumentationFailed();
437
449
  }
438
450
  }
439
451
  /** Reads W3C trace context from a carrier for use as `parentContext`. */
@@ -442,7 +454,7 @@ export class HueClient {
442
454
  return propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter);
443
455
  }
444
456
  catch {
445
- this.transport.instrumentationFailure();
457
+ this.instrumentationFailed();
446
458
  return ROOT_CONTEXT;
447
459
  }
448
460
  }
@@ -461,7 +473,7 @@ export class HueClient {
461
473
  span.addEvent("exception", { "exception.type": type });
462
474
  }
463
475
  catch {
464
- this.transport.instrumentationFailure();
476
+ this.instrumentationFailed();
465
477
  }
466
478
  }
467
479
  /**
@@ -498,7 +510,7 @@ export class HueClient {
498
510
  else if (typeof explicit === "string" && explicit.trim() && explicit.length <= 256)
499
511
  attributes[key] = explicit;
500
512
  else
501
- this.transport.instrumentationFailure("logs");
513
+ this.instrumentationFailed("logs");
502
514
  };
503
515
  stamp("gen_ai.operation.name", messages.operation, enclosing?.model?.operation);
504
516
  stamp("gen_ai.provider.name", messages.provider, enclosing?.model?.provider);
@@ -513,7 +525,7 @@ export class HueClient {
513
525
  });
514
526
  }
515
527
  catch {
516
- this.transport.instrumentationFailure("logs");
528
+ this.instrumentationFailed("logs");
517
529
  }
518
530
  }
519
531
  /**
@@ -545,7 +557,7 @@ export class HueClient {
545
557
  const addresses = hostedServerAddresses(provider, options.request);
546
558
  const activity = hostedToolActivity(provider, response);
547
559
  if (activity.skipped > 0)
548
- this.transport.instrumentationFailure("traces", undefined, activity.skipped);
560
+ this.instrumentationFailed("traces", activity.skipped);
549
561
  const server = (label) => {
550
562
  const attributes = {};
551
563
  if (label === undefined)
@@ -564,7 +576,7 @@ export class HueClient {
564
576
  if (isLabel(value))
565
577
  attributes[key] = value;
566
578
  else
567
- this.transport.instrumentationFailure();
579
+ this.instrumentationFailed();
568
580
  }
569
581
  const address = addresses.get(label);
570
582
  if (address !== undefined)
@@ -617,7 +629,7 @@ export class HueClient {
617
629
  span.setAttributes(toolCatalogSummary(encodeContent(listing.definitions, catalogLimits)));
618
630
  }
619
631
  catch {
620
- this.transport.instrumentationFailure();
632
+ this.instrumentationFailed();
621
633
  }
622
634
  if (listing.errorType !== undefined)
623
635
  fail(span, listing.errorType);
@@ -625,7 +637,7 @@ export class HueClient {
625
637
  }
626
638
  }
627
639
  catch {
628
- this.transport.instrumentationFailure();
640
+ this.instrumentationFailed();
629
641
  }
630
642
  }
631
643
  /**
@@ -692,12 +704,12 @@ export class HueClient {
692
704
  if (isLabel(name))
693
705
  attributes["hue.file.name"] = name;
694
706
  else
695
- this.transport.instrumentationFailure();
707
+ this.instrumentationFailed();
696
708
  }
697
709
  span.addEvent("hue.file", attributes);
698
710
  }
699
711
  catch {
700
- this.transport.instrumentationFailure();
712
+ this.instrumentationFailed();
701
713
  }
702
714
  }
703
715
  setContent(span, key, value) {
@@ -707,7 +719,7 @@ export class HueClient {
707
719
  span.setAttribute(key, encodeContent(value));
708
720
  }
709
721
  catch {
710
- this.transport.instrumentationFailure();
722
+ this.instrumentationFailed();
711
723
  }
712
724
  }
713
725
  /**
@@ -23,10 +23,12 @@ export interface AgentEnvironmentOptions {
23
23
  * default; the canonical carriers are the world's own `env`. */
24
24
  legacyMcpVariables?: boolean;
25
25
  }
26
+ /** `environment` without any variable a world or its server owns. */
27
+ export declare function withoutWorldVariables(environment: Record<string, string>): Record<string, string>;
26
28
  /**
27
29
  * The environment for an agent child process running one case (the coordination briefs'
28
- * recommended policy): the parent's variables minus Hue control-plane credentials, then the
29
- * world's carriers, which win over anything the parent set. Nothing here is logged.
30
+ * recommended policy): the parent's variables minus Hue control-plane credentials and any
31
+ * world-scoped variable, then the world's carriers. Nothing here is logged.
30
32
  */
31
33
  export declare function agentEnvironment(world: WorldHandoff, options?: AgentEnvironmentOptions): Record<string, string>;
32
34
  /** The `{ url, token, expiresAt }` shape the `hue_sim_` capability had, projected from the