@hue-run/sdk 0.18.0 → 0.18.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/dist/blobs.js CHANGED
@@ -3,6 +3,7 @@ import { context } from "@opentelemetry/api";
3
3
  import { suppressTracing } from "@opentelemetry/core";
4
4
  import { isLoopbackHost } from "./config.js";
5
5
  import { digestPart, hashInlineFiles, hashNestedInlineFiles, INLINE_FILE_LIMIT, INLINE_FILE_TEXT_PER_RECORD, isMessageKey, keepsPrefix, literalKeys, mediaBytes, mediaContentType, mediaIn, isTextMediaType, mayInlineFiles, isMediaContentType, MAX_INLINE_FILE_TEXT, placeable, placedMediaText, prefixPart, readMediaText, reencodeMedia, } from "./inline-files.js";
6
+ import { isByteSource, JoinedText, longFileBytes, longHead, placedLargeText, readLargeMediaText, } from "./large-media.js";
6
7
  import { capLogBody, TRUNCATED_KEY, truncatedMarker, withTruncatedKeys } from "./privacy.js";
7
8
  import { truncateUtf8 } from "./safety.js";
8
9
  /**
@@ -99,40 +100,53 @@ export function blobPrefix(value) {
99
100
  return Buffer.from(truncateUtf8(value, BLOB_PREFIX_BYTES), "utf8").toString("utf8");
100
101
  }
101
102
  /** A value's size in bytes as it is uploaded: a string's UTF-8 encoding, counted, not made. */
102
- function sizeOf(value) {
103
- return typeof value === "string" ? Buffer.byteLength(value, "utf8") : value.byteLength;
103
+ export function sizeOf(value) {
104
+ if (typeof value === "string")
105
+ return Buffer.byteLength(value, "utf8");
106
+ return isByteSource(value) ? value.size : value.byteLength;
107
+ }
108
+ /** The bytes a value holds in memory: none for a source read as it is uploaded. */
109
+ export function heldBytesOf(value) {
110
+ return isByteSource(value) ? 0 : value.byteLength;
104
111
  }
105
112
  /** A string's UTF-8 encoding a slice at a time, never splitting a surrogate pair. */
106
113
  function* utf8Chunks(text) {
114
+ // One buffer, reused: a reader is done with each slice before it asks for the next.
115
+ const buffer = Buffer.allocUnsafe(TEXT_CHUNK_UNITS * 3);
107
116
  for (let start = 0; start < text.length;) {
108
117
  let end = Math.min(text.length, start + TEXT_CHUNK_UNITS);
109
118
  if (end < text.length && /[\uD800-\uDBFF]/.test(text[end - 1]))
110
119
  end--;
111
- yield Buffer.from(text.slice(start, end), "utf8");
120
+ yield buffer.subarray(0, buffer.write(text.slice(start, end), "utf8"));
112
121
  start = end;
113
122
  }
114
123
  }
115
124
  /** The bytes an upload carries, a slice at a time: no second copy of a large value is made. */
116
- function* uploadChunks(value) {
117
- if (typeof value === "string") {
118
- yield* utf8Chunks(value);
119
- return;
120
- }
121
- for (let start = 0; start < value.byteLength; start += TEXT_CHUNK_UNITS)
122
- yield value.subarray(start, Math.min(value.byteLength, start + TEXT_CHUNK_UNITS));
125
+ function uploadChunks(value) {
126
+ if (isByteSource(value))
127
+ return value.chunks();
128
+ return (function* () {
129
+ if (typeof value === "string") {
130
+ yield* utf8Chunks(value);
131
+ return;
132
+ }
133
+ for (let start = 0; start < value.byteLength; start += TEXT_CHUNK_UNITS)
134
+ yield value.subarray(start, Math.min(value.byteLength, start + TEXT_CHUNK_UNITS));
135
+ })();
123
136
  }
124
137
  const nextTurn = () => new Promise((resolve) => setImmediate(resolve));
125
138
  /** The SHA-256 (lowercase hex) and byte size of what an upload of `value` carries, yielding to
126
- * the event loop between slices so a large value does not stall the application. */
127
- export async function digestOf(value) {
139
+ * the event loop between slices so a large value does not stall the application. Past
140
+ * `deadline`, it throws rather than read on. */
141
+ export async function digestOf(value, deadline = Infinity) {
128
142
  const hash = createHash("sha256");
129
143
  let size = 0;
130
- let slices = 0;
131
- for (const chunk of uploadChunks(value)) {
144
+ for await (const chunk of uploadChunks(value)) {
132
145
  hash.update(chunk);
133
146
  size += chunk.byteLength;
134
- if (++slices % 4 === 0)
135
- await nextTurn();
147
+ await nextTurn();
148
+ if (Date.now() >= deadline)
149
+ throw new Error("The upload ran out of time");
136
150
  }
137
151
  return { sha256: hash.digest("hex"), size };
138
152
  }
@@ -258,9 +272,12 @@ export class BlobUploader {
258
272
  if (digest.size > MAX_BLOB_BYTES)
259
273
  return { ok: false, reason: "tooLarge" };
260
274
  const identity = `${traceId}:${digest.sha256}`;
261
- const known = this.outcomes.get(identity);
262
- if (known)
263
- return known.promise;
275
+ for (let known = this.outcomes.get(identity); known; known = this.outcomes.get(identity)) {
276
+ const shared = await known.promise;
277
+ // Another reference's file changed since it was recorded: this one tries its own bytes.
278
+ if (shared.ok || !shared.changed)
279
+ return shared;
280
+ }
264
281
  if (this.outcomes.size >= 4096) {
265
282
  const oldest = [...this.outcomes].find(([, item]) => item.settled)?.[0];
266
283
  if (oldest === undefined)
@@ -308,6 +325,8 @@ export class BlobUploader {
308
325
  const put = await this.put(grant, value, digest.size, upload.deadline);
309
326
  if (put === "stored")
310
327
  break;
328
+ if (put === "changed")
329
+ return { ok: false, reason: "failed", changed: true };
311
330
  if (this.closed)
312
331
  return { ok: false, reason: "failed" };
313
332
  const expired = put === "expired" || Date.now() >= grant.expiresAt;
@@ -464,14 +483,21 @@ export class BlobUploader {
464
483
  grant: { url, headers, expiresAt: Number.isFinite(expiresAt) ? expiresAt : Infinity },
465
484
  };
466
485
  }
467
- /** The PUT of the value's bytes, with exactly the headers Hue signed. */
486
+ /** The PUT of the value's bytes, with exactly the headers Hue signed. A source that no longer
487
+ * holds its bytes (a file changed since it was recorded) ends the request before its body is
488
+ * complete, and fails the value without a retry. */
468
489
  async put(grant, value, size, deadline) {
469
490
  const reply = await this.request("PUT", grant.url, grant.headers, value, deadline, MAX_ERROR_RESPONSE_BYTES, size);
491
+ if (isByteSource(value) && value.changed)
492
+ return "changed";
470
493
  if (!reply)
471
494
  return "transient";
472
- // 412: the object exists already (another upload of the same value won).
473
- if ((reply.status >= 200 && reply.status < 300) || reply.status === 412)
495
+ // 412: the object exists already (another upload of the same value won). A success answered
496
+ // before the whole body was written stores nothing that was sent.
497
+ if (reply.status === 412)
474
498
  return "stored";
499
+ if (reply.status >= 200 && reply.status < 300)
500
+ return reply.complete ? "stored" : "failed";
475
501
  if (reply.status === 403)
476
502
  return reply.body.toString("utf8").includes("Request has expired") ? "expired" : "failed";
477
503
  if (reply.status >= 500 || reply.status === 408 || reply.status === 429)
@@ -501,6 +527,8 @@ export class BlobUploader {
501
527
  return undefined;
502
528
  return new Promise((resolve) => {
503
529
  let settled = false;
530
+ // Whether the whole body was handed to the request.
531
+ let ended = false;
504
532
  let request;
505
533
  const finish = (reply) => {
506
534
  if (settled)
@@ -509,16 +537,20 @@ export class BlobUploader {
509
537
  clearTimeout(timer);
510
538
  if (request) {
511
539
  this.requests.delete(request);
512
- if (!reply)
540
+ // An answer before the whole body was handed over ends the request: a store that
541
+ // neither reads nor closes would otherwise hold it, and its source, open.
542
+ if (!reply || !ended)
513
543
  request.destroy();
514
544
  }
515
545
  resolve(reply);
516
546
  };
517
547
  const timer = setTimeout(() => finish(), Math.max(1, deadline - Date.now()));
548
+ // The bytes handed to the request: its body is complete once they are its length.
549
+ let sentBytes = 0;
550
+ const written = () => length === undefined || sentBytes === length;
518
551
  const requestHeaders = { ...headers };
519
552
  if (length === undefined)
520
- requestHeaders["Content-Length"] =
521
- typeof body === "string" ? Buffer.byteLength(body, "utf8") : body.byteLength;
553
+ requestHeaders["Content-Length"] = sizeOf(body);
522
554
  try {
523
555
  // Uploads are the SDK's own export traffic: an http instrumentation must not trace them.
524
556
  request = context.with(suppressTracing(context.active()), () => http.request(url, { method, headers: requestHeaders, agent: false }, (response) => {
@@ -528,6 +560,7 @@ export class BlobUploader {
528
560
  status: response.statusCode ?? 0,
529
561
  headers: response.headers,
530
562
  body: Buffer.concat(chunks),
563
+ complete: written(),
531
564
  });
532
565
  response.on("data", (chunk) => {
533
566
  size += chunk.byteLength;
@@ -552,26 +585,41 @@ export class BlobUploader {
552
585
  sent.on("error", () => finish());
553
586
  sent.setTimeout(this.options.timeoutMillis, () => finish());
554
587
  void (async () => {
555
- if (length === undefined) {
588
+ if (length === undefined && !isByteSource(body)) {
589
+ ended = true;
556
590
  sent.end(body);
557
591
  return;
558
592
  }
559
- for (const chunk of uploadChunks(body)) {
593
+ // A source or a text may reuse a slice's memory for the next: each slice is written out
594
+ // before the next is read. Bytes are views of memory that does not change.
595
+ const reused = !(body instanceof Uint8Array);
596
+ for await (const chunk of uploadChunks(body)) {
560
597
  if (settled)
561
598
  return;
562
- if (!sent.write(chunk))
563
- await new Promise((drained) => {
564
- const done = () => {
565
- sent.off("drain", done);
566
- sent.off("close", done);
567
- drained();
568
- };
599
+ sentBytes += chunk.byteLength;
600
+ await new Promise((proceed) => {
601
+ const done = () => {
602
+ sent.off("drain", done);
603
+ sent.off("close", done);
604
+ proceed();
605
+ };
606
+ sent.on("close", done);
607
+ if (reused) {
608
+ sent.write(chunk, () => done());
609
+ return;
610
+ }
611
+ if (sent.write(chunk))
612
+ done();
613
+ else
569
614
  sent.on("drain", done);
570
- sent.on("close", done);
571
- });
615
+ });
616
+ if (settled)
617
+ return;
572
618
  }
573
- if (!settled)
574
- sent.end();
619
+ if (settled)
620
+ return;
621
+ ended = true;
622
+ sent.end();
575
623
  })().catch(() => finish());
576
624
  });
577
625
  }
@@ -615,30 +663,46 @@ export async function offloadRecord(attributes, collector, options) {
615
663
  * uploaded keeps its bytes inline, except in a recorded message: a span's own message keeps its
616
664
  * digest, and a log record's keeps the digest of a file over the inline limit, as nested
617
665
  * messages kept them before log records uploaded. */
618
- const placeFiles = async (index, key, files) => {
666
+ const placeFiles = async (index, key, files, long) => {
619
667
  const message = isMessageKey(key);
620
668
  let changed = false;
621
669
  await Promise.all(files.map((file, position) => {
622
- const decoded = mediaBytes(file);
623
- if (!placeable(file, decoded))
670
+ // A file whose content is a long string is read from its place in the recorded text, a
671
+ // slice at a time: never decoded or copied whole.
672
+ const inPlace = long?.get(file.content);
673
+ const decoded = inPlace ? longFileBytes(file, inPlace) : mediaBytes(file);
674
+ const bytes = "source" in decoded ? decoded.source : decoded.bytes;
675
+ const size = sizeOf(bytes);
676
+ if (!placeable(file, decoded, size))
624
677
  return Promise.resolve();
625
678
  const contentType = mediaContentType(file, decoded);
626
679
  const textual = isTextMediaType(contentType);
627
- if (textual && decoded.bytes.byteLength <= INLINE_FILE_LIMIT)
680
+ if (textual && size <= INLINE_FILE_LIMIT)
628
681
  return Promise.resolve();
629
- const prefix = keepsPrefix(file, textual);
682
+ const prefix = keepsPrefix(file, textual, decoded);
630
683
  const partKey = `${key}#${prefix ? file.pointer : file.partPointer}`;
631
684
  const slot = slots > 0 && isBlobKey(partKey) ? (slots--, true) : false;
632
685
  return (async () => {
633
- const digest = options.memo
634
- ? options.memo.digest(file, decoded, traceId)
635
- : await digestOf(decoded.bytes);
686
+ // Hashing a long file takes an upload place, as hashing a large value does; one the
687
+ // export's upload budget cannot hash stays as recorded and is not uploaded.
688
+ let digest;
689
+ try {
690
+ digest = isByteSource(bytes)
691
+ ? await uploader.limit(() => digestOf(bytes, upload.deadline))
692
+ : options.memo
693
+ ? options.memo.digest(file, decoded, traceId)
694
+ : await digestOf(bytes);
695
+ }
696
+ catch {
697
+ fallback("failed");
698
+ return;
699
+ }
636
700
  const outcome = slot
637
- ? await uploader.store(traceId, partKey, decoded.bytes, contentType, digest, upload)
701
+ ? await uploader.store(traceId, partKey, bytes, contentType, digest, upload)
638
702
  : { ok: false, reason: "budget" };
639
703
  if (outcome.ok) {
640
704
  if (prefix)
641
- prefixPart(file, blobPrefix(file.content));
705
+ prefixPart(file, blobPrefix(inPlace ? longHead(inPlace, BLOB_PREFIX_BYTES + 1) : file.content));
642
706
  else
643
707
  digestPart(file, digest);
644
708
  changed = true;
@@ -649,7 +713,7 @@ export async function offloadRecord(attributes, collector, options) {
649
713
  tally.uploaded++;
650
714
  }
651
715
  else {
652
- if (message && (!collector.log || decoded.bytes.byteLength > INLINE_FILE_LIMIT)) {
716
+ if (message && (!collector.log || size > INLINE_FILE_LIMIT)) {
653
717
  digestPart(file, digest);
654
718
  changed = true;
655
719
  }
@@ -661,30 +725,59 @@ export async function offloadRecord(attributes, collector, options) {
661
725
  };
662
726
  const literal = literalKeys(attributes);
663
727
  const parts = async (index, key, text) => {
664
- // Both bounds count UTF-16 code units, as admission does. Literal text holds no file.
665
- if (!mayInlineFiles(key, text) || text.length > MAX_INLINE_FILE_TEXT || literal.has(key))
728
+ // Literal text holds no file.
729
+ if (!mayInlineFiles(key, text) || literal.has(key))
666
730
  return text;
667
- if (collector.inspected) {
668
- // A value admission skipped stays as recorded.
669
- if (!collector.inspected.has(key))
670
- return text;
731
+ let media;
732
+ let large;
733
+ if (collector.large?.has(key)) {
734
+ // Held whole past the inspection bounds: its long strings were read in place, the JSON
735
+ // around them within the per-attribute bound.
736
+ media = large = largeReads.get(index);
671
737
  }
672
738
  else {
673
- if (inspected + text.length > INLINE_FILE_TEXT_PER_RECORD)
739
+ // Both bounds count UTF-16 code units, as admission does.
740
+ if (text.length > MAX_INLINE_FILE_TEXT)
674
741
  return text;
675
- inspected += text.length;
742
+ if (collector.inspected) {
743
+ // A value admission skipped stays as recorded.
744
+ if (!collector.inspected.has(key))
745
+ return text;
746
+ }
747
+ else {
748
+ if (inspected + text.length > INLINE_FILE_TEXT_PER_RECORD)
749
+ return text;
750
+ inspected += text.length;
751
+ }
752
+ try {
753
+ media = readMediaText(key, text);
754
+ }
755
+ catch {
756
+ return text;
757
+ }
676
758
  }
677
- let media;
759
+ if (!media?.files.length)
760
+ return text;
761
+ if (!(await placeFiles(index, key, media.files, large?.long)))
762
+ return text;
763
+ return large ? await placedLargeText(large) : placedMediaText(media);
764
+ };
765
+ // The values read in place are scanned first, in candidate order, between turns of the event
766
+ // loop: places in `hue.blobs` are then taken in candidate order, as for any value.
767
+ const largeReads = new Map();
768
+ for (const [index, { key, value }] of collector.candidates.entries()) {
769
+ if (typeof value !== "string" ||
770
+ !collector.large?.has(key) ||
771
+ !mayInlineFiles(key, value) ||
772
+ literal.has(key))
773
+ continue;
678
774
  try {
679
- media = readMediaText(key, text);
775
+ largeReads.set(index, await readLargeMediaText(key, value, options.longUnits));
680
776
  }
681
777
  catch {
682
- return text;
778
+ // JSON that does not parse holds no file: the value stays as recorded.
683
779
  }
684
- if (!media?.files.length)
685
- return text;
686
- return (await placeFiles(index, key, media.files)) ? placedMediaText(media) : text;
687
- };
780
+ }
688
781
  /** A log record's body with its files placed. Each file's content counts toward the record's
689
782
  * inspection bounds, as an attribute's text does. */
690
783
  const placeBody = async (body) => {
@@ -710,14 +803,22 @@ export async function offloadRecord(attributes, collector, options) {
710
803
  return media.box.value;
711
804
  };
712
805
  const place = async (index, { key, value }) => {
713
- const placed = typeof value === "string" ? await parts(index, key, value) : value;
714
- const size = sizeOf(placed);
806
+ const read = typeof value === "string" ? await parts(index, key, value) : value;
807
+ // Text joined from pieces of the recorded text is made one string only when it is kept
808
+ // inline; otherwise it is measured, hashed, uploaded and cut from its pieces.
809
+ const joined = read instanceof JoinedText ? read : undefined;
810
+ const size = joined ? joined.utf8Size() : sizeOf(read);
715
811
  if (size <= valueBytes) {
716
- result[key] = placed;
812
+ result[key] = joined ? joined.text() : read;
717
813
  return;
718
814
  }
815
+ const placed = joined ? joined.source(size) : read;
719
816
  listed.push({ order: index, key });
720
- const cut = () => typeof placed === "string" ? truncateUtf8(placed, valueBytes) : truncatedMarker(size);
817
+ const cut = () => joined
818
+ ? truncateUtf8(joined.head(valueBytes + 1), valueBytes)
819
+ : typeof placed === "string"
820
+ ? truncateUtf8(placed, valueBytes)
821
+ : truncatedMarker(size);
721
822
  // A log record's value is cut, as no log reader pages an uploaded text value.
722
823
  if (collector.log) {
723
824
  result[key] = cut();
@@ -733,13 +834,27 @@ export async function offloadRecord(attributes, collector, options) {
733
834
  if (size > MAX_BLOB_BYTES)
734
835
  return { ok: false, reason: "tooLarge" };
735
836
  // Hashing a large value takes a slot as its upload does; the upload takes its own.
736
- const digest = await uploader.limit(() => digestOf(placed));
737
- const contentType = typeof placed === "string" ? textContentType(placed) : "application/octet-stream";
837
+ let digest;
838
+ try {
839
+ digest = await uploader.limit(() => digestOf(placed, upload.deadline));
840
+ }
841
+ catch {
842
+ return { ok: false, reason: "failed" };
843
+ }
844
+ const contentType = joined
845
+ ? textContentType(joined.head(256) + joined.tail(256))
846
+ : typeof placed === "string"
847
+ ? textContentType(placed)
848
+ : "application/octet-stream";
738
849
  const outcome = await uploader.store(traceId, key, placed, contentType, digest, upload);
739
850
  return outcome.ok ? { ...outcome, digest } : outcome;
740
851
  })();
741
852
  if (stored.ok) {
742
- result[key] = typeof placed === "string" ? blobPrefix(placed) : blobPrefix(placed);
853
+ result[key] = joined
854
+ ? blobPrefix(joined.head(BLOB_PREFIX_BYTES + 1))
855
+ : typeof placed === "string"
856
+ ? blobPrefix(placed)
857
+ : blobPrefix(placed);
743
858
  entries.push({
744
859
  order: [index, Infinity],
745
860
  entry: blobEntry(key, stored.digest.sha256, stored.digest.size, stored.contentType),
@@ -754,7 +869,7 @@ export async function offloadRecord(attributes, collector, options) {
754
869
  const body = collector.body ? placeBody(collector.body.value) : undefined;
755
870
  await Promise.all(collector.candidates.map((candidate, index) => place(index, candidate)));
756
871
  await Promise.all((collector.attachments ?? []).map((file, position) => {
757
- if (!file.bytes.byteLength)
872
+ if (!sizeOf(file.bytes))
758
873
  return Promise.resolve();
759
874
  const slot = slots > 0 ? (slots--, true) : false;
760
875
  return (async () => {
@@ -0,0 +1,35 @@
1
+ /**
2
+ * `hue import langfuse`: copies a Langfuse project's traces into Hue from this machine. Langfuse
3
+ * is read with the project's own keys through its public API; each observation becomes the span
4
+ * the Langfuse OpenTelemetry SDKs would have recorded and is exported through the SDK's own
5
+ * transport, so batching, retries, limits, content capture and large-value uploads are the SDK's.
6
+ * Ids are kept or derived from Langfuse's, so a second run sends the same spans again and Hue
7
+ * keeps one copy of each: an interrupted or partly failed import is resumed by running it again.
8
+ */
9
+ /** Streams, environment and timing seams for {@link runImportCommand}; tests inject these. */
10
+ export interface ImportCommandIo {
11
+ stdout?: NodeJS.WritableStream;
12
+ stderr?: NodeJS.WritableStream;
13
+ stdin?: NodeJS.ReadableStream & {
14
+ isTTY?: boolean;
15
+ };
16
+ env?: NodeJS.ProcessEnv;
17
+ cwd?: string;
18
+ fetch?: typeof fetch;
19
+ /** Waits `ms`, resolving early when the import is stopped. */
20
+ sleep?: (ms: number, signal: AbortSignal) => Promise<void>;
21
+ /** Registers the stop request (SIGINT and SIGTERM by default) and returns its removal. */
22
+ onInterrupt?: (listener: () => void) => () => void;
23
+ /** The current time, for the default end of the window. */
24
+ now?: () => Date;
25
+ /** Ends the process after a second stop request (`process.exit` by default), so exports still
26
+ * in flight do not hold it until their requests time out. */
27
+ exit?: (code: number) => void;
28
+ }
29
+ export declare const IMPORT_USAGE = "Usage: hue import langfuse [options]\n\nCopy traces from a Langfuse project into Hue. The import runs on this machine with your own\nLangfuse keys, which are sent only to Langfuse. Each observation becomes a span with the\nlangfuse.* attributes Hue reads; ids are kept, so running the same command again resumes an\nimport without duplicating what was already sent.\n\nLangfuse (keys from the environment or --env-path; the secret key never from the command line):\n LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY The Langfuse project's API keys\n LANGFUSE_BASE_URL or LANGFUSE_HOST Langfuse host (default https://cloud.langfuse.com)\n --langfuse-host URL Langfuse host, instead of the variables\n --langfuse-public-key KEY Public key, instead of LANGFUSE_PUBLIC_KEY\n\nHue:\n HUE_API_KEY The Hue project key the traces are sent with (environment, --env-path\n or ./.env.hue); not needed with --dry-run\n --origin URL Hue origin (default HUE_BASE_URL, then https://app.hue.run)\n --service-name NAME service.name of the imported spans (default langfuse)\n\nSelection (newest first):\n --from TIME Traces that started at or after TIME: an ISO 8601 date (midnight UTC)\n or a date and time with a zone, such as 2026-01-01T09:00:00Z\n --to TIME Traces that started before TIME (default: an hour before the command\n starts, so traces still running are left for a later run)\n --limit N At most N traces\n --trace-id ID This Langfuse trace (repeatable); not with --from, --to or --limit\n\nContent:\n --media Download the files Langfuse media references (@@@langfuseMedia:\u2026@@@)\n point to and upload them to Hue; without it, references stay as text\n --no-content Send metadata only: no inputs, outputs, status messages or model\n parameters (content is sent by default, as the SDK records it)\n\nRun:\n --dry-run Read Langfuse and count what would be imported; send nothing to Hue\n --yes Import without asking for confirmation (required without a terminal)\n --env-path PATH Load a dotenv file first (--env-file also works when it exists);\n ./.env.hue loads when HUE_API_KEY is not set. Variables already set win\n -h, --help Show this help\n\nExit codes: 0 every trace was imported, 1 a trace, span or file was not, 2 usage error,\n130 stopped (run the same command again to resume).";
30
+ /** An instant of the window: an ISO 8601 date (midnight UTC) or a date and time with a zone. */
31
+ export declare function parseInstant(option: string, value: string): string;
32
+ /** The Langfuse host as a base URL without a trailing slash, or why it is refused. */
33
+ export declare function parseLangfuseHost(value: string): string;
34
+ /** Runs `hue import langfuse`; resolves to the exit code. */
35
+ export declare function runImportCommand(argv: string[], io?: ImportCommandIo): Promise<number>;