@hue-run/sdk 0.11.0 → 0.11.2
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/CLI.md +68 -42
- package/dist/cli/eval.d.ts +4 -0
- package/dist/cli/eval.js +6 -5
- package/dist/cli/listen.d.ts +1 -1
- package/dist/cli/listen.js +34 -11
- package/dist/cli/mcp.d.ts +29 -17
- package/dist/cli/mcp.js +143 -78
- package/dist/evals/client.d.ts +11 -5
- package/dist/evals/client.js +29 -12
- package/dist/evals/files.js +18 -10
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/CLI.md
CHANGED
|
@@ -42,7 +42,7 @@ project manifest are refused because managers can update ancestor locks; Python
|
|
|
42
42
|
The generated `hue.setup.mjs` or `hue_setup.py` always selects `captureContent: false` /
|
|
43
43
|
`capture_content=False`. For a supported application, setup installs the dependency and adds the
|
|
44
44
|
managed import and middleware registration to the existing entrypoint; an unreferenced helper is
|
|
45
|
-
not a completed integration. TypeScript uses `@hue-run/sdk@0.11.
|
|
45
|
+
not a completed integration. TypeScript uses `@hue-run/sdk@0.11.2`, `@opentelemetry/api@1.9.1` and
|
|
46
46
|
`@opentelemetry/context-async-hooks@2.11.0`; Python setup uses its separately tested package pin.
|
|
47
47
|
Content capture requires an ordinary account-managed key and a later explicit application decision.
|
|
48
48
|
|
|
@@ -168,12 +168,13 @@ accepted too, but Node 22 and 24 read that flag from the whole command line and
|
|
|
168
168
|
The env file is written with mode `0600` through a temporary file and an atomic rename. Other
|
|
169
169
|
lines are preserved; a symlink or a non-regular file is refused; an existing different value is
|
|
170
170
|
replaced only with `--force`. Empty values, whitespace and URLs are refused before any request.
|
|
171
|
-
The MCP
|
|
172
|
-
`https://mcp.staging.hue.run/mcp`
|
|
173
|
-
plain HTTP origins are accepted for loopback test servers only.
|
|
174
|
-
(`Stored the key (NN chars) as HUE_API_KEY and HUE_MCP_KEY`),
|
|
175
|
-
|
|
176
|
-
`1` failed, `2` usage error, `130`
|
|
171
|
+
The production MCP server is available only at `https://mcp.hue.run/mcp`. `hue login` uses
|
|
172
|
+
`https://mcp.staging.hue.run/mcp` with `https://staging.hue.run` and `<origin>/api/mcp` for other
|
|
173
|
+
development or preview origins; plain HTTP origins are accepted for loopback test servers only.
|
|
174
|
+
Output names variables and lengths (`Stored the key (NN chars) as HUE_API_KEY and HUE_MCP_KEY`),
|
|
175
|
+
never values. When git does not ignore the env file, the command warns; `--gitignore` appends the
|
|
176
|
+
file name to the `.gitignore` next to it. Exit codes: `0` stored, `1` failed, `2` usage error, `130`
|
|
177
|
+
interrupted.
|
|
177
178
|
|
|
178
179
|
## Install the MCP for your coding agent
|
|
179
180
|
|
|
@@ -187,39 +188,63 @@ for staging). Hue's Settings page does not show these snippets; this command kee
|
|
|
187
188
|
- `key` (the default, except for `conductor`) references the `HUE_MCP_KEY` environment variable,
|
|
188
189
|
which `hue login` stores in `.env.hue`. A key value is never written.
|
|
189
190
|
- `oauth` configures the URL only. The client opens Hue in a browser, where you sign in and
|
|
190
|
-
approve the connection; no key
|
|
191
|
+
approve the connection; no key is involved. A new sign-in connection has **Read and
|
|
191
192
|
write** access to every active project in the organization you choose, within your role, so an
|
|
192
193
|
agent first calls `list_projects` and passes the chosen project's id as `project_id`. It is available
|
|
193
194
|
for `claude-code`, `codex` and `conductor`. Hue does not yet accept Cursor's sign-in callback, so
|
|
194
195
|
`cursor` and the other clients use a key.
|
|
195
196
|
|
|
197
|
+
`--project <id-or-slug>` pins either kind of connection to one project. The command adds or
|
|
198
|
+
replaces `?project=<value>` while keeping `toolsets`, and names the server `hue-<value>` so it can
|
|
199
|
+
sit beside the organization-wide `hue` connection. A pinned connection's tools do not publish or
|
|
200
|
+
accept `project_id`; `list_projects` returns only that project with `connection.pinned: true`.
|
|
201
|
+
The printed sign-in Codex TOML keeps its URL bare and sends `X-Hue-MCP-Project` together with any
|
|
202
|
+
`X-Hue-MCP-Toolsets` header. An empty pin is refused. If the selected project is inactive or the
|
|
203
|
+
credential cannot reach it, Hue returns HTTP `404`; change or remove `--project`, because signing
|
|
204
|
+
in again does not change the credential's reach.
|
|
205
|
+
|
|
196
206
|
`--read-only` adds `?read_only=true` to a key configuration's URL, so Hue hides and rejects write
|
|
197
207
|
tools whatever the key allows. A project key, with or without it, keeps reaching its one project.
|
|
198
208
|
With `--auth oauth` it is refused: a sign-in connection has **Read and write** access, so use a
|
|
199
209
|
**Read** project key for read-only access.
|
|
200
210
|
|
|
201
|
-
`--toolsets <names>`
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
`intents` and `docs`, comma-separated.
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
selection
|
|
211
|
+
`--toolsets <names>` chooses the tools the connection lists: `all`; the profiles `observe`
|
|
212
|
+
(production reads), `author` (author evaluations) and `evaluate` (run an agent against a case);
|
|
213
|
+
or the groups `project`, `traces`, `eval_sets`, `runs`, `judges`, `cases`, `runners`,
|
|
214
|
+
`environments`, `intents` and `docs`, comma-separated. Each group holds at most 20 tools. The
|
|
215
|
+
selection is added to the URL as `?toolsets=<names>`, for a key or a sign-in configuration alike.
|
|
216
|
+
Without `--toolsets`, `claude-code`, `codex` and `conductor` select `all`,
|
|
217
|
+
because those agents defer MCP tools behind their own tool search; `cursor`, which caps the tools
|
|
218
|
+
it loads, selects `observe`; the other clients list Hue's default. The default is the production
|
|
219
|
+
reads plus `search_hue_tools`, which finds every other tool the connection can call, and
|
|
220
|
+
`execute_hue_tool` and `execute_hue_write_tool`, which call one by name (the write executor only
|
|
221
|
+
with write access), so nothing is out of reach. `--toolsets` replaces a selection already in
|
|
222
|
+
`--url`, and a selection in `--url` is kept without it. Unknown names are refused rather than
|
|
223
|
+
passed on, because Hue ignores them and would list its default. `get_project_context` is always
|
|
224
|
+
listed and reports the selection.
|
|
225
|
+
|
|
226
|
+
MCP arguments and results name ids after their product objects. Eval tools use `eval_set_id`,
|
|
227
|
+
`eval_set_version_id`, `from_eval_set_version_id` and `eval_set_case_id`; `evaluator_id`,
|
|
228
|
+
`evaluator_version_id` and `evaluator_version_ids`; `run_id` and `baseline_run_id`; and
|
|
229
|
+
`scoring_run_id` and `scoring_item_id`. Dispatches use `managed_run_id` and `local_run_id`,
|
|
230
|
+
reviewed cases `case_id`, environment versions `environment_version_id`, and trace-check versions
|
|
231
|
+
`trace_check_version_id` or `active_trace_check_version_id`. List results are named `eval_sets`,
|
|
232
|
+
`evaluators`, `runs`, `scoring_runs` and `cases`, while a row's own id remains `id`.
|
|
212
233
|
|
|
213
234
|
| Client | Key (`--auth key`) | Sign-in (`--auth oauth`) |
|
|
214
235
|
| --- | --- | --- |
|
|
215
236
|
| `claude-code` | Merges `mcpServers.hue` into `./.mcp.json`. `--scope user` runs `claude mcp add --transport http --scope user hue URL --header 'Authorization: Bearer ${HUE_MCP_KEY}'` when `claude` is on `PATH`, otherwise prints it. | Merges `mcpServers.hue` with only `type` and `url` into `./.mcp.json`; `--scope user` runs `claude mcp add --transport http --scope user hue URL`. Then run `/mcp` in Claude Code, select `hue` and choose **Authenticate**. |
|
|
216
|
-
| `codex` | Runs `codex mcp add hue --url URL --bearer-token-env-var HUE_MCP_KEY`, or prints the `[mcp_servers.hue]` TOML block for `~/.codex/config.toml`. | Runs `codex mcp add hue --url URL`, which can start sign-in right away; `codex mcp login hue` starts or repeats it. |
|
|
237
|
+
| `codex` | Runs `codex mcp add hue --url URL --bearer-token-env-var HUE_MCP_KEY`, or prints the `[mcp_servers.hue]` TOML block for `~/.codex/config.toml`. | Runs `codex mcp add hue --url URL`, which can start sign-in right away; `codex mcp login hue` starts or repeats it. The printed TOML block keeps its URL bare and sends the selection as the `X-Hue-MCP-Toolsets` header. |
|
|
217
238
|
| `conductor` | Runs the Claude Code user-scope and Codex key commands above. | The default: runs `claude mcp add --transport http --scope user hue URL` and `codex mcp add hue --url URL`. Then use `hue`'s authentication action in Conductor's MCP status (the plug icon or `/mcp-status`). |
|
|
218
239
|
| `cursor` | Merges `mcpServers.hue` into `./.cursor/mcp.json` with `${env:HUE_MCP_KEY}`. | Refused. |
|
|
219
240
|
| `vscode` | Merges `servers.hue` and the `hue-mcp-key` password input into `./.vscode/mcp.json`. | Refused. |
|
|
220
241
|
| `windsurf` | Prints the `serverUrl` snippet for `~/.codeium/windsurf/mcp_config.json`; nothing is written to the home directory. | Refused. |
|
|
221
242
|
| `gemini` | Runs `gemini mcp add --scope user --transport http hue URL --header 'Authorization: Bearer ${HUE_MCP_KEY}'`, or prints it. | Refused. |
|
|
222
243
|
|
|
244
|
+
Every Codex TOML block sets `supports_parallel_tool_calls = true`, so Codex runs Hue's independent
|
|
245
|
+
calls in parallel rather than one at a time. `codex mcp add` has no option for it, so after
|
|
246
|
+
registering with Codex the command names the line to add under `[mcp_servers.hue]`.
|
|
247
|
+
|
|
223
248
|
Conductor has no MCP configuration of its own: its Claude Code and Codex agents read their user
|
|
224
249
|
configuration in every workspace, so `conductor` registers the server at user scope with each CLI
|
|
225
250
|
on `PATH` and prints the command for a CLI that is missing. A failing CLI does not stop the other;
|
|
@@ -228,7 +253,8 @@ through a trace read) end to end; if Hue's tools do not load after signing in, r
|
|
|
228
253
|
again with `--auth key`.
|
|
229
254
|
|
|
230
255
|
JSON files are parsed and merged: other servers, inputs and top-level fields are kept, only the
|
|
231
|
-
|
|
256
|
+
selected server entry is replaced (`hue`, or `hue-<value>` with `--project`), and invalid JSON
|
|
257
|
+
(including comments) is refused together with the snippet
|
|
232
258
|
to add by hand. Files are written with mode `0644` through a temporary file and an atomic rename;
|
|
233
259
|
symlinks are refused. `--dry-run` prints the resulting file content or commands without writing or
|
|
234
260
|
running; `--print` prints only the snippet. Client CLIs run without a shell, so the
|
|
@@ -240,7 +266,7 @@ the client (VS Code prompts for the key instead). A desktop app started from the
|
|
|
240
266
|
does not see that shell's variables, and Conductor's agents read the login-shell environment that
|
|
241
267
|
Conductor captures, so sign-in is the simpler choice there. After a sign-in installation it names
|
|
242
268
|
the client's authentication action. Both end with the prompt that verifies the connection:
|
|
243
|
-
`Use the Hue MCP: call list_projects and confirm which project to inspect. Then call get_project_context and show that project's traces from the last 24 hours that need attention or have errors, with links; if there are none, show its 5 most recent traces. For an organization connection, pass the project's id as project_id on each call after list_projects.` `list_projects` returns a project key's one project too. Exit codes:
|
|
269
|
+
`Use the Hue MCP: call list_projects and confirm which project to inspect. Then call get_project_context and show that project's traces from the last 24 hours that need attention or have errors, with links; if there are none, show its 5 most recent traces. For an organization connection, pass the project's id or slug as project_id on each call after list_projects.` A pinned installation instead verifies `connection.pinned: true` and omits `project_id`. `list_projects` returns a project key's one project too. Exit codes:
|
|
244
270
|
`0` done or printed, `1` failed, `2` usage error.
|
|
245
271
|
|
|
246
272
|
## Deliver simulated events to a local bot
|
|
@@ -249,12 +275,10 @@ the client's authentication action. Both end with the prompt that verifies the c
|
|
|
249
275
|
agent such as a Slack bot can be tested without a public URL, the way `stripe listen --forward-to`
|
|
250
276
|
delivers webhooks. Hue signs each request as the provider does (Slack Events API requests today);
|
|
251
277
|
the command pulls them, forwards each one unchanged to `--forward-to` and reports the bot's answer
|
|
252
|
-
to Hue
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
has answered it. Event subscriptions are not yet available on `https://app.hue.run`; until the
|
|
257
|
-
origin offers them, the command stops with Hue's refusal.
|
|
278
|
+
to Hue. It needs a **listen** event subscription on the world or on a connection key, and the bot
|
|
279
|
+
configured with that subscription's signing secret where `SLACK_SIGNING_SECRET` went. Event
|
|
280
|
+
subscriptions are not yet available on `https://app.hue.run`; until the origin offers them, the
|
|
281
|
+
command exits `1` with the refusal it receives.
|
|
258
282
|
|
|
259
283
|
```sh
|
|
260
284
|
hue listen --subscription <id> --forward-to http://localhost:3000/slack/events --env-path .env.world
|
|
@@ -263,8 +287,8 @@ The credential is the subscription's own, read from the environment (or `--env-p
|
|
|
263
287
|
a variable already set keeps its value): `HUE_WORLD_TOKEN` for a world's subscription or
|
|
264
288
|
`HUE_CONNECTION_KEY` for a connection key's, chosen with `--credential world-token|connection-key`
|
|
265
289
|
when both are set. It is never taken from the command line, where other local processes and shell
|
|
266
|
-
history can read it. A project key is refused: a value equal to `HUE_API_KEY
|
|
267
|
-
|
|
290
|
+
history can read it. A project key is refused: a value equal to `HUE_API_KEY`, `HUE_MCP_KEY`,
|
|
291
|
+
`HUE_PROJECT_KEY` or `HUE_SERVICE_KEY` exits 2 before any request.
|
|
268
292
|
`--origin` (default `HUE_BASE_URL`, then `https://app.hue.run`) must be HTTPS, or plain HTTP on a
|
|
269
293
|
loopback test server.
|
|
270
294
|
|
|
@@ -276,12 +300,13 @@ a later one is a timeout. A failing answer with `x-slack-no-retry: 1` is reporte
|
|
|
276
300
|
retrying it. Only a successful URL verification answer's body (at most 4 KiB, for its challenge) is
|
|
277
301
|
sent to Hue; any other answer body stays on this machine. Each pull waits for at most 20 seconds and
|
|
278
302
|
leases up to `--max` deliveries (1 to 10, default 10), which are forwarded concurrently; the next
|
|
279
|
-
pull starts once they are acknowledged, and a delivery beyond `--max` is never forwarded. A
|
|
280
|
-
|
|
303
|
+
pull starts once they are acknowledged, and a delivery beyond `--max` is never forwarded. A pull
|
|
304
|
+
that answers at once with nothing new to forward is not repeated for a second. A provider retry of
|
|
305
|
+
an event is a new delivery and is forwarded again with its `X-Slack-Retry-Num` and
|
|
281
306
|
`X-Slack-Retry-Reason`, so the bot deduplicates by `event_id` as it does with Slack. A delivery Hue
|
|
282
307
|
hands out a second time is not sent again; its recorded answer is repeated. An acknowledgement Hue
|
|
283
|
-
did not answer is repeated until the delivery's lease ends, as
|
|
284
|
-
|
|
308
|
+
did not answer is repeated until the delivery's lease ends, as the pull's answer stated it, and for
|
|
309
|
+
at most 30 seconds.
|
|
285
310
|
|
|
286
311
|
`--forward-to` must name this machine: `localhost`, a `.localhost` name or a loopback address, and a
|
|
287
312
|
name is refused unless every address it resolves to is a loopback address. `--allow-remote-forward`
|
|
@@ -292,15 +317,16 @@ bot's status and duration, and the state Hue recorded), never a body, a signatur
|
|
|
292
317
|
credentials in error messages are replaced with `[redacted]`.
|
|
293
318
|
|
|
294
319
|
Ctrl+C or `SIGTERM` stops pulling (a waiting pull is cancelled), finishes the deliveries in flight
|
|
295
|
-
and acknowledges them, then exits `0
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
320
|
+
and acknowledges them, then exits `0`, no sooner than a second after the first Ctrl+C. A second
|
|
321
|
+
Ctrl+C a second or more after the first abandons the forwards and acknowledgements still in flight
|
|
322
|
+
and exits `130`; one within a second of the first, such as the copy `npx` forwards to the command,
|
|
323
|
+
is the same stop. Nothing is acknowledged before the bot answered or its window closed; a delivery
|
|
324
|
+
left unacknowledged is left to its lease. Network errors, `429`, `5xx`, an unreadable pull answer
|
|
299
325
|
and another open pull for the same subscription (a second `hue listen`) are retried with backoff up
|
|
300
326
|
to 30 seconds; a `Retry-After` can lengthen a wait to at most 60 seconds, never shorten it. Exit
|
|
301
327
|
codes: `0` stopped, `1` Hue refused the credential, the subscription (unknown, revoked, or one that
|
|
302
|
-
delivers to a request URL) or the pull itself (a redirect, another refusal or an
|
|
303
|
-
`2` usage error, `130` interrupted twice.
|
|
328
|
+
delivers to a request URL) or the pull itself (a redirect, another refusal or an answer without
|
|
329
|
+
deliveries), `2` usage error, `130` interrupted twice.
|
|
304
330
|
|
|
305
331
|
## Local state and conflicts
|
|
306
332
|
|
|
@@ -406,7 +432,7 @@ node packages/sdk-typescript/scripts/verify-package.mjs --artifacts-dir .artifac
|
|
|
406
432
|
# Set project to an existing supported fixture; use the same directory on resume.
|
|
407
433
|
project=/absolute/path/to/supported-fixture
|
|
408
434
|
node packages/sdk-typescript/scripts/verify-setup-live.mjs \
|
|
409
|
-
--archive .artifacts/typescript/hue-run-sdk-0.11.
|
|
435
|
+
--archive .artifacts/typescript/hue-run-sdk-0.11.2.tgz \
|
|
410
436
|
--origin https://STAGING_ORIGIN \
|
|
411
437
|
--project "$project" --command setup \
|
|
412
438
|
--evidence .context/setup-staging-before-claim.json
|
|
@@ -417,7 +443,7 @@ the private local handoff and finish the real browser claim, then reconcile the
|
|
|
417
443
|
|
|
418
444
|
```sh
|
|
419
445
|
node packages/sdk-typescript/scripts/verify-setup-live.mjs \
|
|
420
|
-
--archive .artifacts/typescript/hue-run-sdk-0.11.
|
|
446
|
+
--archive .artifacts/typescript/hue-run-sdk-0.11.2.tgz \
|
|
421
447
|
--origin https://STAGING_ORIGIN \
|
|
422
448
|
--project "$project" --command claim \
|
|
423
449
|
--evidence .context/setup-staging-after-claim.json
|
package/dist/cli/eval.d.ts
CHANGED
|
@@ -27,6 +27,10 @@ export interface Output {
|
|
|
27
27
|
/** Diagnostics; always stderr. */
|
|
28
28
|
error(line: string): void;
|
|
29
29
|
}
|
|
30
|
+
/** Agent key from the adapter filename, or the first script-like token of a command. An
|
|
31
|
+
* interpreter is skipped with a Windows executable suffix too (`node.exe`, and `bun.exe`, as npm
|
|
32
|
+
* installs Bun on every platform). */
|
|
33
|
+
export declare function derivedAgentKey(adapterFile: string | undefined, command: string | undefined): string;
|
|
30
34
|
/**
|
|
31
35
|
* Spawns the agent command once in its own process group and returns its trimmed stdout. A
|
|
32
36
|
* timeout or Ctrl+C stops the agent the shell started, not only the shell: a survivor would still
|
package/dist/cli/eval.js
CHANGED
|
@@ -179,15 +179,16 @@ const INTERPRETERS = new Set([
|
|
|
179
179
|
"zsh",
|
|
180
180
|
"env",
|
|
181
181
|
]);
|
|
182
|
-
/** Agent key from the adapter filename, or the first script-like token of a command.
|
|
183
|
-
|
|
182
|
+
/** Agent key from the adapter filename, or the first script-like token of a command. An
|
|
183
|
+
* interpreter is skipped with a Windows executable suffix too (`node.exe`, and `bun.exe`, as npm
|
|
184
|
+
* installs Bun on every platform). */
|
|
185
|
+
export function derivedAgentKey(adapterFile, command) {
|
|
184
186
|
if (adapterFile)
|
|
185
187
|
return slug(basename(adapterFile, extname(adapterFile)));
|
|
186
188
|
const tokens = (command ?? "").trim().split(/\s+/);
|
|
187
189
|
const names = tokens.map((token) => token.split(/[\\/]/).pop() ?? "");
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
"";
|
|
190
|
+
const interpreter = (name) => INTERPRETERS.has(name.toLowerCase().replace(/\.(?:exe|cmd|bat)$/, ""));
|
|
191
|
+
const script = names.find((name) => name && !name.startsWith("-") && !interpreter(name)) ?? names[0] ?? "";
|
|
191
192
|
return slug(basename(script, extname(script)));
|
|
192
193
|
}
|
|
193
194
|
function defaultRunName(agent) {
|
package/dist/cli/listen.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export interface ListenCommandIo {
|
|
|
23
23
|
/** Delays between retries of a failed pull. */
|
|
24
24
|
retryDelaysMs?: readonly number[];
|
|
25
25
|
}
|
|
26
|
-
export declare const LISTEN_USAGE = "Usage: hue listen --subscription ID --forward-to URL [--origin URL] [--env-path PATH]\n [--credential world-token|connection-key] [--max N] [--allow-remote-forward]\n\nDeliver a simulated world's events to a receiver on this machine. Hue's signed requests (Slack\nEvents API requests today) are pulled with the subscription's own credential, forwarded unchanged\nto --forward-to, and acknowledged with the receiver's answer. Press Ctrl+C to stop: pulls stop,\ndeliveries in flight are finished and acknowledged, and nothing unanswered is acknowledged.\n\nThe credential is read from the environment, never from the command line:\n HUE_WORLD_TOKEN the world token, for a subscription on one world\n HUE_CONNECTION_KEY the connection key, for a subscription on a connection key\nA project key (HUE_API_KEY, HUE_MCP_KEY) is refused.\n\nOptions:\n --subscription ID The listen subscription to deliver (its id from Hue)\n --forward-to URL The local receiver, for example http://localhost:3000/slack/events\n --origin URL Hue origin (default HUE_BASE_URL, then https://app.hue.run)\n --env-path PATH Load a dotenv file first (--env-file also works when it exists)\n --credential KIND world-token or connection-key, when both variables are set\n --max N Deliveries leased per pull and forwarded at once, 1 to 10 (default 10)\n --allow-remote-forward Allow a --forward-to host other than this machine, over https only\n -h, --help Show this help";
|
|
26
|
+
export declare const LISTEN_USAGE = "Usage: hue listen --subscription ID --forward-to URL [--origin URL] [--env-path PATH]\n [--credential world-token|connection-key] [--max N] [--allow-remote-forward]\n\nDeliver a simulated world's events to a receiver on this machine. Hue's signed requests (Slack\nEvents API requests today) are pulled with the subscription's own credential, forwarded unchanged\nto --forward-to, and acknowledged with the receiver's answer. Press Ctrl+C to stop: pulls stop,\ndeliveries in flight are finished and acknowledged, and nothing unanswered is acknowledged.\n\nThe credential is read from the environment, never from the command line:\n HUE_WORLD_TOKEN the world token, for a subscription on one world\n HUE_CONNECTION_KEY the connection key, for a subscription on a connection key\nA project key (HUE_API_KEY, HUE_MCP_KEY, HUE_PROJECT_KEY or HUE_SERVICE_KEY) is refused.\n\nOptions:\n --subscription ID The listen subscription to deliver (its id from Hue)\n --forward-to URL The local receiver, for example http://localhost:3000/slack/events\n --origin URL Hue origin (default HUE_BASE_URL, then https://app.hue.run)\n --env-path PATH Load a dotenv file first (--env-file also works when it exists)\n --credential KIND world-token or connection-key, when both variables are set\n --max N Deliveries leased per pull and forwarded at once, 1 to 10 (default 10)\n --allow-remote-forward Allow a --forward-to host other than this machine, over https only\n -h, --help Show this help";
|
|
27
27
|
/** A leased delivery: the exact request the provider would send. */
|
|
28
28
|
interface Delivery {
|
|
29
29
|
deliveryId: string;
|
package/dist/cli/listen.js
CHANGED
|
@@ -5,6 +5,7 @@ import { request as httpsRequest } from "node:https";
|
|
|
5
5
|
import { isIP } from "node:net";
|
|
6
6
|
import { resolve } from "node:path";
|
|
7
7
|
import { parseArgs, parseEnv } from "node:util";
|
|
8
|
+
import { HUE_CONTROL_PLANE_VARIABLES } from "../environment/world.js";
|
|
8
9
|
import { scrubCredentialText } from "../tool-definitions.js";
|
|
9
10
|
import { sdkVersion } from "../version.js";
|
|
10
11
|
import { envFileArgument, envFileOptions } from "./env-file.js";
|
|
@@ -32,6 +33,11 @@ const ACK_RETRY_DELAYS_MS = [250, 500, 1_000, 2_000, 4_000];
|
|
|
32
33
|
const MAX_RETRY_AFTER_MS = 60_000;
|
|
33
34
|
/** Deliveries whose answer is remembered, so a delivery Hue hands out again is not re-sent. */
|
|
34
35
|
const REMEMBERED_DELIVERIES = 1_024;
|
|
36
|
+
/** The least time between pulls that forwarded nothing new, so repeats cannot spin. */
|
|
37
|
+
const IDLE_PULL_FLOOR_MS = 1_000;
|
|
38
|
+
/** A stop request this soon after the first is the same one: under `npx`, npm forwards the
|
|
39
|
+
* terminal's SIGINT to a process group that already received it. */
|
|
40
|
+
const REPEATED_STOP_MS = 1_000;
|
|
35
41
|
/** Connection-level headers the local request sets for itself. */
|
|
36
42
|
const HOP_BY_HOP = new Set([
|
|
37
43
|
"host",
|
|
@@ -64,7 +70,7 @@ deliveries in flight are finished and acknowledged, and nothing unanswered is ac
|
|
|
64
70
|
The credential is read from the environment, never from the command line:
|
|
65
71
|
HUE_WORLD_TOKEN the world token, for a subscription on one world
|
|
66
72
|
HUE_CONNECTION_KEY the connection key, for a subscription on a connection key
|
|
67
|
-
A project key (HUE_API_KEY, HUE_MCP_KEY) is refused.
|
|
73
|
+
A project key (HUE_API_KEY, HUE_MCP_KEY, HUE_PROJECT_KEY or HUE_SERVICE_KEY) is refused.
|
|
68
74
|
|
|
69
75
|
Options:
|
|
70
76
|
--subscription ID The listen subscription to deliver (its id from Hue)
|
|
@@ -413,7 +419,7 @@ function selectCredential(env, choice) {
|
|
|
413
419
|
const credential = kind === "world-token" ? worldToken : connectionKey;
|
|
414
420
|
if (!credential)
|
|
415
421
|
throw new UsageError(`${variable} is not set`);
|
|
416
|
-
for (const projectVariable of
|
|
422
|
+
for (const projectVariable of HUE_CONTROL_PLANE_VARIABLES)
|
|
417
423
|
if (env[projectVariable]?.trim() === credential)
|
|
418
424
|
throw new UsageError(`${variable} holds the project key in ${projectVariable}; hue listen pulls only with the subscription's world token or connection key`);
|
|
419
425
|
if (kind === "world-token" && !WORLD_TOKEN.test(credential))
|
|
@@ -547,16 +553,18 @@ export async function runListenCommand(argv, io = {}) {
|
|
|
547
553
|
accept: "application/json",
|
|
548
554
|
"user-agent": `hue-sdk-typescript/${sdkVersion} hue-listen`,
|
|
549
555
|
};
|
|
550
|
-
// The first stop request stops pulling and lets deliveries in flight finish; a second one
|
|
551
|
-
// abandons their acknowledgements, whose leases then lapse into timeouts.
|
|
556
|
+
// The first stop request stops pulling and lets deliveries in flight finish; a second one, a
|
|
557
|
+
// second or more later, abandons their acknowledgements, whose leases then lapse into timeouts.
|
|
552
558
|
const stopping = new AbortController();
|
|
553
559
|
const forced = new AbortController();
|
|
560
|
+
let stoppedAt = 0;
|
|
554
561
|
const removeInterrupt = (io.onInterrupt ?? defaultOnInterrupt)(() => {
|
|
555
562
|
if (!stopping.signal.aborted) {
|
|
563
|
+
stoppedAt = performance.now();
|
|
556
564
|
stopping.abort();
|
|
557
565
|
warn("Stopping: finishing deliveries in flight (press Ctrl+C again to abandon them).");
|
|
558
566
|
}
|
|
559
|
-
else
|
|
567
|
+
else if (performance.now() - stoppedAt >= REPEATED_STOP_MS)
|
|
560
568
|
forced.abort();
|
|
561
569
|
});
|
|
562
570
|
const remembered = new Map();
|
|
@@ -736,6 +744,7 @@ export async function runListenCommand(argv, io = {}) {
|
|
|
736
744
|
}
|
|
737
745
|
}
|
|
738
746
|
};
|
|
747
|
+
/** Forwards and acknowledges one delivery; true when its request reached the receiver now. */
|
|
739
748
|
const deliver = async (delivery, deadline) => {
|
|
740
749
|
// Hue may hand out a delivery again (a pull whose answer was retried); its request already
|
|
741
750
|
// reached the receiver, so the recorded answer is repeated and nothing is sent twice.
|
|
@@ -751,7 +760,7 @@ export async function runListenCommand(argv, io = {}) {
|
|
|
751
760
|
: `${delivery.eventId ?? "event"}${delivery.retryNum ? ` retry ${delivery.retryNum}` : ""}`;
|
|
752
761
|
if (answer === null) {
|
|
753
762
|
out(`${new Date().toISOString()} ${name} -> abandoned; not acknowledged, the lease lapses into a timeout`);
|
|
754
|
-
return;
|
|
763
|
+
return false;
|
|
755
764
|
}
|
|
756
765
|
if (!previous) {
|
|
757
766
|
remembered.set(delivery.deliveryId, answer);
|
|
@@ -762,6 +771,7 @@ export async function runListenCommand(argv, io = {}) {
|
|
|
762
771
|
if (result.kind === "revoked")
|
|
763
772
|
fatal ??= `Subscription ${subscription} is revoked.`;
|
|
764
773
|
out(`${new Date().toISOString()} ${name}${previous ? " (repeated)" : ""} -> ${describeAnswer(answer)}; ${describeAck(result)}`);
|
|
774
|
+
return !previous;
|
|
765
775
|
};
|
|
766
776
|
out(`Forwarding subscription ${subscription} from ${foreign(origin)} to ${foreign(target.href)} using the ${label}. Press Ctrl+C to stop.`);
|
|
767
777
|
let failures = 0;
|
|
@@ -786,9 +796,7 @@ export async function runListenCommand(argv, io = {}) {
|
|
|
786
796
|
continue;
|
|
787
797
|
}
|
|
788
798
|
failures = 0;
|
|
789
|
-
|
|
790
|
-
if (ready && result.deliveries.length === 0 && performance.now() - pulledAt < 1_000)
|
|
791
|
-
await sleep(1_000, stopping.signal);
|
|
799
|
+
const waited = ready;
|
|
792
800
|
if (!ready) {
|
|
793
801
|
ready = true;
|
|
794
802
|
out("Ready: waiting for events.");
|
|
@@ -797,11 +805,26 @@ export async function runListenCommand(argv, io = {}) {
|
|
|
797
805
|
warn(`Skipped ${result.skipped} delivery(ies) this version of @hue-run/sdk cannot forward; their leases lapse and Hue retries them.`);
|
|
798
806
|
if (result.excess)
|
|
799
807
|
warn(`Hue leased ${result.excess} more delivery(ies) than --max ${max}; they are not forwarded, their leases lapse and Hue retries them.`);
|
|
800
|
-
await Promise.all(result.deliveries.map((delivery) => deliver(delivery, result.receivedAt + result.leaseLeftMs(delivery))));
|
|
808
|
+
const forwarded = await Promise.all(result.deliveries.map((delivery) => deliver(delivery, result.receivedAt + result.leaseLeftMs(delivery))));
|
|
809
|
+
// A pull that could wait but answered at once with nothing new to forward (none due, only
|
|
810
|
+
// deliveries already answered, or ones this client skips) is not repeated at once, so a
|
|
811
|
+
// Hue that hands them out again or refuses their acknowledgements is not pulled in a loop.
|
|
812
|
+
if (waited && !forwarded.includes(true) && performance.now() - pulledAt < IDLE_PULL_FLOOR_MS)
|
|
813
|
+
await sleep(IDLE_PULL_FLOOR_MS, stopping.signal);
|
|
801
814
|
}
|
|
802
815
|
}
|
|
803
816
|
finally {
|
|
804
|
-
|
|
817
|
+
// A repeated stop request can still arrive once the loop has ended (npm forwards Ctrl+C a few
|
|
818
|
+
// milliseconds late), and without a listener, or once the process has begun to exit, the
|
|
819
|
+
// runtime's default handler would end it by SIGINT. So the listener, and the process, stay
|
|
820
|
+
// until such a request would count as a new one: at most a second after the first.
|
|
821
|
+
const repeatable = stopping.signal.aborted
|
|
822
|
+
? REPEATED_STOP_MS - (performance.now() - stoppedAt)
|
|
823
|
+
: 0;
|
|
824
|
+
if (repeatable > 0)
|
|
825
|
+
setTimeout(removeInterrupt, repeatable);
|
|
826
|
+
else
|
|
827
|
+
removeInterrupt();
|
|
805
828
|
}
|
|
806
829
|
if (fatal !== null) {
|
|
807
830
|
warn(`Error: ${fatal}`);
|
package/dist/cli/mcp.d.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* `hue mcp install`: writes or prints the coding-agent configuration for Hue's MCP server. The
|
|
3
3
|
* shapes are those of Hue's published connection guide (https://docs.hue.run/agents/mcp-server),
|
|
4
4
|
* kept as a separate copy here. Key configurations reference the `HUE_MCP_KEY` environment
|
|
5
|
-
* variable (or a VS Code password input); sign-in configurations hold
|
|
6
|
-
* never written.
|
|
5
|
+
* variable (or a VS Code password input); sign-in configurations hold no credential. A key value
|
|
6
|
+
* is never written.
|
|
7
7
|
*/
|
|
8
8
|
/** Streams, environment and working directory for {@link runMcpCommand}; tests inject these. */
|
|
9
9
|
export interface McpCommandIo {
|
|
@@ -20,12 +20,17 @@ export declare const DEFAULT_MCP_URL = "https://mcp.hue.run/mcp";
|
|
|
20
20
|
* lists them, after which each call takes `project_id`. It reads what needs attention and falls
|
|
21
21
|
* back to recent traces, so a project without errors still proves the read path.
|
|
22
22
|
*/
|
|
23
|
-
export declare const MCP_VERIFY_PROMPT = "Use the Hue MCP: call list_projects and confirm which project to inspect. Then call get_project_context and show that project's traces from the last 24 hours that need attention or have errors, with links; if there are none, show its 5 most recent traces. For an organization connection, pass the project's id as project_id on each call after list_projects.";
|
|
24
|
-
/** Toolset names Hue's MCP server accepts: `all`, its catalog groups and
|
|
25
|
-
export declare const MCP_TOOLSETS: readonly ["all", "observe", "project", "traces", "
|
|
26
|
-
/**
|
|
23
|
+
export declare const MCP_VERIFY_PROMPT = "Use the Hue MCP: call list_projects and confirm which project to inspect. Then call get_project_context and show that project's traces from the last 24 hours that need attention or have errors, with links; if there are none, show its 5 most recent traces. For an organization connection, pass the project's id or slug as project_id on each call after list_projects.";
|
|
24
|
+
/** Toolset names Hue's MCP server accepts: `all`, its catalog groups and its curated profiles. */
|
|
25
|
+
export declare const MCP_TOOLSETS: readonly ["all", "observe", "author", "evaluate", "project", "traces", "eval_sets", "runs", "judges", "cases", "runners", "environments", "intents", "docs"];
|
|
26
|
+
/**
|
|
27
|
+
* The header form of `?toolsets=`. Only the sign-in Codex TOML uses it, as Hue's guide does, to
|
|
28
|
+
* keep that URL bare; every other configuration carries the selection in its URL.
|
|
29
|
+
*/
|
|
27
30
|
export declare const TOOLSETS_HEADER = "X-Hue-MCP-Toolsets";
|
|
28
|
-
|
|
31
|
+
/** The header form of `?project=`, used by the sign-in Codex TOML to keep its URL bare. */
|
|
32
|
+
export declare const PROJECT_HEADER = "X-Hue-MCP-Project";
|
|
33
|
+
export declare const MCP_USAGE = "Usage: hue mcp install --client <claude-code|codex|conductor|cursor|vscode|windsurf|gemini>\n [--auth key|oauth] [--read-only] [--project ID-OR-SLUG]\n [--toolsets NAMES] [--url URL]\n [--scope project|user] [--dry-run] [--print]\n\nConfigure a coding agent to use the Hue MCP server. A key configuration references the\nHUE_MCP_KEY environment variable; a key value is never written. A sign-in configuration holds\nonly the URL: the client opens Hue in a browser, where you approve access to the projects of\none organization.\n\nOptions:\n --client NAME Coding agent to configure (required)\n --auth MODE key: reference HUE_MCP_KEY (the default, except for conductor)\n oauth: URL only, sign in with Hue in the client (claude-code, codex and\n conductor; the default for conductor)\n --read-only key only: add ?read_only=true to the URL so write tools are hidden. A\n sign-in connection has Read and write access; use a Read key for read-only\n --project VALUE Pin the connection to one project by id or slug. Its tools omit project_id,\n and the server is named hue-<value> so it can coexist with an unpinned hue\n --toolsets NAMES\n Tools to list, comma-separated: all; the observe, author or evaluate\n profiles; or project, traces, eval_sets, runs, judges, cases, runners,\n environments, intents or docs. Added to the URL as ?toolsets=.\n Defaults: all for claude-code, codex and conductor,\n which search their own tools; observe for cursor. Without a selection\n Hue lists the production reads and search_hue_tools, which reaches\n every other tool\n --url URL Hue MCP endpoint (default https://mcp.hue.run/mcp)\n --scope SCOPE claude-code only: project writes .mcp.json (default); user runs\n `claude mcp add --scope user`\n --dry-run Print the resulting file content or commands without writing or running\n --print Print the configuration snippet only\n -h, --help Show this help\n\nFiles: claude-code .mcp.json, cursor .cursor/mcp.json, vscode .vscode/mcp.json (relative to the\ncurrent directory). codex and gemini use their own CLI when it is on PATH; windsurf prints the\nsnippet for its user configuration file. conductor registers the server for its Claude Code\n(user scope) and Codex agents with their CLIs, printing the command for a CLI not on PATH.";
|
|
29
34
|
/**
|
|
30
35
|
* A URL as a POSIX shell word for printed commands: `?` is a zsh glob and `&` ends a command, so a
|
|
31
36
|
* URL with a query (such as `?read_only=true`) is single-quoted. Executed commands pass argv.
|
|
@@ -35,7 +40,7 @@ export declare function shellWord(value: string): string;
|
|
|
35
40
|
* Canonical Hue client snippets for a key configuration; the JSON values are also the merge
|
|
36
41
|
* entries for config files.
|
|
37
42
|
*/
|
|
38
|
-
export declare function renderMcpSnippets(url: string): {
|
|
43
|
+
export declare function renderMcpSnippets(url: string, serverName?: string): {
|
|
39
44
|
claudeCodeServer: {
|
|
40
45
|
type: string;
|
|
41
46
|
url: string;
|
|
@@ -87,16 +92,14 @@ export declare function renderMcpSnippets(url: string): {
|
|
|
87
92
|
};
|
|
88
93
|
};
|
|
89
94
|
/**
|
|
90
|
-
* Sign-in snippets: the
|
|
91
|
-
* and
|
|
95
|
+
* Sign-in snippets: the server URL and nothing secret. The client discovers Hue's OAuth metadata
|
|
96
|
+
* from it and sends it, query included, on every request; the token's resource ignores the query,
|
|
97
|
+
* so `?toolsets=` and `?project=` select tools and a project here as they do for a key.
|
|
92
98
|
*/
|
|
93
|
-
export declare function renderMcpSignInSnippets(url: string,
|
|
99
|
+
export declare function renderMcpSignInSnippets(url: string, serverName?: string): {
|
|
94
100
|
claudeCodeServer: {
|
|
95
101
|
type: string;
|
|
96
102
|
url: string;
|
|
97
|
-
headers?: {
|
|
98
|
-
"X-Hue-MCP-Toolsets": string;
|
|
99
|
-
} | undefined;
|
|
100
103
|
};
|
|
101
104
|
claudeCodeProjectJson: string;
|
|
102
105
|
claudeCodeCli: {
|
|
@@ -111,7 +114,7 @@ export declare function renderMcpSignInSnippets(url: string, toolsets?: string):
|
|
|
111
114
|
};
|
|
112
115
|
/**
|
|
113
116
|
* Parses a comma-separated `--toolsets` value into Hue's canonical form, or returns an error.
|
|
114
|
-
* Unknown names are refused: Hue ignores them and would list
|
|
117
|
+
* Unknown names are refused: Hue ignores them and would list its default, hiding a typo.
|
|
115
118
|
*/
|
|
116
119
|
export declare function parseToolsets(value: string): {
|
|
117
120
|
toolsets: string;
|
|
@@ -119,10 +122,19 @@ export declare function parseToolsets(value: string): {
|
|
|
119
122
|
error: string;
|
|
120
123
|
};
|
|
121
124
|
/**
|
|
122
|
-
* `?toolsets=` lists only the selected tools
|
|
123
|
-
*
|
|
125
|
+
* `?toolsets=` lists only the selected tools; commas stay readable. Without a selection the
|
|
126
|
+
* parameter is removed, so the connection lists Hue's default.
|
|
124
127
|
*/
|
|
125
128
|
export declare function toolsetsMcpUrl(url: string, toolsets: string | undefined): string;
|
|
129
|
+
/** Parses the id or slug used to pin a connection and derives its coexisting server name. */
|
|
130
|
+
export declare function parseProject(value: string): {
|
|
131
|
+
project: string;
|
|
132
|
+
serverName: string;
|
|
133
|
+
} | {
|
|
134
|
+
error: string;
|
|
135
|
+
};
|
|
136
|
+
/** Replaces `?project=` while preserving the URL's other selections. */
|
|
137
|
+
export declare function projectMcpUrl(url: string, project: string | undefined): string;
|
|
126
138
|
/** `?read_only=true` hides and rejects Hue's write tools for any key. */
|
|
127
139
|
export declare function readOnlyMcpUrl(url: string): string;
|
|
128
140
|
/** Validates the MCP endpoint: HTTPS, or HTTP for loopback test servers; no credentials or hash. */
|
package/dist/cli/mcp.js
CHANGED
|
@@ -16,7 +16,7 @@ const MAX_CONFIG_BYTES = 1024 * 1024;
|
|
|
16
16
|
* lists them, after which each call takes `project_id`. It reads what needs attention and falls
|
|
17
17
|
* back to recent traces, so a project without errors still proves the read path.
|
|
18
18
|
*/
|
|
19
|
-
export const MCP_VERIFY_PROMPT = "Use the Hue MCP: call list_projects and confirm which project to inspect. Then call get_project_context and show that project's traces from the last 24 hours that need attention or have errors, with links; if there are none, show its 5 most recent traces. For an organization connection, pass the project's id as project_id on each call after list_projects.";
|
|
19
|
+
export const MCP_VERIFY_PROMPT = "Use the Hue MCP: call list_projects and confirm which project to inspect. Then call get_project_context and show that project's traces from the last 24 hours that need attention or have errors, with links; if there are none, show its 5 most recent traces. For an organization connection, pass the project's id or slug as project_id on each call after list_projects.";
|
|
20
20
|
const CLIENT_IDS = [
|
|
21
21
|
"claude-code",
|
|
22
22
|
"codex",
|
|
@@ -37,23 +37,48 @@ const CLIENT_LABELS = {
|
|
|
37
37
|
};
|
|
38
38
|
/** Clients whose browser sign-in with Hue works against the deployed server. */
|
|
39
39
|
const OAUTH_CLIENTS = ["claude-code", "codex", "conductor"];
|
|
40
|
-
/** Toolset names Hue's MCP server accepts: `all`, its catalog groups and
|
|
40
|
+
/** Toolset names Hue's MCP server accepts: `all`, its catalog groups and its curated profiles. */
|
|
41
41
|
export const MCP_TOOLSETS = [
|
|
42
42
|
"all",
|
|
43
43
|
"observe",
|
|
44
|
+
"author",
|
|
45
|
+
"evaluate",
|
|
44
46
|
"project",
|
|
45
47
|
"traces",
|
|
46
|
-
"
|
|
48
|
+
"eval_sets",
|
|
49
|
+
"runs",
|
|
50
|
+
"judges",
|
|
51
|
+
"cases",
|
|
52
|
+
"runners",
|
|
47
53
|
"environments",
|
|
48
54
|
"intents",
|
|
49
55
|
"docs",
|
|
50
56
|
];
|
|
51
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* The header form of `?toolsets=`. Only the sign-in Codex TOML uses it, as Hue's guide does, to
|
|
59
|
+
* keep that URL bare; every other configuration carries the selection in its URL.
|
|
60
|
+
*/
|
|
52
61
|
export const TOOLSETS_HEADER = "X-Hue-MCP-Toolsets";
|
|
53
|
-
/**
|
|
54
|
-
const
|
|
62
|
+
/** The header form of `?project=`, used by the sign-in Codex TOML to keep its URL bare. */
|
|
63
|
+
export const PROJECT_HEADER = "X-Hue-MCP-Project";
|
|
64
|
+
/**
|
|
65
|
+
* Toolsets a client's configuration selects unless `--toolsets` says otherwise. Claude Code,
|
|
66
|
+
* Codex and Conductor's agents defer MCP tools behind their own tool search, so listing every tool
|
|
67
|
+
* costs them little. Cursor caps the tools it loads, so it names the production reads. The other
|
|
68
|
+
* clients list Hue's default: the production reads, and `search_hue_tools` with its executors,
|
|
69
|
+
* which reach every other tool.
|
|
70
|
+
*/
|
|
71
|
+
const DEFAULT_TOOLSETS = {
|
|
72
|
+
"claude-code": "all",
|
|
73
|
+
codex: "all",
|
|
74
|
+
conductor: "all",
|
|
75
|
+
cursor: "observe",
|
|
76
|
+
};
|
|
77
|
+
/** Codex runs a server's tool calls one at a time without it; each Hue call is independent. */
|
|
78
|
+
const CODEX_PARALLEL = "supports_parallel_tool_calls = true";
|
|
55
79
|
export const MCP_USAGE = `Usage: hue mcp install --client <claude-code|codex|conductor|cursor|vscode|windsurf|gemini>
|
|
56
|
-
[--auth key|oauth] [--read-only] [--
|
|
80
|
+
[--auth key|oauth] [--read-only] [--project ID-OR-SLUG]
|
|
81
|
+
[--toolsets NAMES] [--url URL]
|
|
57
82
|
[--scope project|user] [--dry-run] [--print]
|
|
58
83
|
|
|
59
84
|
Configure a coding agent to use the Hue MCP server. A key configuration references the
|
|
@@ -68,11 +93,16 @@ Options:
|
|
|
68
93
|
conductor; the default for conductor)
|
|
69
94
|
--read-only key only: add ?read_only=true to the URL so write tools are hidden. A
|
|
70
95
|
sign-in connection has Read and write access; use a Read key for read-only
|
|
96
|
+
--project VALUE Pin the connection to one project by id or slug. Its tools omit project_id,
|
|
97
|
+
and the server is named hue-<value> so it can coexist with an unpinned hue
|
|
71
98
|
--toolsets NAMES
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
99
|
+
Tools to list, comma-separated: all; the observe, author or evaluate
|
|
100
|
+
profiles; or project, traces, eval_sets, runs, judges, cases, runners,
|
|
101
|
+
environments, intents or docs. Added to the URL as ?toolsets=.
|
|
102
|
+
Defaults: all for claude-code, codex and conductor,
|
|
103
|
+
which search their own tools; observe for cursor. Without a selection
|
|
104
|
+
Hue lists the production reads and search_hue_tools, which reaches
|
|
105
|
+
every other tool
|
|
76
106
|
--url URL Hue MCP endpoint (default ${DEFAULT_MCP_URL})
|
|
77
107
|
--scope SCOPE claude-code only: project writes .mcp.json (default); user runs
|
|
78
108
|
\`claude mcp add --scope user\`
|
|
@@ -96,7 +126,7 @@ export function shellWord(value) {
|
|
|
96
126
|
* Canonical Hue client snippets for a key configuration; the JSON values are also the merge
|
|
97
127
|
* entries for config files.
|
|
98
128
|
*/
|
|
99
|
-
export function renderMcpSnippets(url) {
|
|
129
|
+
export function renderMcpSnippets(url, serverName = SERVER_NAME) {
|
|
100
130
|
const bearer = (reference) => `Bearer ${reference}`;
|
|
101
131
|
const claudeCodeServer = {
|
|
102
132
|
type: "http",
|
|
@@ -121,7 +151,7 @@ export function renderMcpSnippets(url) {
|
|
|
121
151
|
};
|
|
122
152
|
return {
|
|
123
153
|
claudeCodeServer,
|
|
124
|
-
claudeCodeProjectJson: json({ mcpServers: { [
|
|
154
|
+
claudeCodeProjectJson: json({ mcpServers: { [serverName]: claudeCodeServer } }),
|
|
125
155
|
claudeCodeCli: {
|
|
126
156
|
args: [
|
|
127
157
|
"mcp",
|
|
@@ -130,25 +160,25 @@ export function renderMcpSnippets(url) {
|
|
|
130
160
|
"http",
|
|
131
161
|
"--scope",
|
|
132
162
|
"user",
|
|
133
|
-
|
|
163
|
+
serverName,
|
|
134
164
|
url,
|
|
135
165
|
"--header",
|
|
136
166
|
`Authorization: Bearer \${${ENV_VAR}}`,
|
|
137
167
|
],
|
|
138
|
-
display: `claude mcp add --transport http --scope user ${
|
|
168
|
+
display: `claude mcp add --transport http --scope user ${serverName} ${shellWord(url)} --header 'Authorization: Bearer \${${ENV_VAR}}'`,
|
|
139
169
|
},
|
|
140
170
|
cursorServer,
|
|
141
|
-
cursorJson: json({ mcpServers: { [
|
|
171
|
+
cursorJson: json({ mcpServers: { [serverName]: cursorServer } }),
|
|
142
172
|
codexCli: {
|
|
143
|
-
args: ["mcp", "add",
|
|
144
|
-
display: `codex mcp add ${
|
|
173
|
+
args: ["mcp", "add", serverName, "--url", url, "--bearer-token-env-var", ENV_VAR],
|
|
174
|
+
display: `codex mcp add ${serverName} --url ${shellWord(url)} --bearer-token-env-var ${ENV_VAR}`,
|
|
145
175
|
},
|
|
146
|
-
codexToml: `[mcp_servers.${
|
|
176
|
+
codexToml: `[mcp_servers.${serverName}]\nurl = "${url}"\nbearer_token_env_var = "${ENV_VAR}"\n${CODEX_PARALLEL}\n`,
|
|
147
177
|
vscodeServer,
|
|
148
178
|
vscodeInput,
|
|
149
|
-
vscodeJson: json({ servers: { [
|
|
179
|
+
vscodeJson: json({ servers: { [serverName]: vscodeServer }, inputs: [vscodeInput] }),
|
|
150
180
|
windsurfServer,
|
|
151
|
-
windsurfJson: json({ mcpServers: { [
|
|
181
|
+
windsurfJson: json({ mcpServers: { [serverName]: windsurfServer } }),
|
|
152
182
|
geminiCli: {
|
|
153
183
|
args: [
|
|
154
184
|
"mcp",
|
|
@@ -157,53 +187,49 @@ export function renderMcpSnippets(url) {
|
|
|
157
187
|
"user",
|
|
158
188
|
"--transport",
|
|
159
189
|
"http",
|
|
160
|
-
|
|
190
|
+
serverName,
|
|
161
191
|
url,
|
|
162
192
|
"--header",
|
|
163
193
|
`Authorization: Bearer \${${ENV_VAR}}`,
|
|
164
194
|
],
|
|
165
195
|
// Single quotes keep the reference literal when a person runs this in a shell where the
|
|
166
196
|
// key is exported; Gemini CLI expands ${HUE_MCP_KEY} from its settings at connection time.
|
|
167
|
-
display: `gemini mcp add --scope user --transport http ${
|
|
197
|
+
display: `gemini mcp add --scope user --transport http ${serverName} ${shellWord(url)} --header 'Authorization: Bearer \${${ENV_VAR}}'`,
|
|
168
198
|
},
|
|
169
199
|
};
|
|
170
200
|
}
|
|
171
201
|
/**
|
|
172
|
-
* Sign-in snippets: the
|
|
173
|
-
* and
|
|
202
|
+
* Sign-in snippets: the server URL and nothing secret. The client discovers Hue's OAuth metadata
|
|
203
|
+
* from it and sends it, query included, on every request; the token's resource ignores the query,
|
|
204
|
+
* so `?toolsets=` and `?project=` select tools and a project here as they do for a key.
|
|
174
205
|
*/
|
|
175
|
-
export function renderMcpSignInSnippets(url,
|
|
176
|
-
const
|
|
177
|
-
const
|
|
178
|
-
const
|
|
206
|
+
export function renderMcpSignInSnippets(url, serverName = SERVER_NAME) {
|
|
207
|
+
const parsed = new URL(url);
|
|
208
|
+
const toolsets = parsed.searchParams.get("toolsets");
|
|
209
|
+
const project = parsed.searchParams.get("project");
|
|
210
|
+
const headers = [
|
|
211
|
+
toolsets ? `"${TOOLSETS_HEADER}" = "${toolsets}"` : null,
|
|
212
|
+
project ? `"${PROJECT_HEADER}" = "${project}"` : null,
|
|
213
|
+
].filter((value) => value !== null);
|
|
214
|
+
const claudeCodeServer = { type: "http", url };
|
|
179
215
|
return {
|
|
180
216
|
claudeCodeServer,
|
|
181
|
-
claudeCodeProjectJson: json({ mcpServers: { [
|
|
217
|
+
claudeCodeProjectJson: json({ mcpServers: { [serverName]: claudeCodeServer } }),
|
|
182
218
|
claudeCodeCli: {
|
|
183
|
-
args: [
|
|
184
|
-
|
|
185
|
-
"add",
|
|
186
|
-
"--transport",
|
|
187
|
-
"http",
|
|
188
|
-
"--scope",
|
|
189
|
-
"user",
|
|
190
|
-
SERVER_NAME,
|
|
191
|
-
url,
|
|
192
|
-
...headerArgs,
|
|
193
|
-
],
|
|
194
|
-
display: `claude mcp add --transport http --scope user ${SERVER_NAME} ${shellWord(url)}${toolsets ? ` --header '${TOOLSETS_HEADER}: ${toolsets}'` : ""}`,
|
|
219
|
+
args: ["mcp", "add", "--transport", "http", "--scope", "user", serverName, url],
|
|
220
|
+
display: `claude mcp add --transport http --scope user ${serverName} ${shellWord(url)}`,
|
|
195
221
|
},
|
|
196
|
-
// `codex mcp add` cannot store a header; the TOML carries it and the next steps say so.
|
|
197
222
|
codexCli: {
|
|
198
|
-
args: ["mcp", "add",
|
|
199
|
-
display: `codex mcp add ${
|
|
223
|
+
args: ["mcp", "add", serverName, "--url", url],
|
|
224
|
+
display: `codex mcp add ${serverName} --url ${shellWord(url)}`,
|
|
200
225
|
},
|
|
201
|
-
|
|
226
|
+
// Header forms of the selections, for a TOML entry that keeps its URL bare.
|
|
227
|
+
codexToml: `[mcp_servers.${serverName}]\nurl = "${projectMcpUrl(toolsetsMcpUrl(url, undefined), undefined)}"\n${headers.length ? `http_headers = { ${headers.join(", ")} }\n` : ""}${CODEX_PARALLEL}\n`,
|
|
202
228
|
};
|
|
203
229
|
}
|
|
204
230
|
/**
|
|
205
231
|
* Parses a comma-separated `--toolsets` value into Hue's canonical form, or returns an error.
|
|
206
|
-
* Unknown names are refused: Hue ignores them and would list
|
|
232
|
+
* Unknown names are refused: Hue ignores them and would list its default, hiding a typo.
|
|
207
233
|
*/
|
|
208
234
|
export function parseToolsets(value) {
|
|
209
235
|
const names = [...new Set(value.split(",").map((name) => name.trim()))];
|
|
@@ -217,8 +243,8 @@ export function parseToolsets(value) {
|
|
|
217
243
|
return { toolsets: names.join(",") };
|
|
218
244
|
}
|
|
219
245
|
/**
|
|
220
|
-
* `?toolsets=` lists only the selected tools
|
|
221
|
-
*
|
|
246
|
+
* `?toolsets=` lists only the selected tools; commas stay readable. Without a selection the
|
|
247
|
+
* parameter is removed, so the connection lists Hue's default.
|
|
222
248
|
*/
|
|
223
249
|
export function toolsetsMcpUrl(url, toolsets) {
|
|
224
250
|
const parsed = new URL(url);
|
|
@@ -229,6 +255,25 @@ export function toolsetsMcpUrl(url, toolsets) {
|
|
|
229
255
|
parsed.search = `${query ? `${query}&` : ""}toolsets=${toolsets}`;
|
|
230
256
|
return parsed.href;
|
|
231
257
|
}
|
|
258
|
+
/** Parses the id or slug used to pin a connection and derives its coexisting server name. */
|
|
259
|
+
export function parseProject(value) {
|
|
260
|
+
const project = value.trim();
|
|
261
|
+
if (!project)
|
|
262
|
+
return { error: "--project is empty. Name a project id or slug, or remove the option." };
|
|
263
|
+
if (project.length > 256 || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/iu.test(project))
|
|
264
|
+
return {
|
|
265
|
+
error: "--project takes a project id or slug, using letters, numbers and hyphens.",
|
|
266
|
+
};
|
|
267
|
+
return { project, serverName: `${SERVER_NAME}-${project.toLowerCase()}` };
|
|
268
|
+
}
|
|
269
|
+
/** Replaces `?project=` while preserving the URL's other selections. */
|
|
270
|
+
export function projectMcpUrl(url, project) {
|
|
271
|
+
const parsed = new URL(url);
|
|
272
|
+
parsed.searchParams.delete("project");
|
|
273
|
+
if (project)
|
|
274
|
+
parsed.searchParams.append("project", project);
|
|
275
|
+
return parsed.href;
|
|
276
|
+
}
|
|
232
277
|
/** `?read_only=true` hides and rejects Hue's write tools for any key. */
|
|
233
278
|
export function readOnlyMcpUrl(url) {
|
|
234
279
|
const parsed = new URL(url);
|
|
@@ -244,15 +289,15 @@ class ConfigError extends Error {
|
|
|
244
289
|
function isRecord(value) {
|
|
245
290
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
246
291
|
}
|
|
247
|
-
/** Replaces only the
|
|
248
|
-
function mergeServerEntry(existing, key, entry, display) {
|
|
292
|
+
/** Replaces only the selected server entry under `key`, keeping every other server and field. */
|
|
293
|
+
function mergeServerEntry(existing, key, entry, display, serverName = SERVER_NAME) {
|
|
249
294
|
if (existing !== undefined && !isRecord(existing))
|
|
250
295
|
throw new ConfigError(`${display} must contain a JSON object.`);
|
|
251
296
|
const root = existing ?? {};
|
|
252
297
|
const servers = root[key];
|
|
253
298
|
if (servers !== undefined && !isRecord(servers))
|
|
254
299
|
throw new ConfigError(`${display}: "${key}" must be a JSON object.`);
|
|
255
|
-
return { ...root, [key]: { ...(servers ?? {}), [
|
|
300
|
+
return { ...root, [key]: { ...(servers ?? {}), [serverName]: entry } };
|
|
256
301
|
}
|
|
257
302
|
function mergeVscodeInput(root, input, display) {
|
|
258
303
|
const inputs = root.inputs;
|
|
@@ -390,6 +435,7 @@ function parseMcpArguments(argv) {
|
|
|
390
435
|
client: { type: "string" },
|
|
391
436
|
auth: { type: "string" },
|
|
392
437
|
"read-only": { type: "boolean", default: false },
|
|
438
|
+
project: { type: "string" },
|
|
393
439
|
toolsets: { type: "string" },
|
|
394
440
|
url: { type: "string" },
|
|
395
441
|
scope: { type: "string" },
|
|
@@ -405,10 +451,10 @@ function displayPath(cwd, path) {
|
|
|
405
451
|
}
|
|
406
452
|
const notOnPath = (executable, label) => `${executable} is not on PATH. Run this where ${label} is installed:`;
|
|
407
453
|
/** `auth: "oauth"` is only planned for {@link OAUTH_CLIENTS}; the caller refuses the others. */
|
|
408
|
-
function planFor(client, auth, scope, url,
|
|
409
|
-
const snippets = renderMcpSnippets(url);
|
|
454
|
+
function planFor(client, auth, scope, url, serverName) {
|
|
455
|
+
const snippets = renderMcpSnippets(url, serverName);
|
|
410
456
|
// Claude Code and Codex take the same plan shape with or without a key.
|
|
411
|
-
const shared = auth === "oauth" ? renderMcpSignInSnippets(url,
|
|
457
|
+
const shared = auth === "oauth" ? renderMcpSignInSnippets(url, serverName) : snippets;
|
|
412
458
|
const claudeUser = {
|
|
413
459
|
executable: "claude",
|
|
414
460
|
label: "Claude Code",
|
|
@@ -479,21 +525,23 @@ function planFor(client, auth, scope, url, toolsets) {
|
|
|
479
525
|
}
|
|
480
526
|
}
|
|
481
527
|
}
|
|
482
|
-
function nextSteps(client, auth,
|
|
528
|
+
function nextSteps(client, auth, serverName, project) {
|
|
483
529
|
const label = CLIENT_LABELS[client];
|
|
484
|
-
const approve =
|
|
530
|
+
const approve = project
|
|
531
|
+
? `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.`
|
|
532
|
+
: "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.";
|
|
485
533
|
let lines;
|
|
486
534
|
if (auth === "oauth")
|
|
487
535
|
lines =
|
|
488
536
|
client === "claude-code"
|
|
489
|
-
? [`In Claude Code, run /mcp, select ${
|
|
537
|
+
? [`In Claude Code, run /mcp, select ${serverName} and choose Authenticate. ${approve}`]
|
|
490
538
|
: client === "codex"
|
|
491
539
|
? [
|
|
492
|
-
`If Codex did not open Hue in your browser, run: codex mcp login ${
|
|
540
|
+
`If Codex did not open Hue in your browser, run: codex mcp login ${serverName}`,
|
|
493
541
|
approve,
|
|
494
542
|
]
|
|
495
543
|
: [
|
|
496
|
-
`In Conductor, open MCP status from the plug icon or /mcp-status, refresh, and use ${
|
|
544
|
+
`In Conductor, open MCP status from the plug icon or /mcp-status, refresh, and use ${serverName}'s authentication action. ${approve}`,
|
|
497
545
|
"Start a new agent session if the Hue tools do not appear. Hue has not yet verified signed-in Conductor end to end; if the tools still do not load, rerun with --auth key.",
|
|
498
546
|
];
|
|
499
547
|
else if (client === "vscode")
|
|
@@ -510,10 +558,15 @@ function nextSteps(client, auth, toolsets) {
|
|
|
510
558
|
" set -a; . ./.env.hue; set +a",
|
|
511
559
|
`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" : ""}.`,
|
|
512
560
|
];
|
|
513
|
-
// `codex mcp add`
|
|
514
|
-
if (
|
|
515
|
-
lines.push(`
|
|
516
|
-
|
|
561
|
+
// `codex mcp add` has no option for it, so a registered server gets the line by hand.
|
|
562
|
+
if (client === "codex" || client === "conductor")
|
|
563
|
+
lines.push(`If codex mcp add registered ${serverName}, add this line under [mcp_servers.${serverName}] in ~/.codex/config.toml so Codex runs Hue's tool calls in parallel:`, ` ${CODEX_PARALLEL}`);
|
|
564
|
+
if (project)
|
|
565
|
+
lines.push(`If this connection returns HTTP 404, ${project} is not an active project this credential can reach. Change or remove --project; signing in again will not fix it.`);
|
|
566
|
+
const verify = project
|
|
567
|
+
? `Use the Hue MCP: call list_projects and confirm it returns only the pinned project ${project} with connection.pinned true. Then call get_project_context and show that project's traces from the last 24 hours that need attention or have errors, with links; if there are none, show its 5 most recent traces. Do not pass project_id: this connection is pinned.`
|
|
568
|
+
: MCP_VERIFY_PROMPT;
|
|
569
|
+
return [...lines, "Then ask your agent:", ` ${verify}`];
|
|
517
570
|
}
|
|
518
571
|
/**
|
|
519
572
|
* Runs `hue mcp install` and returns the process exit code: 0 done or printed, 1 failed, 2 usage
|
|
@@ -570,11 +623,27 @@ export async function runMcpCommand(argv, io = {}) {
|
|
|
570
623
|
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);
|
|
571
624
|
if (parsed.values["read-only"])
|
|
572
625
|
url = readOnlyMcpUrl(url);
|
|
573
|
-
//
|
|
574
|
-
|
|
575
|
-
|
|
626
|
+
// --project replaces a pin already in --url. Without it, validate and keep the URL's pin.
|
|
627
|
+
const urlProjects = new URL(url).searchParams.getAll("project");
|
|
628
|
+
let projectValue = parsed.values.project;
|
|
629
|
+
if (projectValue === undefined && urlProjects.length) {
|
|
630
|
+
const trimmed = urlProjects.map((value) => value.trim());
|
|
631
|
+
if (trimmed.some((value) => !value) || new Set(trimmed).size !== 1)
|
|
632
|
+
return fail("In --url: ?project= must name one project id or slug. Remove empty or conflicting values.", 2);
|
|
633
|
+
projectValue = trimmed[0];
|
|
634
|
+
}
|
|
635
|
+
let project;
|
|
636
|
+
let serverName = SERVER_NAME;
|
|
637
|
+
if (projectValue !== undefined) {
|
|
638
|
+
const selected = parseProject(projectValue);
|
|
639
|
+
if ("error" in selected)
|
|
640
|
+
return fail(`${parsed.values.project === undefined ? "In --url: " : ""}${selected.error}\n\n${MCP_USAGE}`, 2);
|
|
641
|
+
project = selected.project;
|
|
642
|
+
serverName = selected.serverName;
|
|
643
|
+
}
|
|
644
|
+
url = projectMcpUrl(url, project);
|
|
576
645
|
// --toolsets wins over a selection already in --url, which wins over the client's default.
|
|
577
|
-
// Both sources are validated: Hue ignores an unknown name and would list
|
|
646
|
+
// Both sources are validated: Hue ignores an unknown name and would list its default.
|
|
578
647
|
const requested = parsed.values.toolsets ?? new URL(url).searchParams.get("toolsets");
|
|
579
648
|
let toolsets = DEFAULT_TOOLSETS[clientId];
|
|
580
649
|
if (requested !== null) {
|
|
@@ -583,17 +652,13 @@ export async function runMcpCommand(argv, io = {}) {
|
|
|
583
652
|
return fail(`${parsed.values.toolsets === undefined ? "In --url: " : ""}${selected.error}\n\n${MCP_USAGE}`, 2);
|
|
584
653
|
toolsets = selected.toolsets;
|
|
585
654
|
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
toolsets = undefined;
|
|
589
|
-
if (auth === "key")
|
|
590
|
-
url = toolsetsMcpUrl(url, toolsets);
|
|
591
|
-
const plan = planFor(clientId, auth, scope, url, auth === "oauth" ? toolsets : undefined);
|
|
655
|
+
url = toolsetsMcpUrl(url, toolsets);
|
|
656
|
+
const plan = planFor(clientId, auth, scope, url, serverName);
|
|
592
657
|
if (parsed.values.print) {
|
|
593
658
|
stdout.write(plan.snippet);
|
|
594
659
|
return 0;
|
|
595
660
|
}
|
|
596
|
-
const steps = nextSteps(clientId, auth,
|
|
661
|
+
const steps = nextSteps(clientId, auth, serverName, project);
|
|
597
662
|
if (plan.kind === "manual") {
|
|
598
663
|
out(plan.hint);
|
|
599
664
|
stdout.write(plan.snippet);
|
|
@@ -619,7 +684,7 @@ export async function runMcpCommand(argv, io = {}) {
|
|
|
619
684
|
out(`Running: ${command.display}`);
|
|
620
685
|
const code = await runClientCli(executable, command.args, { cwd, env, stdout, stderr });
|
|
621
686
|
if (code === 0) {
|
|
622
|
-
out(`Registered the "${
|
|
687
|
+
out(`Registered the "${serverName}" MCP server (${url}) with ${command.label}.`);
|
|
623
688
|
continue;
|
|
624
689
|
}
|
|
625
690
|
failed = true;
|
|
@@ -636,7 +701,7 @@ export async function runMcpCommand(argv, io = {}) {
|
|
|
636
701
|
let content;
|
|
637
702
|
try {
|
|
638
703
|
const existing = await readJsonConfig(path, display);
|
|
639
|
-
let merged = mergeServerEntry(existing, plan.key, plan.entry, display);
|
|
704
|
+
let merged = mergeServerEntry(existing, plan.key, plan.entry, display, serverName);
|
|
640
705
|
if (plan.input)
|
|
641
706
|
merged = mergeVscodeInput(merged, plan.input, display);
|
|
642
707
|
content = json(merged);
|
|
@@ -657,7 +722,7 @@ export async function runMcpCommand(argv, io = {}) {
|
|
|
657
722
|
catch (error) {
|
|
658
723
|
return fail(`Could not write ${display}: ${error.message}`);
|
|
659
724
|
}
|
|
660
|
-
out(`Wrote ${display} with the "${
|
|
725
|
+
out(`Wrote ${display} with the "${serverName}" MCP server (${url}).`);
|
|
661
726
|
for (const line of steps)
|
|
662
727
|
out(line);
|
|
663
728
|
return 0;
|
package/dist/evals/client.d.ts
CHANGED
|
@@ -44,7 +44,8 @@ export declare class EvaluationClient {
|
|
|
44
44
|
private readonly timeoutMillis;
|
|
45
45
|
constructor(options: EvaluationClientOptions);
|
|
46
46
|
/** Sends one request to Hue, again after the wait while Hue refuses it with a short
|
|
47
|
-
* `Retry-After`. `init` runs for every attempt, so each has its own timeout
|
|
47
|
+
* `Retry-After`. `init` runs for every attempt, so each has its own timeout; `signal` also ends
|
|
48
|
+
* each attempt, and a wait to send one again, whose refusal is then the caller's error. */
|
|
48
49
|
private send;
|
|
49
50
|
private request;
|
|
50
51
|
/** Verified bytes of one ready artifact in this project, bounded to the 25 MiB pilot file size.
|
|
@@ -170,8 +171,10 @@ export declare class EvaluationClient {
|
|
|
170
171
|
revokeAttemptConnection(input: {
|
|
171
172
|
bindingId: string;
|
|
172
173
|
}): Promise<import("./attempt.js").RevokeAttemptResult>;
|
|
173
|
-
/** Reads one artifact reservation and its verification state. */
|
|
174
|
-
getArtifact(id: string
|
|
174
|
+
/** Reads one artifact reservation and its verification state. `signal` ends the request. */
|
|
175
|
+
getArtifact(id: string, options?: {
|
|
176
|
+
signal?: AbortSignal;
|
|
177
|
+
}): Promise<ArtifactReservation>;
|
|
175
178
|
/** Reserves an artifact by declared identity; replaying the key returns the same reservation. */
|
|
176
179
|
reserveArtifact(input: {
|
|
177
180
|
/** Stable key; replaying it returns the same reservation. */
|
|
@@ -187,8 +190,11 @@ export declare class EvaluationClient {
|
|
|
187
190
|
}): Promise<ArtifactReservation>;
|
|
188
191
|
/** Issues a short-lived storage capability for staging the reserved artifact's bytes. */
|
|
189
192
|
requestArtifactUpload(id: string): Promise<ArtifactUpload>;
|
|
190
|
-
/** Asks Hue to verify the staged bytes against the declared identity.
|
|
191
|
-
|
|
193
|
+
/** Asks Hue to verify the staged bytes against the declared identity. `signal` ends the
|
|
194
|
+
* request, and a wait to send it again. */
|
|
195
|
+
completeArtifact(id: string, options?: {
|
|
196
|
+
signal?: AbortSignal;
|
|
197
|
+
}): Promise<ArtifactReservation>;
|
|
192
198
|
/** Saves an execution's outcome and creates its immutable subject. */
|
|
193
199
|
completeExecution(id: string, input: CompleteExecution): Promise<Completion>;
|
|
194
200
|
/** Marks an experiment finished. */
|
package/dist/evals/client.js
CHANGED
|
@@ -149,12 +149,15 @@ export class EvaluationClient {
|
|
|
149
149
|
this.timeoutMillis = validated.timeoutMillis;
|
|
150
150
|
}
|
|
151
151
|
/** Sends one request to Hue, again after the wait while Hue refuses it with a short
|
|
152
|
-
* `Retry-After`. `init` runs for every attempt, so each has its own timeout
|
|
153
|
-
|
|
152
|
+
* `Retry-After`. `init` runs for every attempt, so each has its own timeout; `signal` also ends
|
|
153
|
+
* each attempt, and a wait to send one again, whose refusal is then the caller's error. */
|
|
154
|
+
async send(url, init, signal) {
|
|
154
155
|
for (let attempt = 0;; attempt++) {
|
|
155
156
|
let response;
|
|
156
157
|
try {
|
|
157
|
-
|
|
158
|
+
const attemptInit = init();
|
|
159
|
+
const signals = [attemptInit.signal, signal].filter((item) => !!item);
|
|
160
|
+
response = await fetch(url, { ...attemptInit, signal: AbortSignal.any(signals) });
|
|
158
161
|
}
|
|
159
162
|
catch {
|
|
160
163
|
throw new HueApiError();
|
|
@@ -164,10 +167,23 @@ export class EvaluationClient {
|
|
|
164
167
|
return response;
|
|
165
168
|
await response.body?.cancel().catch(() => undefined);
|
|
166
169
|
// Never sooner than asked; the jitter spreads out parallel requests refused together.
|
|
167
|
-
await new Promise((resolve) =>
|
|
170
|
+
await new Promise((resolve) => {
|
|
171
|
+
const done = () => {
|
|
172
|
+
clearTimeout(timer);
|
|
173
|
+
signal?.removeEventListener("abort", done);
|
|
174
|
+
resolve();
|
|
175
|
+
};
|
|
176
|
+
const timer = setTimeout(done, seconds * 1000 * (1 + Math.random() * 0.5));
|
|
177
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
178
|
+
// The signal may have ended while the refusal's body was cancelled.
|
|
179
|
+
if (signal?.aborted)
|
|
180
|
+
done();
|
|
181
|
+
});
|
|
182
|
+
if (signal?.aborted)
|
|
183
|
+
throw new HueApiError(response.status, askedRetryAfter(response));
|
|
168
184
|
}
|
|
169
185
|
}
|
|
170
|
-
async request(method, path, body, bounds = { ...valueBounds, bytes: 1024 * 1024 }) {
|
|
186
|
+
async request(method, path, body, bounds = { ...valueBounds, bytes: 1024 * 1024 }, signal) {
|
|
171
187
|
// Optional top-level fields are omitted intentionally; nested undefined remains invalid.
|
|
172
188
|
const payload = body === undefined
|
|
173
189
|
? undefined
|
|
@@ -181,7 +197,7 @@ export class EvaluationClient {
|
|
|
181
197
|
body: payload,
|
|
182
198
|
redirect: "error",
|
|
183
199
|
signal: AbortSignal.timeout(this.timeoutMillis),
|
|
184
|
-
}));
|
|
200
|
+
}), signal);
|
|
185
201
|
if (!response.ok) {
|
|
186
202
|
await response.body?.cancel();
|
|
187
203
|
throw new HueApiError(response.status, askedRetryAfter(response));
|
|
@@ -503,9 +519,9 @@ export class EvaluationClient {
|
|
|
503
519
|
throw new HueApiError();
|
|
504
520
|
}
|
|
505
521
|
}
|
|
506
|
-
/** Reads one artifact reservation and its verification state. */
|
|
507
|
-
getArtifact(id) {
|
|
508
|
-
return this.request("GET", `/artifacts/${uuid(id)}
|
|
522
|
+
/** Reads one artifact reservation and its verification state. `signal` ends the request. */
|
|
523
|
+
getArtifact(id, options = {}) {
|
|
524
|
+
return this.request("GET", `/artifacts/${uuid(id)}`, undefined, undefined, options.signal);
|
|
509
525
|
}
|
|
510
526
|
/** Reserves an artifact by declared identity; replaying the key returns the same reservation. */
|
|
511
527
|
reserveArtifact(input) {
|
|
@@ -515,9 +531,10 @@ export class EvaluationClient {
|
|
|
515
531
|
requestArtifactUpload(id) {
|
|
516
532
|
return this.request("POST", `/artifacts/${uuid(id)}/upload`, {});
|
|
517
533
|
}
|
|
518
|
-
/** Asks Hue to verify the staged bytes against the declared identity.
|
|
519
|
-
|
|
520
|
-
|
|
534
|
+
/** Asks Hue to verify the staged bytes against the declared identity. `signal` ends the
|
|
535
|
+
* request, and a wait to send it again. */
|
|
536
|
+
completeArtifact(id, options = {}) {
|
|
537
|
+
return this.request("POST", `/artifacts/${uuid(id)}/complete`, {}, undefined, options.signal);
|
|
521
538
|
}
|
|
522
539
|
/** Saves an execution's outcome and creates its immutable subject. */
|
|
523
540
|
completeExecution(id, input) {
|
package/dist/evals/files.js
CHANGED
|
@@ -339,21 +339,33 @@ function mayStillVerify(error) {
|
|
|
339
339
|
* a pause: ready is returned; verifying is completed again, which Hue refuses while its lease
|
|
340
340
|
* holds and restarts once it lapsed; an artifact a retryable refusal (a lost response, 429 or 503)
|
|
341
341
|
* left unverified is completed again up to three times; any other state is returned for the
|
|
342
|
-
* caller to refuse (a resume uploads a rejected artifact again). All within `timing.settleMillis
|
|
342
|
+
* caller to refuse (a resume uploads a rejected artifact again). All within `timing.settleMillis`,
|
|
343
|
+
* which also ends a request in flight and the client's own wait to send one again.
|
|
343
344
|
*/
|
|
344
345
|
async function settleArtifact(client, id, timing) {
|
|
345
346
|
const deadline = Date.now() + timing.settleMillis;
|
|
347
|
+
const signal = AbortSignal.timeout(timing.settleMillis);
|
|
348
|
+
// The last state read, which names the state when the deadline cuts off a request at it.
|
|
349
|
+
let known;
|
|
350
|
+
const timedOut = (last) => {
|
|
351
|
+
const seconds = Math.round(timing.settleMillis / 1000);
|
|
352
|
+
return new Error(last?.state === "verifying"
|
|
353
|
+
? `Hue was still verifying generated file ${id} after ${seconds} seconds`
|
|
354
|
+
: `Hue had not verified generated file ${id} after ${seconds} seconds (${last ? `it was ${last.state}` : "its state could not be read"})`);
|
|
355
|
+
};
|
|
346
356
|
let pause = timing.pollMillis;
|
|
347
357
|
let retries = 0;
|
|
348
358
|
for (;;) {
|
|
349
359
|
let refusedForNow;
|
|
350
360
|
let asked = 0;
|
|
351
361
|
try {
|
|
352
|
-
return (await client.completeArtifact(id)).state;
|
|
362
|
+
return (await client.completeArtifact(id, { signal })).state;
|
|
353
363
|
}
|
|
354
364
|
catch (error) {
|
|
355
|
-
if (!mayStillVerify(error)
|
|
365
|
+
if (!mayStillVerify(error))
|
|
356
366
|
throw error;
|
|
367
|
+
if (Date.now() >= deadline)
|
|
368
|
+
throw timedOut(known);
|
|
357
369
|
refusedForNow = error.status !== 409;
|
|
358
370
|
// A refusal that says how long to wait is waited out, within the settling window.
|
|
359
371
|
asked = (error.retryAfterSeconds ?? 0) * 1000;
|
|
@@ -362,7 +374,7 @@ async function settleArtifact(client, id, timing) {
|
|
|
362
374
|
pause = Math.min(pause * 2, timing.maxPollMillis);
|
|
363
375
|
let current;
|
|
364
376
|
try {
|
|
365
|
-
current = await client.getArtifact(id);
|
|
377
|
+
current = known = await client.getArtifact(id, { signal });
|
|
366
378
|
}
|
|
367
379
|
catch (error) {
|
|
368
380
|
if (!mayStillVerify(error))
|
|
@@ -375,12 +387,8 @@ async function settleArtifact(client, id, timing) {
|
|
|
375
387
|
current.failureCode !== "mismatch";
|
|
376
388
|
if (current && current.state !== "verifying" && !(unverified && refusedForNow && retries++ < 3))
|
|
377
389
|
return current.state;
|
|
378
|
-
if (Date.now() >= deadline)
|
|
379
|
-
|
|
380
|
-
throw new Error(current?.state === "verifying"
|
|
381
|
-
? `Hue was still verifying generated file ${id} after ${seconds} seconds`
|
|
382
|
-
: `Hue had not verified generated file ${id} after ${seconds} seconds (${current ? `it was ${current.state}` : "its state could not be read"})`);
|
|
383
|
-
}
|
|
390
|
+
if (Date.now() >= deadline)
|
|
391
|
+
throw timedOut(current ?? known);
|
|
384
392
|
}
|
|
385
393
|
}
|
|
386
394
|
/**
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Package version shared by the instrumentation scope and the export User-Agent. */
|
|
2
|
-
export declare const sdkVersion = "0.11.
|
|
2
|
+
export declare const sdkVersion = "0.11.2";
|
package/dist/version.js
CHANGED