@hue-run/sdk 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CLI.md +240 -49
  2. package/ENVIRONMENTS.md +48 -9
  3. package/EVALUATIONS.md +41 -21
  4. package/README.md +70 -30
  5. package/dist/cli/eval-direct.d.ts +12 -1
  6. package/dist/cli/eval-direct.js +145 -33
  7. package/dist/cli/eval.d.ts +28 -1
  8. package/dist/cli/eval.js +504 -81
  9. package/dist/cli/listen.d.ts +76 -0
  10. package/dist/cli/listen.js +816 -0
  11. package/dist/cli/mcp.d.ts +65 -7
  12. package/dist/cli/mcp.js +271 -73
  13. package/dist/client.d.ts +7 -4
  14. package/dist/client.js +36 -10
  15. package/dist/evals/checkpoint.d.ts +15 -1
  16. package/dist/evals/checkpoint.js +34 -3
  17. package/dist/evals/client.d.ts +24 -4
  18. package/dist/evals/client.js +92 -38
  19. package/dist/evals/environment-target.d.ts +18 -3
  20. package/dist/evals/environment-target.js +22 -10
  21. package/dist/evals/exit-cleanup.d.ts +4 -0
  22. package/dist/evals/exit-cleanup.js +26 -0
  23. package/dist/evals/files.d.ts +69 -3
  24. package/dist/evals/files.js +255 -42
  25. package/dist/evals/json.js +116 -9
  26. package/dist/evals/local-worker.d.ts +21 -3
  27. package/dist/evals/local-worker.js +14 -0
  28. package/dist/evals/runner.d.ts +45 -0
  29. package/dist/evals/runner.js +199 -21
  30. package/dist/evals/scenarios.d.ts +7 -5
  31. package/dist/evals/scenarios.js +71 -14
  32. package/dist/evals/scorer-publication.js +58 -3
  33. package/dist/evals/simulation.d.ts +17 -4
  34. package/dist/evals/simulation.js +8 -0
  35. package/dist/evals/types.d.ts +48 -0
  36. package/dist/evals/verdicts.d.ts +26 -2
  37. package/dist/evals/verdicts.js +72 -9
  38. package/dist/evals.d.ts +4 -3
  39. package/dist/evals.js +2 -2
  40. package/dist/inline-files.d.ts +4 -0
  41. package/dist/inline-files.js +48 -17
  42. package/dist/provider-tools.d.ts +12 -1
  43. package/dist/provider-tools.js +192 -106
  44. package/dist/safety.d.ts +7 -1
  45. package/dist/safety.js +9 -7
  46. package/dist/setup/cli.js +31 -8
  47. package/dist/setup/peers.d.ts +13 -0
  48. package/dist/setup/peers.js +44 -0
  49. package/dist/snapshot.d.ts +2 -2
  50. package/dist/snapshot.js +47 -12
  51. package/dist/tool-definitions.d.ts +18 -0
  52. package/dist/tool-definitions.js +232 -0
  53. package/dist/transport.js +2 -2
  54. package/dist/version.d.ts +1 -1
  55. package/dist/version.js +1 -1
  56. package/package.json +1 -1
@@ -1,4 +1,5 @@
1
1
  import { MAX_CONTENT_BYTES } from "./config.js";
2
+ import { scrubCredentialText } from "./tool-definitions.js";
2
3
  const MAX_PROVIDER_ITEMS = 128;
3
4
  const MAX_PROVIDER_DEFINITIONS = 512;
4
5
  const MAX_PROVIDER_SERVERS = 512;
@@ -19,13 +20,28 @@ function isProviderToolItem(provider, value) {
19
20
  return value.type === "mcp_tool_use" || value.type === "server_tool_use";
20
21
  }
21
22
  function countProviderToolItems(provider, items, start = 0) {
23
+ let keys;
24
+ try {
25
+ keys = Object.keys(items);
26
+ }
27
+ catch {
28
+ // A tail that cannot be listed still holds at least one item nothing could read.
29
+ return 1;
30
+ }
22
31
  let count = 0;
23
- for (const key of Object.keys(items)) {
32
+ for (const key of keys) {
24
33
  const index = Number(key);
25
34
  if (!Number.isInteger(index) || index < start || index >= items.length || String(index) !== key)
26
35
  continue;
27
- if (isProviderToolItem(provider, items[index]))
36
+ try {
37
+ if (isProviderToolItem(provider, items[index]))
38
+ count++;
39
+ }
40
+ catch {
41
+ // A tail item whose `type` cannot be read counts as skipped; the bounded prefix is still
42
+ // recorded, as the Python SDK does.
28
43
  count++;
44
+ }
29
45
  }
30
46
  return count;
31
47
  }
@@ -41,6 +57,42 @@ function text(value) {
41
57
  function errorCode(value) {
42
58
  return typeof value === "string" && /^[a-z0-9_]{1,64}$/.test(value) ? value : "error";
43
59
  }
60
+ /** The most of a provider's error text scrubbed, and the most exported, in code points. */
61
+ const MAX_ERROR_SCAN = 16_384;
62
+ const MAX_ERROR_TEXT = 1_024;
63
+ /** The first `count` code points of `text`. */
64
+ function codePoints(text, count) {
65
+ let kept = "";
66
+ let seen = 0;
67
+ for (const character of text) {
68
+ if (seen++ === count)
69
+ return { text: kept, cut: true };
70
+ kept += character;
71
+ }
72
+ return { text: kept, cut: false };
73
+ }
74
+ /**
75
+ * A provider's error text as a failed call exports it under content capture: credentials
76
+ * scrubbed first, then at most 1,024 code points with `…` marking a cut, lone surrogates
77
+ * replaced and NUL removed. The Python SDK produces the same text.
78
+ */
79
+ export function providerErrorDescription(value) {
80
+ const scanned = codePoints(value.toWellFormed().replaceAll("\u0000", ""), MAX_ERROR_SCAN);
81
+ const bounded = codePoints(scrubCredentialText(scanned.text), MAX_ERROR_TEXT);
82
+ return bounded.cut || scanned.cut ? `${bounded.text}…` : bounded.text;
83
+ }
84
+ /** An OpenAI MCP call's `error`: its text, or an error object's string `message`. */
85
+ function errorText(value) {
86
+ let message;
87
+ try {
88
+ message = isItem(value) ? value.message : value;
89
+ }
90
+ catch {
91
+ // An error whose message cannot be read still fails its call; only the text is lost.
92
+ return undefined;
93
+ }
94
+ return typeof message === "string" && message.trim() !== "" ? message : undefined;
95
+ }
44
96
  /** MCP arguments arrive as a JSON string; record the structure when it parses, else the text. */
45
97
  function jsonArguments(value) {
46
98
  if (typeof value !== "string")
@@ -59,88 +111,105 @@ function openaiCalls(items, activity) {
59
111
  const count = Math.min(items.length, MAX_PROVIDER_ITEMS);
60
112
  activity.skipped += countProviderToolItems("openai", items, count);
61
113
  for (let index = 0; index < count; index++) {
62
- const item = items[index];
63
- if (!isItem(item))
64
- continue;
65
- const callId = text(item.id);
66
- switch (item.type) {
67
- case "mcp_call": {
68
- const name = text(item.name);
69
- if (!name) {
70
- activity.skipped++;
71
- break;
72
- }
73
- activity.calls.push({
74
- name,
75
- callId,
76
- server: text(item.server_label),
77
- arguments: jsonArguments(item.arguments),
78
- ...(item.output !== undefined && item.output !== null ? { result: item.output } : {}),
79
- ...(item.error !== undefined && item.error !== null ? { errorType: "mcp_error" } : {}),
80
- });
114
+ try {
115
+ openaiItem(items[index], index, activity);
116
+ }
117
+ catch {
118
+ // An item that cannot be read is skipped; the others are still recorded.
119
+ activity.skipped++;
120
+ }
121
+ }
122
+ }
123
+ function openaiItem(item, position, activity) {
124
+ if (!isItem(item))
125
+ return;
126
+ const callId = text(item.id);
127
+ switch (item.type) {
128
+ case "mcp_call": {
129
+ const name = text(item.name);
130
+ if (!name) {
131
+ activity.skipped++;
81
132
  break;
82
133
  }
83
- case "mcp_list_tools": {
84
- const server = text(item.server_label);
85
- if (!server || !Array.isArray(item.tools)) {
86
- activity.skipped++;
87
- break;
88
- }
89
- const definitions = [];
90
- const definitionCount = Math.min(item.tools.length, MAX_PROVIDER_DEFINITIONS);
91
- activity.skipped += item.tools.length - definitionCount;
92
- for (let index = 0; index < definitionCount; index++) {
134
+ const failed = item.error !== undefined && item.error !== null;
135
+ const detail = failed ? errorText(item.error) : undefined;
136
+ activity.calls.push({
137
+ name,
138
+ callId,
139
+ position,
140
+ server: text(item.server_label),
141
+ arguments: jsonArguments(item.arguments),
142
+ ...(item.output !== undefined && item.output !== null ? { result: item.output } : {}),
143
+ ...(failed ? { errorType: "mcp_error" } : {}),
144
+ ...(detail !== undefined ? { errorText: detail } : {}),
145
+ });
146
+ break;
147
+ }
148
+ case "mcp_list_tools": {
149
+ const server = text(item.server_label);
150
+ if (!server || !Array.isArray(item.tools)) {
151
+ activity.skipped++;
152
+ break;
153
+ }
154
+ const definitions = [];
155
+ const definitionCount = Math.min(item.tools.length, MAX_PROVIDER_DEFINITIONS);
156
+ activity.skipped += item.tools.length - definitionCount;
157
+ for (let index = 0; index < definitionCount; index++) {
158
+ try {
93
159
  const tool = item.tools[index];
94
160
  if (!isItem(tool))
95
161
  continue;
162
+ // A null field is left out, as absent, so both SDKs digest the same definition.
96
163
  definitions.push({
97
164
  type: "function",
98
- ...(tool.name !== undefined ? { name: tool.name } : {}),
99
- ...(tool.description !== undefined ? { description: tool.description } : {}),
100
- ...(tool.input_schema !== undefined ? { parameters: tool.input_schema } : {}),
101
- ...(tool.annotations !== undefined ? { annotations: tool.annotations } : {}),
165
+ ...(tool.name != null ? { name: tool.name } : {}),
166
+ ...(tool.description != null ? { description: tool.description } : {}),
167
+ ...(tool.input_schema != null ? { parameters: tool.input_schema } : {}),
168
+ ...(tool.annotations != null ? { annotations: tool.annotations } : {}),
102
169
  });
103
170
  }
104
- activity.listings.push({
105
- server,
106
- definitions,
107
- ...(item.error !== undefined && item.error !== null ? { errorType: "mcp_error" } : {}),
108
- });
109
- break;
110
- }
111
- case "web_search_call":
112
- case "file_search_call":
113
- case "code_interpreter_call": {
114
- const name = item.type.slice(0, -"_call".length);
115
- const failed = item.status === "failed" ? { errorType: "failed" } : {};
116
- if (item.type === "web_search_call")
117
- activity.calls.push({ name, callId, arguments: item.action, ...failed });
118
- else if (item.type === "file_search_call")
119
- activity.calls.push({
120
- name,
121
- callId,
122
- arguments: { queries: item.queries },
123
- ...(item.results !== undefined && item.results !== null
124
- ? { result: item.results }
125
- : {}),
126
- ...failed,
127
- });
128
- else
129
- activity.calls.push({
130
- name,
131
- callId,
132
- arguments: { code: item.code, container_id: item.container_id },
133
- ...(item.outputs !== undefined && item.outputs !== null
134
- ? { result: item.outputs }
135
- : {}),
136
- ...failed,
137
- });
138
- break;
171
+ catch {
172
+ // A definition that cannot be read is skipped; the rest of the listing is kept.
173
+ activity.skipped++;
174
+ }
139
175
  }
140
- default:
141
- // Messages, reasoning, approval requests and other items are not executed tools.
142
- break;
176
+ activity.listings.push({
177
+ server,
178
+ definitions,
179
+ ...(item.error !== undefined && item.error !== null ? { errorType: "mcp_error" } : {}),
180
+ });
181
+ break;
143
182
  }
183
+ case "web_search_call":
184
+ case "file_search_call":
185
+ case "code_interpreter_call": {
186
+ const name = item.type.slice(0, -"_call".length);
187
+ const failed = item.status === "failed" ? { errorType: "failed" } : {};
188
+ if (item.type === "web_search_call")
189
+ activity.calls.push({ name, callId, position, arguments: item.action, ...failed });
190
+ else if (item.type === "file_search_call")
191
+ activity.calls.push({
192
+ name,
193
+ callId,
194
+ position,
195
+ arguments: { queries: item.queries },
196
+ ...(item.results !== undefined && item.results !== null ? { result: item.results } : {}),
197
+ ...failed,
198
+ });
199
+ else
200
+ activity.calls.push({
201
+ name,
202
+ callId,
203
+ position,
204
+ arguments: { code: item.code, container_id: item.container_id },
205
+ ...(item.outputs !== undefined && item.outputs !== null ? { result: item.outputs } : {}),
206
+ ...failed,
207
+ });
208
+ break;
209
+ }
210
+ default:
211
+ // Messages, reasoning, approval requests and other items are not executed tools.
212
+ break;
144
213
  }
145
214
  }
146
215
  /** Anthropic Messages `content` blocks: a use block paired with the result block that names it. */
@@ -149,47 +218,64 @@ function anthropicCalls(blocks, activity) {
149
218
  const count = Math.min(blocks.length, MAX_PROVIDER_ITEMS);
150
219
  const truncated = blocks.length > MAX_PROVIDER_ITEMS;
151
220
  activity.skipped += countProviderToolItems("anthropic", blocks, count);
221
+ // Each block is read once; one that cannot be read is skipped and the rest still recorded.
222
+ const readable = [];
152
223
  for (let index = 0; index < count; index++) {
153
- const block = blocks[index];
154
- if (isItem(block) &&
155
- typeof block.type === "string" &&
156
- block.type.endsWith("_tool_result") &&
157
- typeof block.tool_use_id === "string")
158
- results.set(block.tool_use_id, block);
224
+ try {
225
+ const block = blocks[index];
226
+ if (isItem(block) &&
227
+ typeof block.type === "string" &&
228
+ block.type.endsWith("_tool_result") &&
229
+ typeof block.tool_use_id === "string")
230
+ results.set(block.tool_use_id, block);
231
+ readable.push(block);
232
+ }
233
+ catch {
234
+ activity.skipped++;
235
+ readable.push(undefined);
236
+ }
159
237
  }
160
238
  for (let index = 0; index < count; index++) {
161
- const block = blocks[index];
162
- if (!isItem(block) || (block.type !== "mcp_tool_use" && block.type !== "server_tool_use"))
163
- continue;
164
- const name = text(block.name);
165
- const callId = text(block.id);
166
- if (!name) {
167
- activity.skipped++;
168
- continue;
239
+ try {
240
+ anthropicBlock(readable[index], index, results, truncated, activity);
169
241
  }
170
- const result = callId === undefined ? undefined : results.get(callId);
171
- // When the response was truncated, an unmatched use block may have its result outside the
172
- // bounded prefix. Do not export it as a successful call with a missing result.
173
- if (truncated && result === undefined) {
242
+ catch {
174
243
  activity.skipped++;
175
- continue;
176
244
  }
177
- const content = result?.content;
178
- let errorType;
179
- if (result?.is_error === true)
180
- errorType = "mcp_error";
181
- else if (isItem(content) && typeof content.type === "string" && content.type.endsWith("_error"))
182
- errorType = errorCode(content.error_code);
183
- activity.calls.push({
184
- name,
185
- callId,
186
- ...(block.type === "mcp_tool_use" ? { server: text(block.server_name) } : {}),
187
- arguments: block.input,
188
- ...(content !== undefined ? { result: content } : {}),
189
- ...(errorType ? { errorType } : {}),
190
- });
191
245
  }
192
246
  }
247
+ function anthropicBlock(block, position, results, truncated, activity) {
248
+ if (!isItem(block) || (block.type !== "mcp_tool_use" && block.type !== "server_tool_use"))
249
+ return;
250
+ const name = text(block.name);
251
+ const callId = text(block.id);
252
+ if (!name) {
253
+ activity.skipped++;
254
+ return;
255
+ }
256
+ const result = callId === undefined ? undefined : results.get(callId);
257
+ // When the response was truncated, an unmatched use block may have its result outside the
258
+ // bounded prefix. Do not export it as a successful call with a missing result.
259
+ if (truncated && result === undefined) {
260
+ activity.skipped++;
261
+ return;
262
+ }
263
+ const content = result?.content;
264
+ let errorType;
265
+ if (result?.is_error === true)
266
+ errorType = "mcp_error";
267
+ else if (isItem(content) && typeof content.type === "string" && content.type.endsWith("_error"))
268
+ errorType = errorCode(content.error_code);
269
+ activity.calls.push({
270
+ name,
271
+ callId,
272
+ position,
273
+ ...(block.type === "mcp_tool_use" ? { server: text(block.server_name) } : {}),
274
+ arguments: block.input,
275
+ ...(content !== undefined ? { result: content } : {}),
276
+ ...(errorType ? { errorType } : {}),
277
+ });
278
+ }
193
279
  /**
194
280
  * Reads the hosted tool calls out of a provider response: the `output` items of an OpenAI
195
281
  * Responses API response, or the `content` blocks of an Anthropic Messages API response. An array
package/dist/safety.d.ts CHANGED
@@ -1,7 +1,13 @@
1
1
  import { type Span } from "@opentelemetry/api";
2
2
  export declare function noopSpan(): Span;
3
3
  export declare function safeSpan(source: Span, failed: () => void): Span;
4
+ /** The bounds `encodeContent` enforces while it copies a value. */
5
+ export interface EncodeLimits {
6
+ bytes: number;
7
+ nodes: number;
8
+ depth: number;
9
+ }
4
10
  /** Validate a bounded data tree without invoking toJSON or property getters. */
5
- export declare function encodeContent(value: unknown): string;
11
+ export declare function encodeContent(value: unknown, limits?: EncodeLimits): string;
6
12
  /** Bounded conservative accounting for the record data retained by our queue, not total process RSS. */
7
13
  export declare function estimateRecordBytes(value: unknown, limit: number): number;
package/dist/safety.js CHANGED
@@ -89,21 +89,23 @@ class SafeSpan {
89
89
  export function safeSpan(source, failed) {
90
90
  return new SafeSpan(source, failed);
91
91
  }
92
+ /** One content attribute's bounds. */
93
+ const contentLimits = { bytes: MAX_CONTENT_BYTES, nodes: 16384, depth: 32 };
92
94
  /** Validate a bounded data tree without invoking toJSON or property getters. */
93
- export function encodeContent(value) {
95
+ export function encodeContent(value, limits = contentLimits) {
94
96
  let nodes = 0;
95
97
  let bytes = 0;
96
98
  const ancestors = new Set();
97
99
  const charge = (amount) => {
98
100
  bytes += amount;
99
- if (bytes > MAX_CONTENT_BYTES)
101
+ if (bytes > limits.bytes)
100
102
  throw new RangeError("Content limit exceeded");
101
103
  };
102
104
  const visit = (item, depth) => {
103
- if (++nodes > 16384 || depth > 32)
105
+ if (++nodes > limits.nodes || depth > limits.depth)
104
106
  throw new RangeError("Content complexity limit exceeded");
105
107
  if (typeof item === "string") {
106
- if (item.length > MAX_CONTENT_BYTES)
108
+ if (item.length > limits.bytes)
107
109
  throw new RangeError("Content limit exceeded");
108
110
  charge(Buffer.byteLength(JSON.stringify(item)));
109
111
  return item;
@@ -128,9 +130,9 @@ export function encodeContent(value) {
128
130
  const result = array ? [] : Object.create(null);
129
131
  // Own descriptors avoid executing application accessors during capture.
130
132
  const keys = array
131
- ? Array.from({ length: Math.min(item.length, 16385) }, (_, i) => String(i))
133
+ ? Array.from({ length: Math.min(item.length, limits.nodes + 1) }, (_, i) => String(i))
132
134
  : Object.keys(item);
133
- if (keys.length > 16384)
135
+ if (keys.length > limits.nodes)
134
136
  throw new RangeError("Content complexity limit exceeded");
135
137
  for (const key of keys) {
136
138
  const descriptor = Object.getOwnPropertyDescriptor(item, key);
@@ -147,7 +149,7 @@ export function encodeContent(value) {
147
149
  return result;
148
150
  };
149
151
  const encoded = JSON.stringify(visit(value, 0));
150
- if (Buffer.byteLength(encoded) > MAX_CONTENT_BYTES)
152
+ if (Buffer.byteLength(encoded) > limits.bytes)
151
153
  throw new RangeError("Content limit exceeded");
152
154
  return encoded;
153
155
  }
package/dist/setup/cli.js CHANGED
@@ -4,6 +4,7 @@ import { parseArgs } from "node:util";
4
4
  import { FileSetupCheckpointAdapter, setupRunId } from "./checkpoint.js";
5
5
  import { SetupBackendAdapter } from "./backend.js";
6
6
  import { detectSetupProject } from "./detect.js";
7
+ import { loadCommand, MissingPeerError } from "./peers.js";
7
8
  import { renderHumanEvent, renderJsonlEvent, renderPlainEvent, selectSetupOutputMode, } from "./render.js";
8
9
  import { runSetup } from "./runner.js";
9
10
  import { SETUP_EVENT_CONTRACT_VERSION } from "./types.js";
@@ -11,9 +12,22 @@ const commands = new Set(["setup", "resume", "status", "claim"]);
11
12
  // Additional commands live in ../cli and load lazily so the setup parser, its usage text and its
12
13
  // JSONL error contract stay untouched for every other input. Add a command with one entry.
13
14
  const extensions = new Map([
14
- ["eval", async () => (await import("../cli/eval.js")).runEvalCommand(process.argv.slice(3))],
15
- ["login", async () => (await import("../cli/login.js")).runLoginCommand(process.argv.slice(3))],
16
- ["mcp", async () => (await import("../cli/mcp.js")).runMcpCommand(process.argv.slice(3))],
15
+ [
16
+ "eval",
17
+ async () => (await loadCommand("eval", () => import("../cli/eval.js"))).runEvalCommand(process.argv.slice(3)),
18
+ ],
19
+ [
20
+ "login",
21
+ async () => (await loadCommand("login", () => import("../cli/login.js"))).runLoginCommand(process.argv.slice(3)),
22
+ ],
23
+ [
24
+ "mcp",
25
+ async () => (await loadCommand("mcp", () => import("../cli/mcp.js"))).runMcpCommand(process.argv.slice(3)),
26
+ ],
27
+ [
28
+ "listen",
29
+ async () => (await loadCommand("listen", () => import("../cli/listen.js"))).runListenCommand(process.argv.slice(3)),
30
+ ],
17
31
  ]);
18
32
  function writeEvent(event, mode, width) {
19
33
  const line = mode === "jsonl"
@@ -26,8 +40,17 @@ function writeEvent(event, mode, width) {
26
40
  }
27
41
  async function main() {
28
42
  const extension = extensions.get(process.argv[2] ?? "");
29
- if (extension)
30
- return extension();
43
+ if (extension) {
44
+ try {
45
+ return await extension();
46
+ }
47
+ catch (error) {
48
+ if (!(error instanceof MissingPeerError))
49
+ throw error;
50
+ process.stderr.write(`${error.message}\n`);
51
+ return 2;
52
+ }
53
+ }
31
54
  const agentRequested = process.argv.slice(2).includes("--agent");
32
55
  let parsed;
33
56
  try {
@@ -47,7 +70,7 @@ async function main() {
47
70
  }
48
71
  catch {
49
72
  if (!agentRequested) {
50
- process.stderr.write("Usage: hue <setup|resume|status|claim|login|eval|mcp> [--agent|--format human|plain|jsonl] [--project PATH] [--origin URL] [--restart]\n");
73
+ process.stderr.write("Usage: hue <setup|resume|status|claim|login|eval|mcp|listen> [--agent|--format human|plain|jsonl] [--project PATH] [--origin URL] [--restart]\n");
51
74
  return 2;
52
75
  }
53
76
  const event = {
@@ -78,7 +101,7 @@ async function main() {
78
101
  process.stdout.write(`${renderJsonlEvent(event)}\n`);
79
102
  return 2;
80
103
  }
81
- process.stdout.write("Usage: hue <setup|resume|status|claim|login|eval|mcp> [--agent|--format human|plain|jsonl] [--project PATH] [--origin URL] [--restart]\n");
104
+ process.stdout.write("Usage: hue <setup|resume|status|claim|login|eval|mcp|listen> [--agent|--format human|plain|jsonl] [--project PATH] [--origin URL] [--restart]\n");
82
105
  return 0;
83
106
  }
84
107
  const command = parsed.positionals[0];
@@ -110,7 +133,7 @@ async function main() {
110
133
  process.stdout.write(`${renderJsonlEvent(event)}\n`);
111
134
  }
112
135
  else
113
- process.stderr.write("Usage: hue <setup|resume|status|claim|login|eval|mcp> [--agent|--format human|plain|jsonl] [--project PATH] [--origin URL] [--restart]\n");
136
+ process.stderr.write("Usage: hue <setup|resume|status|claim|login|eval|mcp|listen> [--agent|--format human|plain|jsonl] [--project PATH] [--origin URL] [--restart]\n");
114
137
  return 2;
115
138
  }
116
139
  const mode = selectSetupOutputMode({
@@ -0,0 +1,13 @@
1
+ /** A peer dependency of `@hue-run/sdk` that a command needs and the project has not installed. */
2
+ export declare class MissingPeerError extends Error {
3
+ readonly command: string;
4
+ readonly peer: string;
5
+ readonly range: string;
6
+ constructor(command: string, peer: string, range: string);
7
+ }
8
+ /**
9
+ * Loads a command module. When the import fails because one of the SDK's peers, such as `zod` or
10
+ * `@opentelemetry/api`, is not installed, the failure names that peer and how to add it instead
11
+ * of the runtime's resolution error; any other failure is rethrown unchanged.
12
+ */
13
+ export declare function loadCommand<T>(command: string, load: () => Promise<T>): Promise<T>;
@@ -0,0 +1,44 @@
1
+ import { readFileSync } from "node:fs";
2
+ /** A peer dependency of `@hue-run/sdk` that a command needs and the project has not installed. */
3
+ export class MissingPeerError extends Error {
4
+ command;
5
+ peer;
6
+ range;
7
+ constructor(command, peer, range) {
8
+ super(`hue ${command} needs ${peer}, a peer dependency of @hue-run/sdk that this project has not installed. Install it with: npm install "${peer}@${range}" (or bun add, pnpm add or yarn add with the same argument)`);
9
+ this.command = command;
10
+ this.peer = peer;
11
+ this.range = range;
12
+ this.name = "MissingPeerError";
13
+ }
14
+ }
15
+ /** The range `@hue-run/sdk` declares for a peer, or undefined when `name` is not one of its peers. */
16
+ function peerRange(name) {
17
+ try {
18
+ const manifest = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
19
+ const range = manifest.peerDependencies?.[name];
20
+ return typeof range === "string" ? range : undefined;
21
+ }
22
+ catch {
23
+ return undefined;
24
+ }
25
+ }
26
+ /**
27
+ * Loads a command module. When the import fails because one of the SDK's peers, such as `zod` or
28
+ * `@opentelemetry/api`, is not installed, the failure names that peer and how to add it instead
29
+ * of the runtime's resolution error; any other failure is rethrown unchanged.
30
+ */
31
+ export async function loadCommand(command, load) {
32
+ try {
33
+ return await load();
34
+ }
35
+ catch (error) {
36
+ // Only a package that cannot be found at all; "Cannot find module" for a file or subpath of
37
+ // an installed package is another failure and keeps its own message.
38
+ const missing = /Cannot find package ['"]((?:@[^/'"]+\/)?[^/'"]+)['"]/.exec(error instanceof Error ? error.message : "")?.[1];
39
+ const range = missing === undefined ? undefined : peerRange(missing);
40
+ if (missing === undefined || range === undefined)
41
+ throw error;
42
+ throw new MissingPeerError(command, missing, range);
43
+ }
44
+ }
@@ -1,11 +1,11 @@
1
1
  import type { ReadableSpan } from "@opentelemetry/sdk-trace";
2
2
  import type { ReadableLogRecord, ReadWriteLogRecord } from "@opentelemetry/sdk-logs";
3
- export declare function snapshotSpan(source: ReadableSpan, limit: number): {
3
+ export declare function snapshotSpan(source: ReadableSpan, limit: number, captureContent?: boolean): {
4
4
  record: ReadableSpan;
5
5
  bytes: number;
6
6
  unresolvedResource: boolean;
7
7
  };
8
- export declare function snapshotLog(source: ReadableLogRecord, limit: number): {
8
+ export declare function snapshotLog(source: ReadableLogRecord, limit: number, captureContent?: boolean): {
9
9
  record: ReadWriteLogRecord;
10
10
  bytes: number;
11
11
  unresolvedResource: boolean;