@hue-run/sdk 0.20.1 → 0.20.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.
@@ -0,0 +1,52 @@
1
+ /**
2
+ * A bounded reader of ZIP archives for `hue traces import prepare`: the central directory is
3
+ * read from the end of the file, each member is read on demand at its own offset and inflated
4
+ * in memory with `node:zlib`, never with an external command and never onto disk. Only stored
5
+ * and deflated members are read; a member that is encrypted, uses another method, names an
6
+ * unsafe path or exceeds a bound is reported, not read. ZIP64 sizes and offsets are understood,
7
+ * so an archive over 4 GiB or with many members still lists.
8
+ */
9
+ export declare const ZIP_LIMITS: {
10
+ /** Members one archive may list. */
11
+ readonly members: 20000;
12
+ /** One member, decoded. */
13
+ readonly memberBytes: number;
14
+ /** Every member of the archive, decoded, together. */
15
+ readonly totalBytes: number;
16
+ /** The central directory itself. */
17
+ readonly centralDirectoryBytes: number;
18
+ /** A member name or extra field. */
19
+ readonly fieldBytes: 65535;
20
+ };
21
+ export interface ZipMember {
22
+ /** The member's path as the archive names it. */
23
+ path: string;
24
+ /** Why the member cannot be read, when it cannot; a readable member has none. */
25
+ refused?: string;
26
+ size: number;
27
+ compressedSize: number;
28
+ method: number;
29
+ crc32: number;
30
+ localHeaderOffset: number;
31
+ }
32
+ export declare class ZipError extends Error {
33
+ }
34
+ /** Whether a file starts like a ZIP archive: a local header, or the end record of an empty one. */
35
+ export declare function isZipSignature(head: Uint8Array): boolean;
36
+ /**
37
+ * Why a member path must not be read: an absolute path, a drive or network path, a `..`
38
+ * segment, a backslash, a control character or an empty name. A directory entry (ending in
39
+ * `/`) is not a file and is skipped by the caller.
40
+ */
41
+ export declare function unsafeZipPath(path: string): string | undefined;
42
+ /** The archive's central directory, listed once; members are read on demand. */
43
+ export declare class ZipArchive {
44
+ private readonly handle;
45
+ readonly members: readonly ZipMember[];
46
+ private constructor();
47
+ static open(path: string): Promise<ZipArchive>;
48
+ private static list;
49
+ /** The member's bytes, decoded and checked against its recorded size and CRC-32. */
50
+ read(member: ZipMember): Promise<Uint8Array>;
51
+ close(): Promise<void>;
52
+ }
@@ -0,0 +1,278 @@
1
+ import { open } from "node:fs/promises";
2
+ import { crc32, inflateRawSync } from "node:zlib";
3
+ /**
4
+ * A bounded reader of ZIP archives for `hue traces import prepare`: the central directory is
5
+ * read from the end of the file, each member is read on demand at its own offset and inflated
6
+ * in memory with `node:zlib`, never with an external command and never onto disk. Only stored
7
+ * and deflated members are read; a member that is encrypted, uses another method, names an
8
+ * unsafe path or exceeds a bound is reported, not read. ZIP64 sizes and offsets are understood,
9
+ * so an archive over 4 GiB or with many members still lists.
10
+ */
11
+ export const ZIP_LIMITS = {
12
+ /** Members one archive may list. */
13
+ members: 20_000,
14
+ /** One member, decoded. */
15
+ memberBytes: 64 * 1024 * 1024,
16
+ /** Every member of the archive, decoded, together. */
17
+ totalBytes: 1024 * 1024 * 1024,
18
+ /** The central directory itself. */
19
+ centralDirectoryBytes: 32 * 1024 * 1024,
20
+ /** A member name or extra field. */
21
+ fieldBytes: 65_535,
22
+ };
23
+ const LOCAL_HEADER = 0x04034b50;
24
+ const CENTRAL_HEADER = 0x02014b50;
25
+ const END_OF_CENTRAL_DIRECTORY = 0x06054b50;
26
+ const ZIP64_END_OF_CENTRAL_DIRECTORY = 0x06064b50;
27
+ const ZIP64_LOCATOR = 0x07064b50;
28
+ const ZIP64_EXTRA = 0x0001;
29
+ const END_RECORD_BYTES = 22;
30
+ const MAX_COMMENT_BYTES = 65_535;
31
+ const METHOD_STORED = 0;
32
+ const METHOD_DEFLATE = 8;
33
+ /** The most compressed bytes a deflated member of `size` decoded bytes can take: deflate grows
34
+ * an incompressible input by at most 5 bytes per 64 KiB block, well under a kilobyte per
35
+ * megabyte and a kilobyte of slack. A larger `compressedSize` is a malformed entry, refused
36
+ * before any of it is read. */
37
+ const deflateBound = (size) => size + (size >>> 10) + 1024;
38
+ const FLAG_ENCRYPTED = 0x0001;
39
+ const FLAG_UTF8 = 0x0800;
40
+ export class ZipError extends Error {
41
+ }
42
+ /** Whether a file starts like a ZIP archive: a local header, or the end record of an empty one. */
43
+ export function isZipSignature(head) {
44
+ if (head.byteLength < 4)
45
+ return false;
46
+ const signature = head[0] | (head[1] << 8) | (head[2] << 16) | (head[3] << 24);
47
+ return signature === LOCAL_HEADER || signature === END_OF_CENTRAL_DIRECTORY;
48
+ }
49
+ /**
50
+ * Why a member path must not be read: an absolute path, a drive or network path, a `..`
51
+ * segment, a backslash, a control character or an empty name. A directory entry (ending in
52
+ * `/`) is not a file and is skipped by the caller.
53
+ */
54
+ export function unsafeZipPath(path) {
55
+ if (!path)
56
+ return "an empty member name";
57
+ if (path.includes("\0") || /[\p{Cc}]/u.test(path))
58
+ return "a control character in the name";
59
+ if (path.includes("\\"))
60
+ return "a backslash in the name";
61
+ if (path.startsWith("/") || /^[A-Za-z]:/.test(path) || path.startsWith("//"))
62
+ return "an absolute path";
63
+ if (path.split("/").some((segment) => segment === ".." || segment === "."))
64
+ return "a relative path segment";
65
+ if (!path.isWellFormed())
66
+ return "an invalid name";
67
+ return undefined;
68
+ }
69
+ async function readAt(handle, position, length) {
70
+ const buffer = Buffer.allocUnsafe(length);
71
+ let offset = 0;
72
+ while (offset < length) {
73
+ const { bytesRead } = await handle.read(buffer, offset, length - offset, position + offset);
74
+ if (bytesRead === 0)
75
+ throw new ZipError("The archive ends before a record it names");
76
+ offset += bytesRead;
77
+ }
78
+ return buffer;
79
+ }
80
+ const readU64 = (buffer, offset) => {
81
+ const value = buffer.readBigUInt64LE(offset);
82
+ if (value > BigInt(Number.MAX_SAFE_INTEGER))
83
+ throw new ZipError("A ZIP64 field is out of range");
84
+ return Number(value);
85
+ };
86
+ /** The archive's central directory, listed once; members are read on demand. */
87
+ export class ZipArchive {
88
+ handle;
89
+ members;
90
+ constructor(handle, members) {
91
+ this.handle = handle;
92
+ this.members = members;
93
+ }
94
+ static async open(path) {
95
+ const handle = await open(path, "r");
96
+ try {
97
+ const members = await ZipArchive.list(handle);
98
+ return new ZipArchive(handle, members);
99
+ }
100
+ catch (error) {
101
+ await handle.close();
102
+ throw error;
103
+ }
104
+ }
105
+ static async list(handle) {
106
+ const size = (await handle.stat()).size;
107
+ if (size < END_RECORD_BYTES)
108
+ throw new ZipError("The archive has no end record");
109
+ const tailLength = Math.min(size, END_RECORD_BYTES + MAX_COMMENT_BYTES);
110
+ const tail = await readAt(handle, size - tailLength, tailLength);
111
+ let endOffset = -1;
112
+ for (let at = tailLength - END_RECORD_BYTES; at >= 0; at--) {
113
+ if (tail.readUInt32LE(at) !== END_OF_CENTRAL_DIRECTORY)
114
+ continue;
115
+ // The record's comment length must reach the end of the file for it to be the end record.
116
+ if (at + END_RECORD_BYTES + tail.readUInt16LE(at + 20) === tailLength) {
117
+ endOffset = at;
118
+ break;
119
+ }
120
+ }
121
+ if (endOffset < 0)
122
+ throw new ZipError("The archive has no end record");
123
+ const end = tail.subarray(endOffset);
124
+ if (end.readUInt16LE(4) !== 0 || end.readUInt16LE(6) !== 0)
125
+ throw new ZipError("Multi-disk archives are not supported");
126
+ let count = end.readUInt16LE(10);
127
+ let directoryBytes = end.readUInt32LE(12);
128
+ let directoryOffset = end.readUInt32LE(16);
129
+ const endPosition = size - tailLength + endOffset;
130
+ if (count === 0xffff || directoryBytes === 0xffffffff || directoryOffset === 0xffffffff) {
131
+ if (endPosition < 20)
132
+ throw new ZipError("The ZIP64 locator is missing");
133
+ const locator = await readAt(handle, endPosition - 20, 20);
134
+ if (locator.readUInt32LE(0) !== ZIP64_LOCATOR)
135
+ throw new ZipError("The ZIP64 locator is missing");
136
+ const recordOffset = readU64(locator, 8);
137
+ if (recordOffset + 56 > size)
138
+ throw new ZipError("The ZIP64 end record is out of range");
139
+ const record = await readAt(handle, recordOffset, 56);
140
+ if (record.readUInt32LE(0) !== ZIP64_END_OF_CENTRAL_DIRECTORY)
141
+ throw new ZipError("The ZIP64 end record is missing");
142
+ if (record.readUInt32LE(16) !== 0 || record.readUInt32LE(20) !== 0)
143
+ throw new ZipError("Multi-disk archives are not supported");
144
+ count = readU64(record, 32);
145
+ directoryBytes = readU64(record, 40);
146
+ directoryOffset = readU64(record, 48);
147
+ }
148
+ if (count > ZIP_LIMITS.members)
149
+ throw new ZipError(`The archive lists more than ${ZIP_LIMITS.members} members`);
150
+ if (directoryBytes > ZIP_LIMITS.centralDirectoryBytes)
151
+ throw new ZipError("The archive's central directory is too large");
152
+ if (directoryOffset + directoryBytes > size)
153
+ throw new ZipError("The archive's central directory is out of range");
154
+ const directory = await readAt(handle, directoryOffset, directoryBytes);
155
+ const members = [];
156
+ let at = 0;
157
+ const utf8 = new TextDecoder("utf-8");
158
+ for (let index = 0; index < count; index++) {
159
+ if (at + 46 > directory.byteLength)
160
+ throw new ZipError("The archive's central directory is truncated");
161
+ if (directory.readUInt32LE(at) !== CENTRAL_HEADER)
162
+ throw new ZipError("The archive's central directory is malformed");
163
+ const flags = directory.readUInt16LE(at + 8);
164
+ const method = directory.readUInt16LE(at + 10);
165
+ const crc = directory.readUInt32LE(at + 16);
166
+ let compressedSize = directory.readUInt32LE(at + 20);
167
+ let uncompressedSize = directory.readUInt32LE(at + 24);
168
+ const nameLength = directory.readUInt16LE(at + 28);
169
+ const extraLength = directory.readUInt16LE(at + 30);
170
+ const commentLength = directory.readUInt16LE(at + 32);
171
+ const disk = directory.readUInt16LE(at + 34);
172
+ let localHeaderOffset = directory.readUInt32LE(at + 42);
173
+ const nameStart = at + 46;
174
+ const extraStart = nameStart + nameLength;
175
+ const next = extraStart + extraLength + commentLength;
176
+ if (next > directory.byteLength)
177
+ throw new ZipError("The archive's central directory is truncated");
178
+ const nameBytes = directory.subarray(nameStart, extraStart);
179
+ const extra = directory.subarray(extraStart, extraStart + extraLength);
180
+ // ZIP64 fields replace, in order, only the header fields that are at their maximum.
181
+ if (compressedSize === 0xffffffff ||
182
+ uncompressedSize === 0xffffffff ||
183
+ localHeaderOffset === 0xffffffff) {
184
+ let field = 0;
185
+ let found = false;
186
+ while (field + 4 <= extra.byteLength) {
187
+ const id = extra.readUInt16LE(field);
188
+ const length = extra.readUInt16LE(field + 2);
189
+ if (id === ZIP64_EXTRA) {
190
+ let cursor = field + 4;
191
+ const take = () => {
192
+ if (cursor + 8 > field + 4 + length)
193
+ throw new ZipError("A ZIP64 extra field is truncated");
194
+ const value = readU64(extra, cursor);
195
+ cursor += 8;
196
+ return value;
197
+ };
198
+ if (uncompressedSize === 0xffffffff)
199
+ uncompressedSize = take();
200
+ if (compressedSize === 0xffffffff)
201
+ compressedSize = take();
202
+ if (localHeaderOffset === 0xffffffff)
203
+ localHeaderOffset = take();
204
+ found = true;
205
+ break;
206
+ }
207
+ field += 4 + length;
208
+ }
209
+ if (!found)
210
+ throw new ZipError("A member needs a ZIP64 extra field it lacks");
211
+ }
212
+ const path = utf8.decode(nameBytes);
213
+ const member = {
214
+ path,
215
+ size: uncompressedSize,
216
+ compressedSize,
217
+ method,
218
+ crc32: crc,
219
+ localHeaderOffset,
220
+ };
221
+ const unsafe = unsafeZipPath(path);
222
+ if (!(flags & FLAG_UTF8) && nameBytes.some((byte) => byte >= 0x80))
223
+ member.refused = "a member name that is not UTF-8";
224
+ else if (unsafe)
225
+ member.refused = unsafe;
226
+ else if (disk !== 0)
227
+ member.refused = "a member on another disk";
228
+ else if (flags & FLAG_ENCRYPTED)
229
+ member.refused = "an encrypted member";
230
+ else if (method !== METHOD_STORED && method !== METHOD_DEFLATE)
231
+ member.refused = `compression method ${method} (only stored and deflate are read)`;
232
+ else if (uncompressedSize > ZIP_LIMITS.memberBytes)
233
+ member.refused = `a member over ${ZIP_LIMITS.memberBytes} bytes`;
234
+ else if (method === METHOD_STORED
235
+ ? compressedSize !== uncompressedSize
236
+ : compressedSize > deflateBound(uncompressedSize))
237
+ member.refused = "a member whose compressed size does not fit its recorded size";
238
+ else if (localHeaderOffset + 30 > size || compressedSize > size)
239
+ member.refused = "a member outside the archive";
240
+ members.push(member);
241
+ at = next;
242
+ }
243
+ return members;
244
+ }
245
+ /** The member's bytes, decoded and checked against its recorded size and CRC-32. */
246
+ async read(member) {
247
+ if (member.refused)
248
+ throw new ZipError(`The member cannot be read: ${member.refused}`);
249
+ const header = await readAt(this.handle, member.localHeaderOffset, 30);
250
+ if (header.readUInt32LE(0) !== LOCAL_HEADER)
251
+ throw new ZipError("A member's local header is malformed");
252
+ const dataOffset = member.localHeaderOffset + 30 + header.readUInt16LE(26) + header.readUInt16LE(28);
253
+ // The listing refused any member past this bound; a member built elsewhere is held to it
254
+ // before its compressed bytes are allocated.
255
+ if (member.compressedSize > deflateBound(Math.min(member.size, ZIP_LIMITS.memberBytes)))
256
+ throw new ZipError("A member's compressed data exceeds the archive bounds");
257
+ const compressed = await readAt(this.handle, dataOffset, member.compressedSize);
258
+ let bytes;
259
+ if (member.method === METHOD_STORED)
260
+ bytes = compressed;
261
+ else {
262
+ try {
263
+ bytes = inflateRawSync(compressed, { maxOutputLength: Math.max(1, member.size) });
264
+ }
265
+ catch {
266
+ throw new ZipError("A member's compressed data is invalid");
267
+ }
268
+ }
269
+ if (bytes.byteLength !== member.size)
270
+ throw new ZipError("A member's size differs from its recorded size");
271
+ if (crc32(bytes) !== member.crc32)
272
+ throw new ZipError("A member's CRC-32 does not match");
273
+ return new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);
274
+ }
275
+ async close() {
276
+ await this.handle.close();
277
+ }
278
+ }
@@ -0,0 +1,18 @@
1
+ import { type UploadIo } from "./traces-import-upload.js";
2
+ /**
3
+ * `hue traces import`: imports a bundle of existing traces into Hue in two steps that keep the
4
+ * payloads on this machine until the agent has authorized their delivery. `prepare` converts a
5
+ * file, directory or ZIP archive of OTLP/JSON, Langfuse observation or ATIF exports into a
6
+ * bundle and prints its manifest digest and totals; `prepare_trace_import` (Hue MCP) takes
7
+ * those and answers an import authorization; `upload` delivers the bundle with it.
8
+ */
9
+ /** Streams and seams for {@link runTracesCommand}; tests inject these. */
10
+ export interface TracesCommandIo extends UploadIo {
11
+ stdout?: NodeJS.WritableStream;
12
+ stderr?: NodeJS.WritableStream;
13
+ env?: NodeJS.ProcessEnv;
14
+ cwd?: string;
15
+ }
16
+ export declare const TRACES_USAGE = "Usage: hue traces import prepare <path> [--out <directory>] [--json]\n hue traces import upload <directory> --authorization-file <file> [--import-url <url>] [--json]\n\nImport a bundle of existing traces into Hue. Nothing leaves this machine until the upload step,\nand no project key is involved: a Hue MCP connection authorizes the delivery.\n\nprepare Reads a file, a directory or a ZIP archive and recognizes each file by its content:\n OTLP/JSON trace exports ({\"resourceSpans\": [\u2026]}, also one per line), complete Langfuse\n observation exports (the data or observations envelope) and text ATIF v1.7/v1.8 exports.\n Every trace becomes gzip OTLP/JSON batches written to --out (default <path>.hue-import)\n with manifest.json. The report names the manifest_digest and totals that\n prepare_trace_import takes, the files it could not read, invalid vendor ids it mapped\n to OpenTelemetry ids, traces without a history owner and the origin each source\n recorded.\n --out <directory> Where the bundle is written (created with mode 0700)\n --json Print the report as one JSON object\n\nupload Registers the manifest, then sends each trace's values and batches in order, two\n requests at a time, checks every span's receipt, reconciles and completes. Progress is\n kept in <directory>/progress.json, so running the same command again resumes.\n --authorization-file <file> A private file holding what prepare_trace_import answered: its\n JSON (authorization and upload_url), or the authorization\n (hue_it_\u2026) alone on the first line, the upload_url on the second.\n The authorization is never taken from the command line or the\n environment, and never printed.\n --import-url <url> The upload_url prepare_trace_import returned (https://\u2026/api/v1/trace-imports/<id>),\n when the authorization file does not hold it\n --origin <url> Accept only an import URL on this Hue origin\n --json Print one JSON progress object per line on stderr and the report on stdout\n\nExit codes: 0 prepared or completed, 1 not completed (the report says what to do next),\n2 usage error, 130 stopped (run the same command again to resume).";
17
+ /** Runs `hue traces import …`; resolves to the exit code. */
18
+ export declare function runTracesCommand(argv: string[], io?: TracesCommandIo): Promise<number>;
@@ -0,0 +1,231 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+ import { parseArgs } from "node:util";
4
+ import { parseHueOrigin } from "./login.js";
5
+ import { PrepareError, prepareTraceBundle } from "./traces-import-prepare.js";
6
+ import { parseAuthorizationFile, parseImportUrl, UploadStop, uploadTraceBundle, } from "./traces-import-upload.js";
7
+ export const TRACES_USAGE = `Usage: hue traces import prepare <path> [--out <directory>] [--json]
8
+ hue traces import upload <directory> --authorization-file <file> [--import-url <url>] [--json]
9
+
10
+ Import a bundle of existing traces into Hue. Nothing leaves this machine until the upload step,
11
+ and no project key is involved: a Hue MCP connection authorizes the delivery.
12
+
13
+ prepare Reads a file, a directory or a ZIP archive and recognizes each file by its content:
14
+ OTLP/JSON trace exports ({"resourceSpans": […]}, also one per line), complete Langfuse
15
+ observation exports (the data or observations envelope) and text ATIF v1.7/v1.8 exports.
16
+ Every trace becomes gzip OTLP/JSON batches written to --out (default <path>.hue-import)
17
+ with manifest.json. The report names the manifest_digest and totals that
18
+ prepare_trace_import takes, the files it could not read, invalid vendor ids it mapped
19
+ to OpenTelemetry ids, traces without a history owner and the origin each source
20
+ recorded.
21
+ --out <directory> Where the bundle is written (created with mode 0700)
22
+ --json Print the report as one JSON object
23
+
24
+ upload Registers the manifest, then sends each trace's values and batches in order, two
25
+ requests at a time, checks every span's receipt, reconciles and completes. Progress is
26
+ kept in <directory>/progress.json, so running the same command again resumes.
27
+ --authorization-file <file> A private file holding what prepare_trace_import answered: its
28
+ JSON (authorization and upload_url), or the authorization
29
+ (hue_it_…) alone on the first line, the upload_url on the second.
30
+ The authorization is never taken from the command line or the
31
+ environment, and never printed.
32
+ --import-url <url> The upload_url prepare_trace_import returned (https://…/api/v1/trace-imports/<id>),
33
+ when the authorization file does not hold it
34
+ --origin <url> Accept only an import URL on this Hue origin
35
+ --json Print one JSON progress object per line on stderr and the report on stdout
36
+
37
+ Exit codes: 0 prepared or completed, 1 not completed (the report says what to do next),
38
+ 2 usage error, 130 stopped (run the same command again to resume).`;
39
+ class UsageError extends Error {
40
+ }
41
+ const IMPORT_TOKEN = /hue_it_[A-Za-z0-9_-]{43}/g;
42
+ function plural(count, one, many = `${one}s`) {
43
+ return `${count} ${count === 1 ? one : many}`;
44
+ }
45
+ /** Runs `hue traces import …`; resolves to the exit code. */
46
+ export async function runTracesCommand(argv, io = {}) {
47
+ const stdout = io.stdout ?? process.stdout;
48
+ const stderr = io.stderr ?? process.stderr;
49
+ const cwd = io.cwd ?? process.cwd();
50
+ // Every line loses import authorizations and control characters.
51
+ const clean = (line) => line.replace(IMPORT_TOKEN, "[redacted]").replace(/[^\P{Cc}\n\t]|\p{Cf}/gu, " ");
52
+ const out = (line) => void stdout.write(`${clean(line)}\n`);
53
+ const warn = (line) => void stderr.write(`${clean(line)}\n`);
54
+ try {
55
+ if (argv.includes("--help") || argv.includes("-h")) {
56
+ stdout.write(`${TRACES_USAGE}\n`);
57
+ return 0;
58
+ }
59
+ const [group, action, ...rest] = argv;
60
+ if (group !== "import")
61
+ throw new UsageError(`Unknown command: hue traces ${group ?? ""}`.trim());
62
+ if (action === "prepare")
63
+ return await prepare(rest, cwd, out, warn);
64
+ if (action === "upload")
65
+ return await upload(rest, cwd, io, out, warn);
66
+ throw new UsageError(action ? `Unknown command: hue traces import ${action}` : "Name the step: prepare or upload");
67
+ }
68
+ catch (error) {
69
+ if (!(error instanceof UsageError))
70
+ throw error;
71
+ warn(error.message);
72
+ stderr.write(`\n${TRACES_USAGE}\n`);
73
+ return 2;
74
+ }
75
+ }
76
+ function parsePrepareArguments(argv) {
77
+ try {
78
+ return parseArgs({
79
+ args: argv,
80
+ allowPositionals: true,
81
+ strict: true,
82
+ options: {
83
+ out: { type: "string" },
84
+ json: { type: "boolean", default: false },
85
+ },
86
+ });
87
+ }
88
+ catch (error) {
89
+ throw new UsageError(error.message);
90
+ }
91
+ }
92
+ async function prepare(argv, cwd, out, warn) {
93
+ const { values, positionals } = parsePrepareArguments(argv);
94
+ if (positionals.length !== 1)
95
+ throw new UsageError(positionals.length
96
+ ? `Unexpected argument: ${positionals[1]}`
97
+ : "Name the file, directory or archive to prepare");
98
+ const input = resolve(cwd, positionals[0]);
99
+ let report;
100
+ try {
101
+ report = await prepareTraceBundle(input, {
102
+ ...(values.out !== undefined ? { out: resolve(cwd, values.out) } : {}),
103
+ });
104
+ }
105
+ catch (error) {
106
+ if (error instanceof PrepareError) {
107
+ warn(error.message);
108
+ return 1;
109
+ }
110
+ if (error.code === "ENOENT") {
111
+ warn(`${input} does not exist`);
112
+ return 1;
113
+ }
114
+ throw error;
115
+ }
116
+ if (values.json) {
117
+ out(JSON.stringify(report));
118
+ return 0;
119
+ }
120
+ out(`Prepared ${plural(report.totals.traces, "trace")} (${plural(report.totals.spans, "span")}, ${plural(report.totals.batches, "batch", "batches")}, ${plural(report.totals.blobs, "large value")}) in ${report.output_directory}.`);
121
+ out(`Formats: ${Object.entries(report.formats)
122
+ .filter(([, count]) => count)
123
+ .map(([format, count]) => `${format} ${count}`)
124
+ .join(", ")}.`);
125
+ out(`Manifest digest: ${report.manifest_digest}`);
126
+ out(`Totals: ${JSON.stringify(report.totals)}`);
127
+ out(`Source: ${JSON.stringify(report.source)}`);
128
+ if (report.traces.without_identity)
129
+ out(`${plural(report.traces.without_identity, "trace names", "traces name")} no history owner.`);
130
+ const origins = Object.entries(report.traces.by_origin).filter(([, count]) => count);
131
+ out(`Origin: ${origins.map(([origin, count]) => `${origin} ${count}`).join(", ")}.`);
132
+ if (report.traces.remapped.traces)
133
+ out(`Mapped invalid vendor ids to OpenTelemetry ids in ${plural(report.traces.remapped.traces, "trace")} (${plural(report.traces.remapped.spans, "span")}).`);
134
+ for (const file of report.files.unsupported)
135
+ warn(`Not read: ${file.file}: ${file.reason}.`);
136
+ for (const refusal of report.traces.refused)
137
+ warn(`Not converted: ${[refusal.file, refusal.trace_id ? `trace ${refusal.trace_id}` : undefined].filter(Boolean).join(" ")}: ${refusal.reason}.`);
138
+ out("Next: call prepare_trace_import with manifest_digest, totals and source, write the authorization to a private file and run hue traces import upload.");
139
+ return 0;
140
+ }
141
+ function parseUploadArguments(argv) {
142
+ try {
143
+ return parseArgs({
144
+ args: argv,
145
+ allowPositionals: true,
146
+ strict: true,
147
+ options: {
148
+ "authorization-file": { type: "string" },
149
+ "import-url": { type: "string" },
150
+ origin: { type: "string" },
151
+ json: { type: "boolean", default: false },
152
+ },
153
+ });
154
+ }
155
+ catch (error) {
156
+ throw new UsageError(error.message);
157
+ }
158
+ }
159
+ async function upload(argv, cwd, io, out, warn) {
160
+ const { values, positionals } = parseUploadArguments(argv);
161
+ if (positionals.length !== 1)
162
+ throw new UsageError(positionals.length
163
+ ? `Unexpected argument: ${positionals[1]}`
164
+ : "Name the prepared directory to upload");
165
+ if (!values["authorization-file"])
166
+ throw new UsageError("--authorization-file is required: the file holding the import authorization");
167
+ let origin;
168
+ if (values.origin !== undefined) {
169
+ const parsed = parseHueOrigin(values.origin);
170
+ if (!parsed)
171
+ throw new UsageError("--origin must be an HTTPS origin such as https://app.hue.run (plain HTTP is accepted for loopback test servers only)");
172
+ origin = parsed;
173
+ }
174
+ const directory = resolve(cwd, positionals[0]);
175
+ let token;
176
+ let importUrl = values["import-url"];
177
+ try {
178
+ const content = await readFile(resolve(cwd, values["authorization-file"]), "utf8").catch(() => {
179
+ throw new UploadStop(`${values["authorization-file"]} could not be read`, "authorization_invalid");
180
+ });
181
+ const parsed = parseAuthorizationFile(content);
182
+ token = parsed.token;
183
+ importUrl ??= parsed.importUrl;
184
+ if (!importUrl)
185
+ throw new UploadStop("Pass --import-url, or put the upload_url prepare_trace_import returned on the authorization file's second line", "url_invalid");
186
+ importUrl = parseImportUrl(importUrl, origin).href;
187
+ }
188
+ catch (error) {
189
+ if (!(error instanceof UploadStop))
190
+ throw error;
191
+ throw new UsageError(error.message);
192
+ }
193
+ const json = values.json;
194
+ const progress = (event) => {
195
+ if (json)
196
+ warn(JSON.stringify(event));
197
+ else if (event.event === "manifest.registered")
198
+ out(`Registered the manifest with import ${String(event.import_id)}.`);
199
+ else if (event.event === "trace.conflicted")
200
+ warn(`Trace ${String(event.trace_id)} conflicts with evidence Hue already holds.`);
201
+ else if (event.event === "trace.unverified")
202
+ warn(`Trace ${String(event.trace_id)}: ${String(event.missing_spans)} span(s) not yet confirmed.`);
203
+ else if (event.event === "trace.verified" && Number(event.index) % 100 === 0)
204
+ out(`Verified ${String(event.index)} of ${String(event.of)} traces.`);
205
+ };
206
+ let report;
207
+ try {
208
+ report = await uploadTraceBundle({ directory, token, importUrl }, { ...io, progress });
209
+ }
210
+ catch (error) {
211
+ if (!(error instanceof UploadStop))
212
+ throw error;
213
+ warn(error.message);
214
+ return 1;
215
+ }
216
+ if (json)
217
+ out(JSON.stringify(report));
218
+ else {
219
+ out(`${report.state === "completed" ? "Completed" : report.state === "stopped" ? "Stopped" : report.state === "failed" ? "Failed" : "Not completed"}: ${report.traces.verified} of ${plural(report.traces.declared, "trace")} verified, ${report.traces.conflicted} conflicted, ${report.batches.landed} of ${plural(report.batches.declared, "batch", "batches")} landed, ${report.blobs.stored} of ${plural(report.blobs.declared, "large value")} stored.`);
220
+ for (const conflict of report.conflicts.slice(0, 20))
221
+ warn(` ${conflict.trace_id} ${conflict.sha256}: ${conflict.message}`);
222
+ if (report.conflicts.length > 20)
223
+ warn(` … and ${report.conflicts.length - 20} more`);
224
+ if (report.error)
225
+ warn(`${report.error.message}`);
226
+ if (report.note)
227
+ out(report.note);
228
+ out(report.next_action);
229
+ }
230
+ return report.state === "completed" ? 0 : report.state === "stopped" ? 130 : 1;
231
+ }
package/dist/setup/cli.js CHANGED
@@ -32,6 +32,10 @@ const extensions = new Map([
32
32
  "listen",
33
33
  async () => (await loadCommand("listen", () => import("../cli/listen.js"))).runListenCommand(process.argv.slice(3)),
34
34
  ],
35
+ [
36
+ "traces",
37
+ async () => (await loadCommand("traces", () => import("../cli/traces-import.js"))).runTracesCommand(process.argv.slice(3)),
38
+ ],
35
39
  ]);
36
40
  function writeEvent(event, mode, width) {
37
41
  const line = mode === "jsonl"
@@ -74,7 +78,7 @@ async function main() {
74
78
  }
75
79
  catch {
76
80
  if (!agentRequested) {
77
- process.stderr.write("Usage: hue <setup|resume|status|claim|login|eval|mcp|listen|import> [--agent|--format human|plain|jsonl] [--project PATH] [--origin URL] [--restart]\n");
81
+ process.stderr.write("Usage: hue <setup|resume|status|claim|login|eval|mcp|listen|import|traces> [--agent|--format human|plain|jsonl] [--project PATH] [--origin URL] [--restart]\n");
78
82
  return 2;
79
83
  }
80
84
  const event = {
@@ -105,7 +109,7 @@ async function main() {
105
109
  process.stdout.write(`${renderJsonlEvent(event)}\n`);
106
110
  return 2;
107
111
  }
108
- process.stdout.write("Usage: hue <setup|resume|status|claim|login|eval|mcp|listen|import> [--agent|--format human|plain|jsonl] [--project PATH] [--origin URL] [--restart]\n");
112
+ process.stdout.write("Usage: hue <setup|resume|status|claim|login|eval|mcp|listen|import|traces> [--agent|--format human|plain|jsonl] [--project PATH] [--origin URL] [--restart]\n");
109
113
  return 0;
110
114
  }
111
115
  const command = parsed.positionals[0];
@@ -137,7 +141,7 @@ async function main() {
137
141
  process.stdout.write(`${renderJsonlEvent(event)}\n`);
138
142
  }
139
143
  else
140
- process.stderr.write("Usage: hue <setup|resume|status|claim|login|eval|mcp|listen|import> [--agent|--format human|plain|jsonl] [--project PATH] [--origin URL] [--restart]\n");
144
+ process.stderr.write("Usage: hue <setup|resume|status|claim|login|eval|mcp|listen|import|traces> [--agent|--format human|plain|jsonl] [--project PATH] [--origin URL] [--restart]\n");
141
145
  return 2;
142
146
  }
143
147
  const mode = selectSetupOutputMode({
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.20.1";
2
+ export declare const sdkVersion = "0.20.2";
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // Generated by scripts/write-version.mjs from package.json; do not edit by hand.
2
2
  /** Package version shared by the instrumentation scope and the export User-Agent. */
3
- export const sdkVersion = "0.20.1";
3
+ export const sdkVersion = "0.20.2";
@@ -16,8 +16,9 @@ export declare function worldNow(source?: WorldHandoff | Record<string, string |
16
16
  /** Variables that authenticate against Hue's control plane rather than a simulated provider. */
17
17
  export declare const HUE_CONTROL_PLANE_VARIABLES: readonly string[];
18
18
  /** Prefixes of the Hue control-plane credentials the server issues: project and setup keys,
19
- * attempt grants, the project MCP key, invocation and installation tokens, and OAuth tokens and
20
- * secrets. World tokens (`hue_world_`) and MCP capabilities (`hue_sim_`) are what the agent holds. */
19
+ * attempt grants, the project MCP key, invocation and installation tokens, OAuth tokens, trace
20
+ * import authorizations and secrets. World tokens (`hue_world_`) and MCP capabilities
21
+ * (`hue_sim_`) are what the agent holds. */
21
22
  export declare const HUE_CONTROL_PLANE_CREDENTIAL_PREFIXES: readonly string[];
22
23
  /** True for a control-plane variable by name, or for a value carrying a Hue control-plane
23
24
  * credential anywhere in it, including wrapped, percent-encoded or JSON-Unicode-escaped values. */
@@ -53,8 +53,9 @@ export const HUE_CONTROL_PLANE_VARIABLES = [
53
53
  "HUE_SERVICE_KEY",
54
54
  ];
55
55
  /** Prefixes of the Hue control-plane credentials the server issues: project and setup keys,
56
- * attempt grants, the project MCP key, invocation and installation tokens, and OAuth tokens and
57
- * secrets. World tokens (`hue_world_`) and MCP capabilities (`hue_sim_`) are what the agent holds. */
56
+ * attempt grants, the project MCP key, invocation and installation tokens, OAuth tokens, trace
57
+ * import authorizations and secrets. World tokens (`hue_world_`) and MCP capabilities
58
+ * (`hue_sim_`) are what the agent holds. */
58
59
  export const HUE_CONTROL_PLANE_CREDENTIAL_PREFIXES = [
59
60
  "hue_sk_",
60
61
  "hue_attempt_",
@@ -64,6 +65,7 @@ export const HUE_CONTROL_PLANE_CREDENTIAL_PREFIXES = [
64
65
  "hue_install_",
65
66
  "hue_at_",
66
67
  "hue_rt_",
68
+ "hue_it_",
67
69
  "hue_oauth_",
68
70
  ];
69
71
  /** A Hue credential anywhere in a value, including JSON Unicode escapes, not inside a longer word. */