@hue-run/sdk 0.4.2 → 0.5.1

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.
@@ -0,0 +1,76 @@
1
+ /**
2
+ * `hue mcp install`: writes or prints the coding-agent configuration for Hue's MCP server. Every
3
+ * shape mirrors the snippets Hue shows in Settings and references the `HUE_MCP_KEY` environment
4
+ * variable (or a VS Code password input); a key value is never written.
5
+ */
6
+ /** Streams, environment and working directory for {@link runMcpCommand}; tests inject these. */
7
+ export interface McpCommandIo {
8
+ stdin?: NodeJS.ReadableStream;
9
+ stdout?: NodeJS.WritableStream;
10
+ stderr?: NodeJS.WritableStream;
11
+ env?: NodeJS.ProcessEnv;
12
+ cwd?: string;
13
+ }
14
+ export declare const DEFAULT_MCP_URL = "https://mcp.hue.run/mcp";
15
+ /** Prompt to paste into the agent after installation; identical to the one Hue shows. */
16
+ export declare const MCP_VERIFY_PROMPT = "Use the Hue MCP: call get_project_context, then show my 5 most recent error traces with links.";
17
+ export declare const MCP_USAGE = "Usage: hue mcp install --client <claude-code|cursor|codex|vscode|windsurf|gemini>\n [--url URL] [--scope project|user] [--dry-run] [--print]\n\nConfigure a coding agent to use the Hue MCP server. The configuration references the\nHUE_MCP_KEY environment variable; a key value is never written.\n\nOptions:\n --client NAME Coding agent to configure (required)\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 command 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.";
18
+ /** Canonical Hue client snippets; the JSON values are also the merge entries for config files. */
19
+ export declare function renderMcpSnippets(url: string): {
20
+ claudeCodeServer: {
21
+ type: string;
22
+ url: string;
23
+ headers: {
24
+ Authorization: string;
25
+ };
26
+ };
27
+ claudeCodeProjectJson: string;
28
+ claudeCodeCli: {
29
+ args: string[];
30
+ display: string;
31
+ };
32
+ cursorServer: {
33
+ url: string;
34
+ headers: {
35
+ Authorization: string;
36
+ };
37
+ };
38
+ cursorJson: string;
39
+ codexCli: {
40
+ args: string[];
41
+ display: string;
42
+ };
43
+ codexToml: string;
44
+ vscodeServer: {
45
+ type: string;
46
+ url: string;
47
+ headers: {
48
+ Authorization: string;
49
+ };
50
+ };
51
+ vscodeInput: {
52
+ type: string;
53
+ id: string;
54
+ description: string;
55
+ password: boolean;
56
+ };
57
+ vscodeJson: string;
58
+ windsurfServer: {
59
+ serverUrl: string;
60
+ headers: {
61
+ Authorization: string;
62
+ };
63
+ };
64
+ windsurfJson: string;
65
+ geminiCli: {
66
+ args: string[];
67
+ display: string;
68
+ };
69
+ };
70
+ /** Validates the MCP endpoint: HTTPS, or HTTP for loopback test servers; no credentials or hash. */
71
+ export declare function parseMcpUrl(value: string): string | null;
72
+ /**
73
+ * Runs `hue mcp install` and returns the process exit code: 0 done or printed, 1 failed, 2 usage
74
+ * error. `argv` may start with the `mcp` command word.
75
+ */
76
+ export declare function runMcpCommand(argv: string[], io?: McpCommandIo): Promise<number>;
@@ -0,0 +1,466 @@
1
+ import { spawn } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { constants } from "node:fs";
4
+ import { access, chmod, lstat, mkdir, open, readFile, rename, stat, unlink, } from "node:fs/promises";
5
+ import { basename, delimiter, dirname, isAbsolute, join, relative, resolve } from "node:path";
6
+ import { parseArgs } from "node:util";
7
+ import { isLoopbackHost } from "../config.js";
8
+ export const DEFAULT_MCP_URL = "https://mcp.hue.run/mcp";
9
+ const SERVER_NAME = "hue";
10
+ const ENV_VAR = "HUE_MCP_KEY";
11
+ const INPUT_ID = `${SERVER_NAME}-mcp-key`;
12
+ const MAX_CONFIG_BYTES = 1024 * 1024;
13
+ /** Prompt to paste into the agent after installation; identical to the one Hue shows. */
14
+ export const MCP_VERIFY_PROMPT = "Use the Hue MCP: call get_project_context, then show my 5 most recent error traces with links.";
15
+ const CLIENT_IDS = ["claude-code", "cursor", "codex", "vscode", "windsurf", "gemini"];
16
+ const CLIENT_LABELS = {
17
+ "claude-code": "Claude Code",
18
+ cursor: "Cursor",
19
+ codex: "Codex",
20
+ vscode: "VS Code",
21
+ windsurf: "Windsurf",
22
+ gemini: "Gemini CLI",
23
+ };
24
+ export const MCP_USAGE = `Usage: hue mcp install --client <claude-code|cursor|codex|vscode|windsurf|gemini>
25
+ [--url URL] [--scope project|user] [--dry-run] [--print]
26
+
27
+ Configure a coding agent to use the Hue MCP server. The configuration references the
28
+ ${ENV_VAR} environment variable; a key value is never written.
29
+
30
+ Options:
31
+ --client NAME Coding agent to configure (required)
32
+ --url URL Hue MCP endpoint (default ${DEFAULT_MCP_URL})
33
+ --scope SCOPE claude-code only: project writes .mcp.json (default); user runs
34
+ \`claude mcp add --scope user\`
35
+ --dry-run Print the resulting file content or command without writing or running
36
+ --print Print the configuration snippet only
37
+ -h, --help Show this help
38
+
39
+ Files: claude-code .mcp.json, cursor .cursor/mcp.json, vscode .vscode/mcp.json (relative to the
40
+ current directory). codex and gemini use their own CLI when it is on PATH; windsurf prints the
41
+ snippet for its user configuration file.`;
42
+ const json = (value) => `${JSON.stringify(value, null, 2)}\n`;
43
+ /** Canonical Hue client snippets; the JSON values are also the merge entries for config files. */
44
+ export function renderMcpSnippets(url) {
45
+ const bearer = (reference) => `Bearer ${reference}`;
46
+ const claudeCodeServer = {
47
+ type: "http",
48
+ url,
49
+ headers: { Authorization: bearer(`\${${ENV_VAR}}`) },
50
+ };
51
+ const cursorServer = { url, headers: { Authorization: bearer(`\${env:${ENV_VAR}}`) } };
52
+ const vscodeServer = {
53
+ type: "http",
54
+ url,
55
+ headers: { Authorization: bearer(`\${input:${INPUT_ID}}`) },
56
+ };
57
+ const vscodeInput = {
58
+ type: "promptString",
59
+ id: INPUT_ID,
60
+ description: "Hue coding-agent key",
61
+ password: true,
62
+ };
63
+ const windsurfServer = {
64
+ serverUrl: url,
65
+ headers: { Authorization: bearer(`\${env:${ENV_VAR}}`) },
66
+ };
67
+ return {
68
+ claudeCodeServer,
69
+ claudeCodeProjectJson: json({ mcpServers: { [SERVER_NAME]: claudeCodeServer } }),
70
+ claudeCodeCli: {
71
+ args: [
72
+ "mcp",
73
+ "add",
74
+ "--transport",
75
+ "http",
76
+ "--scope",
77
+ "user",
78
+ SERVER_NAME,
79
+ url,
80
+ "--header",
81
+ `Authorization: Bearer \${${ENV_VAR}}`,
82
+ ],
83
+ display: `claude mcp add --transport http --scope user ${SERVER_NAME} ${url} --header 'Authorization: Bearer \${${ENV_VAR}}'`,
84
+ },
85
+ cursorServer,
86
+ cursorJson: json({ mcpServers: { [SERVER_NAME]: cursorServer } }),
87
+ codexCli: {
88
+ args: ["mcp", "add", SERVER_NAME, "--url", url, "--bearer-token-env-var", ENV_VAR],
89
+ display: `codex mcp add ${SERVER_NAME} --url ${url} --bearer-token-env-var ${ENV_VAR}`,
90
+ },
91
+ codexToml: `[mcp_servers.${SERVER_NAME}]\nurl = "${url}"\nbearer_token_env_var = "${ENV_VAR}"\n`,
92
+ vscodeServer,
93
+ vscodeInput,
94
+ vscodeJson: json({ servers: { [SERVER_NAME]: vscodeServer }, inputs: [vscodeInput] }),
95
+ windsurfServer,
96
+ windsurfJson: json({ mcpServers: { [SERVER_NAME]: windsurfServer } }),
97
+ geminiCli: {
98
+ args: [
99
+ "mcp",
100
+ "add",
101
+ "--transport",
102
+ "http",
103
+ SERVER_NAME,
104
+ url,
105
+ "-H",
106
+ `Authorization: Bearer $${ENV_VAR}`,
107
+ ],
108
+ // Single quotes keep the reference literal when a person runs this in a shell where the
109
+ // key is exported; Gemini CLI resolves $HUE_MCP_KEY from its settings at runtime.
110
+ display: `gemini mcp add --transport http ${SERVER_NAME} ${url} -H 'Authorization: Bearer $${ENV_VAR}'`,
111
+ },
112
+ };
113
+ }
114
+ class ConfigError extends Error {
115
+ constructor(message) {
116
+ super(message);
117
+ this.name = "ConfigError";
118
+ }
119
+ }
120
+ function isRecord(value) {
121
+ return typeof value === "object" && value !== null && !Array.isArray(value);
122
+ }
123
+ /** Replaces only the `hue` entry under `key`, keeping every other server and top-level field. */
124
+ function mergeServerEntry(existing, key, entry, display) {
125
+ if (existing !== undefined && !isRecord(existing))
126
+ throw new ConfigError(`${display} must contain a JSON object.`);
127
+ const root = existing ?? {};
128
+ const servers = root[key];
129
+ if (servers !== undefined && !isRecord(servers))
130
+ throw new ConfigError(`${display}: "${key}" must be a JSON object.`);
131
+ return { ...root, [key]: { ...(servers ?? {}), [SERVER_NAME]: entry } };
132
+ }
133
+ function mergeVscodeInput(root, input, display) {
134
+ const inputs = root.inputs;
135
+ if (inputs !== undefined && !Array.isArray(inputs))
136
+ throw new ConfigError(`${display}: "inputs" must be a JSON array.`);
137
+ const others = (inputs ?? []).filter((item) => !(isRecord(item) && item.id === INPUT_ID));
138
+ return { ...root, inputs: [...others, input] };
139
+ }
140
+ async function readJsonConfig(path, display) {
141
+ let info;
142
+ try {
143
+ info = await lstat(path);
144
+ }
145
+ catch (error) {
146
+ if (error.code === "ENOENT")
147
+ return undefined;
148
+ throw new ConfigError(`Cannot read ${display}: ${error.message}`);
149
+ }
150
+ if (info.isSymbolicLink())
151
+ throw new ConfigError(`Refusing to use ${display}: it is a symbolic link.`);
152
+ if (!info.isFile())
153
+ throw new ConfigError(`Refusing to use ${display}: it is not a regular file.`);
154
+ if (info.size > MAX_CONFIG_BYTES)
155
+ throw new ConfigError(`Refusing to use ${display}: it is larger than 1 MiB.`);
156
+ const text = await readFile(path, "utf8");
157
+ if (!text.trim())
158
+ return undefined;
159
+ try {
160
+ return JSON.parse(text);
161
+ }
162
+ catch {
163
+ throw new ConfigError(`${display} is not valid JSON (comments are not supported); fix it or add the snippet by hand.`);
164
+ }
165
+ }
166
+ async function rejectSymlink(path, display) {
167
+ try {
168
+ if ((await lstat(path)).isSymbolicLink())
169
+ throw new ConfigError(`Refusing to write ${display}: it is a symbolic link.`);
170
+ }
171
+ catch (error) {
172
+ if (error.code === "ENOENT")
173
+ return;
174
+ throw error;
175
+ }
176
+ }
177
+ /** Atomic write for a secret-free config file: temporary file, fsync, rename; mode 0644. */
178
+ async function writeConfigFile(path, text, display) {
179
+ await mkdir(dirname(path), { recursive: true });
180
+ await rejectSymlink(path, display);
181
+ const temporary = join(dirname(path), `.${basename(path)}.${randomUUID()}.tmp`);
182
+ try {
183
+ const handle = await open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o644);
184
+ try {
185
+ await handle.writeFile(text, "utf8");
186
+ await handle.sync();
187
+ }
188
+ finally {
189
+ await handle.close();
190
+ }
191
+ await chmod(temporary, 0o644);
192
+ await rejectSymlink(path, display);
193
+ await rename(temporary, path);
194
+ }
195
+ catch (error) {
196
+ await unlink(temporary).catch(() => undefined);
197
+ throw error;
198
+ }
199
+ }
200
+ async function findExecutable(name, env) {
201
+ const searchPath = env.PATH ?? env.Path ?? "";
202
+ const extensions = process.platform === "win32" ? (env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";") : [""];
203
+ for (const directory of searchPath.split(delimiter)) {
204
+ if (!directory)
205
+ continue;
206
+ for (const extension of extensions) {
207
+ const candidate = join(directory, `${name}${extension.toLowerCase()}`);
208
+ try {
209
+ if (!(await stat(candidate)).isFile())
210
+ continue;
211
+ if (process.platform !== "win32")
212
+ await access(candidate, constants.X_OK);
213
+ return candidate;
214
+ }
215
+ catch {
216
+ continue;
217
+ }
218
+ }
219
+ }
220
+ return null;
221
+ }
222
+ /** Runs a client CLI without a shell, forwarding its output; null when it could not start. */
223
+ function runClientCli(executable, args, options) {
224
+ return new Promise((resolveRun) => {
225
+ try {
226
+ const child = spawn(executable, args, {
227
+ cwd: options.cwd,
228
+ env: options.env,
229
+ stdio: ["ignore", "pipe", "pipe"],
230
+ });
231
+ child.stdout.on("data", (chunk) => {
232
+ options.stdout.write(chunk);
233
+ });
234
+ child.stderr.on("data", (chunk) => {
235
+ options.stderr.write(chunk);
236
+ });
237
+ child.once("error", () => resolveRun(null));
238
+ child.once("close", (code) => resolveRun(code));
239
+ }
240
+ catch {
241
+ resolveRun(null);
242
+ }
243
+ });
244
+ }
245
+ /** Validates the MCP endpoint: HTTPS, or HTTP for loopback test servers; no credentials or hash. */
246
+ export function parseMcpUrl(value) {
247
+ let url;
248
+ try {
249
+ url = new URL(value);
250
+ }
251
+ catch {
252
+ return null;
253
+ }
254
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && isLoopbackHost(url.hostname)))
255
+ return null;
256
+ if (url.username || url.password || url.hash)
257
+ return null;
258
+ return url.href;
259
+ }
260
+ function parseMcpArguments(argv) {
261
+ return parseArgs({
262
+ args: argv,
263
+ allowPositionals: true,
264
+ strict: true,
265
+ options: {
266
+ client: { type: "string" },
267
+ url: { type: "string" },
268
+ scope: { type: "string" },
269
+ "dry-run": { type: "boolean", default: false },
270
+ print: { type: "boolean", default: false },
271
+ help: { type: "boolean", short: "h", default: false },
272
+ },
273
+ });
274
+ }
275
+ function displayPath(cwd, path) {
276
+ const shown = relative(cwd, path);
277
+ return shown && !shown.startsWith("..") && !isAbsolute(shown) ? shown : path;
278
+ }
279
+ function planFor(client, scope, url) {
280
+ const snippets = renderMcpSnippets(url);
281
+ switch (client) {
282
+ case "claude-code":
283
+ return scope === "user"
284
+ ? {
285
+ kind: "cli",
286
+ executable: "claude",
287
+ args: snippets.claudeCodeCli.args,
288
+ display: snippets.claudeCodeCli.display,
289
+ snippet: `${snippets.claudeCodeCli.display}\n`,
290
+ fallback: ["claude is not on PATH. Run this where Claude Code is installed:"],
291
+ }
292
+ : {
293
+ kind: "file",
294
+ file: ".mcp.json",
295
+ key: "mcpServers",
296
+ entry: snippets.claudeCodeServer,
297
+ snippet: snippets.claudeCodeProjectJson,
298
+ };
299
+ case "cursor":
300
+ return {
301
+ kind: "file",
302
+ file: join(".cursor", "mcp.json"),
303
+ key: "mcpServers",
304
+ entry: snippets.cursorServer,
305
+ snippet: snippets.cursorJson,
306
+ };
307
+ case "codex":
308
+ return {
309
+ kind: "cli",
310
+ executable: "codex",
311
+ args: snippets.codexCli.args,
312
+ display: snippets.codexCli.display,
313
+ snippet: snippets.codexToml,
314
+ fallback: [
315
+ "codex is not on PATH. Add this to ~/.codex/config.toml (or run the command where Codex is installed):",
316
+ snippets.codexToml.trimEnd(),
317
+ ],
318
+ };
319
+ case "vscode":
320
+ return {
321
+ kind: "file",
322
+ file: join(".vscode", "mcp.json"),
323
+ key: "servers",
324
+ entry: snippets.vscodeServer,
325
+ input: snippets.vscodeInput,
326
+ snippet: snippets.vscodeJson,
327
+ };
328
+ case "windsurf":
329
+ return {
330
+ kind: "manual",
331
+ snippet: snippets.windsurfJson,
332
+ hint: "Merge this into ~/.codeium/windsurf/mcp_config.json (Windsurf > Settings > MCP); the command does not write to your home directory.",
333
+ };
334
+ case "gemini":
335
+ return {
336
+ kind: "cli",
337
+ executable: "gemini",
338
+ args: snippets.geminiCli.args,
339
+ display: snippets.geminiCli.display,
340
+ snippet: `${snippets.geminiCli.display}\n`,
341
+ fallback: ["gemini is not on PATH. Run this where Gemini CLI is installed:"],
342
+ };
343
+ }
344
+ }
345
+ function nextSteps(client) {
346
+ const label = CLIENT_LABELS[client];
347
+ const lines = client === "vscode"
348
+ ? [
349
+ `${label} prompts for the key (input ${INPUT_ID}) when the server starts; paste the ${ENV_VAR} value that hue login stored in .env.hue.`,
350
+ ]
351
+ : [
352
+ `Export ${ENV_VAR} in the shell that starts ${label}; hue login stores it in .env.hue:`,
353
+ " set -a; . ./.env.hue; set +a",
354
+ ];
355
+ return [...lines, "Then ask your agent:", ` ${MCP_VERIFY_PROMPT}`];
356
+ }
357
+ /**
358
+ * Runs `hue mcp install` and returns the process exit code: 0 done or printed, 1 failed, 2 usage
359
+ * error. `argv` may start with the `mcp` command word.
360
+ */
361
+ export async function runMcpCommand(argv, io = {}) {
362
+ const stdout = io.stdout ?? process.stdout;
363
+ const stderr = io.stderr ?? process.stderr;
364
+ const env = io.env ?? process.env;
365
+ const cwd = io.cwd ?? process.cwd();
366
+ const out = (line) => {
367
+ stdout.write(`${line}\n`);
368
+ };
369
+ const fail = (message, code = 1) => {
370
+ stderr.write(`${message}\n`);
371
+ return code;
372
+ };
373
+ let parsed;
374
+ try {
375
+ parsed = parseMcpArguments(argv);
376
+ }
377
+ catch (error) {
378
+ return fail(`${error.message}\n\n${MCP_USAGE}`, 2);
379
+ }
380
+ if (parsed.values.help) {
381
+ out(MCP_USAGE);
382
+ return 0;
383
+ }
384
+ const positionals = parsed.positionals[0] === "mcp" ? parsed.positionals.slice(1) : parsed.positionals;
385
+ if (positionals.length !== 1 || positionals[0] !== "install")
386
+ return fail(`${positionals.length === 0 ? "Missing subcommand." : `Unknown subcommand: ${positionals.join(" ")}`}\n\n${MCP_USAGE}`, 2);
387
+ const client = parsed.values.client;
388
+ if (!client || !CLIENT_IDS.includes(client))
389
+ return fail(`${client ? `Unknown client: ${client}.` : "--client is required."} Choose one of ${CLIENT_IDS.join(", ")}.\n\n${MCP_USAGE}`, 2);
390
+ const scope = parsed.values.scope ?? "project";
391
+ if (scope !== "project" && scope !== "user")
392
+ return fail(`--scope must be project or user.\n\n${MCP_USAGE}`, 2);
393
+ if (scope === "user" && client !== "claude-code")
394
+ return fail("--scope user is only available with --client claude-code.", 2);
395
+ const url = parseMcpUrl(parsed.values.url ?? DEFAULT_MCP_URL);
396
+ if (!url)
397
+ 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);
398
+ const plan = planFor(client, scope, url);
399
+ if (parsed.values.print) {
400
+ stdout.write(plan.snippet);
401
+ return 0;
402
+ }
403
+ const steps = nextSteps(client);
404
+ if (plan.kind === "manual") {
405
+ out(plan.hint);
406
+ stdout.write(plan.snippet);
407
+ for (const line of steps)
408
+ out(line);
409
+ return 0;
410
+ }
411
+ if (plan.kind === "cli") {
412
+ if (parsed.values["dry-run"]) {
413
+ out(`Would run: ${plan.display}`);
414
+ return 0;
415
+ }
416
+ const executable = await findExecutable(plan.executable, env);
417
+ if (!executable) {
418
+ for (const line of plan.fallback)
419
+ out(line);
420
+ out(plan.display);
421
+ for (const line of steps)
422
+ out(line);
423
+ return 0;
424
+ }
425
+ out(`Running: ${plan.display}`);
426
+ const code = await runClientCli(executable, plan.args, { cwd, env, stdout, stderr });
427
+ if (code === null)
428
+ return fail(`${plan.executable} could not be started. Run this command yourself:\n${plan.display}`);
429
+ if (code !== 0)
430
+ return fail(`${plan.executable} exited with code ${code}. Run this command yourself:\n${plan.display}`);
431
+ out(`Registered the "${SERVER_NAME}" MCP server (${url}) with ${CLIENT_LABELS[client]}.`);
432
+ for (const line of steps)
433
+ out(line);
434
+ return 0;
435
+ }
436
+ const path = resolve(cwd, plan.file);
437
+ const display = displayPath(cwd, path);
438
+ let content;
439
+ try {
440
+ const existing = await readJsonConfig(path, display);
441
+ let merged = mergeServerEntry(existing, plan.key, plan.entry, display);
442
+ if (plan.input)
443
+ merged = mergeVscodeInput(merged, plan.input, display);
444
+ content = json(merged);
445
+ }
446
+ catch (error) {
447
+ if (error instanceof ConfigError)
448
+ return fail(`${error.message}\nSnippet for ${display}:\n${plan.snippet.trimEnd()}`);
449
+ return fail(`Could not read ${display}: ${error.message}`);
450
+ }
451
+ if (parsed.values["dry-run"]) {
452
+ out(`Would write ${display}:`);
453
+ stdout.write(content);
454
+ return 0;
455
+ }
456
+ try {
457
+ await writeConfigFile(path, content, display);
458
+ }
459
+ catch (error) {
460
+ return fail(`Could not write ${display}: ${error.message}`);
461
+ }
462
+ out(`Wrote ${display} with the "${SERVER_NAME}" MCP server (${url}).`);
463
+ for (const line of steps)
464
+ out(line);
465
+ return 0;
466
+ }
@@ -1,6 +1,6 @@
1
1
  import type { ProjectConnection } from "../types.js";
2
2
  import { type AttemptConnectionBundleV2, type PrepareAttemptRequestV2 } from "./attempt.js";
3
- import type { CaseWrite, CompleteExecution, Completion, Dataset, DatasetCase, DatasetVersion, EvaluationItem, EvaluationRun, EnvironmentEvidenceSnapshot, Execution, Experiment, ExperimentCase, ExperimentItem, Identity, JsonValue, LocalAgentClaim, LocalAgentRegistration, RegisteredLocalAgent, JudgeBudget, JudgeJob, Page, PageOptions, RegistryPageOptions, Result, ResultSummary, Scorer, ScorerDefinition, ScorerVersion, SimulationMcpCapability, StartExecution, Subject, StoredResult } from "./types.js";
3
+ import type { ArtifactReservation, ArtifactUpload, CaseConversion, CaseConversionSummary, CaseWrite, CompleteExecution, Completion, Dataset, DatasetCase, DatasetVersion, EvaluationItem, EvaluationRun, EvaluationRunSummary, EnvironmentEvidenceSnapshot, Execution, Experiment, ExperimentCase, ExperimentItem, Identity, JsonValue, LocalAgentClaim, LocalAgentRegistration, RegisteredLocalAgent, JudgeBudget, JudgeJob, Page, PageOptions, RegistryPageOptions, Result, ResultSummary, Scorer, ScorerDefinition, ScorerVersion, SimulationMcpCapability, StartExecution, Subject, StoredResult } from "./types.js";
4
4
  /** Connection options for {@link createEvaluationClient}. */
5
5
  export interface EvaluationClientOptions {
6
6
  /** Project service key sent as a Bearer token; server side only. */
@@ -30,6 +30,10 @@ export declare class EvaluationClient {
30
30
  private readonly timeoutMillis;
31
31
  constructor(options: EvaluationClientOptions);
32
32
  private request;
33
+ /** Verified bytes of one ready artifact in this project, bounded to the 25 MiB pilot file size. */
34
+ downloadArtifact(id: string): Promise<Uint8Array>;
35
+ /** Stage bytes at the storage capability Hue issued. The Hue key is never sent to storage. */
36
+ uploadArtifactBytes(upload: ArtifactUpload, bytes: Uint8Array, contentType: string): Promise<void>;
33
37
  private page;
34
38
  private registryPage;
35
39
  /** Reads the current project to confirm the key and origin. */
@@ -113,6 +117,25 @@ export declare class EvaluationClient {
113
117
  revokeAttemptConnection(input: {
114
118
  bindingId: string;
115
119
  }): Promise<import("./attempt.js").RevokeAttemptResult>;
120
+ /** Reads one artifact reservation and its verification state. */
121
+ getArtifact(id: string): Promise<ArtifactReservation>;
122
+ /** Reserves an artifact by declared identity; replaying the key returns the same reservation. */
123
+ reserveArtifact(input: {
124
+ /** Stable key; replaying it returns the same reservation. */
125
+ idempotencyKey: string;
126
+ /** Declared file name. */
127
+ filename: string;
128
+ /** Declared content type. */
129
+ contentType: string;
130
+ /** Declared size in bytes. */
131
+ byteSize: number;
132
+ /** Declared SHA-256, hex encoded. */
133
+ sha256: string;
134
+ }): Promise<ArtifactReservation>;
135
+ /** Issues a short-lived storage capability for staging the reserved artifact's bytes. */
136
+ requestArtifactUpload(id: string): Promise<ArtifactUpload>;
137
+ /** Asks Hue to verify the staged bytes against the declared identity. */
138
+ completeArtifact(id: string): Promise<ArtifactReservation>;
116
139
  /** Saves an execution's outcome and creates its immutable subject. */
117
140
  completeExecution(id: string, input: CompleteExecution): Promise<Completion>;
118
141
  /** Marks an experiment finished. */
@@ -134,6 +157,8 @@ export declare class EvaluationClient {
134
157
  }>;
135
158
  /** Reads an evaluation run and its scoring progress. */
136
159
  getEvaluationRun(id: string): Promise<EvaluationRun>;
160
+ /** Every evaluation run of the project, oldest first; a grading worker polls this for pending pins. */
161
+ listEvaluationRuns(page?: PageOptions): Promise<Page<EvaluationRunSummary>>;
137
162
  /** Lists the subjects of an evaluation run. */
138
163
  listEvaluationItems(id: string, page?: PageOptions): Promise<Page<EvaluationItem>>;
139
164
  /** Reads an immutable subject, including output and reference when available. */
@@ -205,6 +230,10 @@ export declare class EvaluationClient {
205
230
  /** Acknowledged terminal queue state. */
206
231
  state: "completed" | "attention";
207
232
  }>;
233
+ /** Lists Scenarios (draft and published) of the project; requires a Read and write key. */
234
+ listCaseConversions(page?: PageOptions): Promise<Page<CaseConversionSummary>>;
235
+ /** Reads one Scenario with its immutable publication pins. */
236
+ getCaseConversion(id: string): Promise<CaseConversion>;
208
237
  /** Creates the legacy execution-scoped generic MCP capability for one world. */
209
238
  createSimulationMcpCapability(input: {
210
239
  runId: string;