@prometheus-ai/utils 0.5.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 (61) hide show
  1. package/dist/types/abortable.d.ts +27 -0
  2. package/dist/types/async.d.ts +6 -0
  3. package/dist/types/cli.d.ts +117 -0
  4. package/dist/types/color.d.ts +102 -0
  5. package/dist/types/dirs.d.ts +171 -0
  6. package/dist/types/env.d.ts +55 -0
  7. package/dist/types/fetch-retry.d.ts +80 -0
  8. package/dist/types/format.d.ts +37 -0
  9. package/dist/types/frontmatter.d.ts +25 -0
  10. package/dist/types/fs-error.d.ts +31 -0
  11. package/dist/types/glob.d.ts +28 -0
  12. package/dist/types/hook-fetch.d.ts +16 -0
  13. package/dist/types/index.d.ts +29 -0
  14. package/dist/types/json.d.ts +4 -0
  15. package/dist/types/logger.d.ts +66 -0
  16. package/dist/types/mermaid-ascii.d.ts +11 -0
  17. package/dist/types/mime.d.ts +29 -0
  18. package/dist/types/peek-file.d.ts +29 -0
  19. package/dist/types/postmortem.d.ts +29 -0
  20. package/dist/types/procmgr.d.ts +25 -0
  21. package/dist/types/prompt.d.ts +18 -0
  22. package/dist/types/ptree.d.ts +108 -0
  23. package/dist/types/ring.d.ts +93 -0
  24. package/dist/types/sanitize-text.d.ts +14 -0
  25. package/dist/types/snowflake.d.ts +25 -0
  26. package/dist/types/stream.d.ts +68 -0
  27. package/dist/types/tab-spacing.d.ts +9 -0
  28. package/dist/types/temp.d.ts +14 -0
  29. package/dist/types/type-guards.d.ts +3 -0
  30. package/dist/types/which.d.ts +37 -0
  31. package/package.json +61 -0
  32. package/src/abortable.ts +73 -0
  33. package/src/async.ts +50 -0
  34. package/src/cli.ts +432 -0
  35. package/src/color.ts +302 -0
  36. package/src/dirs.ts +584 -0
  37. package/src/env.ts +172 -0
  38. package/src/fetch-retry.ts +325 -0
  39. package/src/format.ts +113 -0
  40. package/src/frontmatter.ts +128 -0
  41. package/src/fs-error.ts +56 -0
  42. package/src/glob.ts +189 -0
  43. package/src/hook-fetch.ts +30 -0
  44. package/src/index.ts +49 -0
  45. package/src/json.ts +10 -0
  46. package/src/logger.ts +417 -0
  47. package/src/mermaid-ascii.ts +31 -0
  48. package/src/mime.ts +159 -0
  49. package/src/peek-file.ts +188 -0
  50. package/src/postmortem.ts +196 -0
  51. package/src/procmgr.ts +195 -0
  52. package/src/prompt.ts +471 -0
  53. package/src/ptree.ts +390 -0
  54. package/src/ring.ts +169 -0
  55. package/src/sanitize-text.ts +38 -0
  56. package/src/snowflake.ts +136 -0
  57. package/src/stream.ts +403 -0
  58. package/src/tab-spacing.ts +342 -0
  59. package/src/temp.ts +77 -0
  60. package/src/type-guards.ts +11 -0
  61. package/src/which.ts +232 -0
@@ -0,0 +1,136 @@
1
+ // 16-bit hex lookup table (65536 entries) for fast conversion
2
+ const HEX4 = Array.from({ length: 65536 }, (_, i) => i.toString(16).padStart(4, "0"));
3
+
4
+ function randu32() {
5
+ return crypto.getRandomValues(new Uint32Array(1))[0];
6
+ }
7
+
8
+ const EPOCH = 1420070400000;
9
+ const MAX_SEQ = 0x3fffff;
10
+
11
+ // Snowflake as a hex string (16 chars, zero-padded).
12
+ //
13
+ // Since this is not distributed (no machine ID needed), we use an extended
14
+ // 22-bit sequence instead of the standard 10-bit machine ID + 12-bit sequence.
15
+ //
16
+ type Snowflake = string & { readonly __brand: unique symbol };
17
+
18
+ namespace Snowflake {
19
+ // Hex string validation pattern (16 lowercase hex chars).
20
+ //
21
+ export const PATTERN = /^[0-9a-f]{16}$/;
22
+
23
+ // Epoch timestamp.
24
+ //
25
+ export const EPOCH_TIMESTAMP = EPOCH;
26
+
27
+ // Maximum sequence number.
28
+ //
29
+ export const MAX_SEQUENCE = MAX_SEQ;
30
+
31
+ // Parses a hex string or bigint to bigint.
32
+ //
33
+ function toBigInt(value: Snowflake): bigint {
34
+ const hi = Number.parseInt(value.substring(0, 8), 16);
35
+ const lo = Number.parseInt(value.substring(8, 16), 16);
36
+ return (BigInt(hi) << 32n) | BigInt(lo);
37
+ }
38
+
39
+ // Formats a sequence and timestamp into a snowflake hex string.
40
+ //
41
+ export function formatParts(dt: number, seq: number): Snowflake {
42
+ // Split dt into hi/lo to avoid exceeding Number.MAX_SAFE_INTEGER.
43
+ // dt is ~39 bits; dt<<22 would be ~61 bits, so we split at bit 10:
44
+ // lo32 = (dtLo << 22) | seq (10+22 = 32 bits, no overlap)
45
+ // hi32 = dtHi (~29 bits)
46
+ const dtLo = dt % 1024;
47
+ const hi = (dt - dtLo) / 1024; // dt >>> 10
48
+ const lo = ((dtLo << 22) | seq) >>> 0;
49
+ const hi1 = (hi >>> 16) & 0xffff;
50
+ const hi2 = hi & 0xffff;
51
+ const lo1 = (lo >>> 16) & 0xffff;
52
+ const lo2 = lo & 0xffff;
53
+ return `${HEX4[hi1]}${HEX4[hi2]}${HEX4[lo1]}${HEX4[lo2]}` as Snowflake;
54
+ }
55
+
56
+ // Snowflake generator type.
57
+ //
58
+ export class Source {
59
+ #seq = 0;
60
+ constructor(sequence: number = randu32() & MAX_SEQ) {
61
+ this.#seq = sequence & MAX_SEQ;
62
+ }
63
+
64
+ // Sequence number.
65
+ //
66
+ get sequence() {
67
+ return this.#seq & MAX_SEQ;
68
+ }
69
+ set sequence(v: number) {
70
+ this.#seq = v & MAX_SEQ;
71
+ }
72
+ reset() {
73
+ this.#seq = 0;
74
+ }
75
+
76
+ // Generates the next value as a hex string.
77
+ //
78
+ generate(timestamp: number): Snowflake {
79
+ const seq = (this.#seq + 1) & MAX_SEQ;
80
+ const dt = timestamp - EPOCH;
81
+ this.#seq = seq;
82
+ return formatParts(dt, seq);
83
+ }
84
+ }
85
+
86
+ // Gets the next snowflake given the timestamp.
87
+ //
88
+ const defaultSource = new Source();
89
+ export function next(timestamp = Date.now()): Snowflake {
90
+ return defaultSource.generate(timestamp);
91
+ }
92
+
93
+ // Validates a snowflake hex string.
94
+ //
95
+ export function valid(value: string): value is Snowflake {
96
+ return value.length === 16 && PATTERN.test(value);
97
+ }
98
+
99
+ // Returns the upper/lower boundaries for the given timestamp.
100
+ //
101
+ export function lowerbound(timelike: Date | number | Snowflake): Snowflake {
102
+ switch (typeof timelike) {
103
+ case "object": // Date
104
+ return formatParts(timelike.getTime() - EPOCH, 0);
105
+ case "number":
106
+ return formatParts(timelike - EPOCH, 0);
107
+ case "string": // Snowflake hex string
108
+ return timelike;
109
+ }
110
+ }
111
+ export function upperbound(timelike: Date | number | Snowflake): Snowflake {
112
+ switch (typeof timelike) {
113
+ case "object": // Date
114
+ return formatParts(timelike.getTime() - EPOCH, MAX_SEQ);
115
+ case "number":
116
+ return formatParts(timelike - EPOCH, MAX_SEQ);
117
+ case "string": // Snowflake hex string
118
+ return timelike;
119
+ }
120
+ }
121
+
122
+ // Returns the individual bits given the snowflake.
123
+ //
124
+ export function getSequence(value: Snowflake) {
125
+ return Number.parseInt(value.substring(8, 16), 16) & MAX_SEQ;
126
+ }
127
+ export function getTimestamp(value: Snowflake) {
128
+ const n = toBigInt(value) >> 22n;
129
+ return Number(n + BigInt(EPOCH));
130
+ }
131
+ export function getDate(value: Snowflake) {
132
+ return new Date(getTimestamp(value));
133
+ }
134
+ }
135
+
136
+ export { Snowflake };
package/src/stream.ts ADDED
@@ -0,0 +1,403 @@
1
+ import { createAbortableStream } from "./abortable";
2
+
3
+ const LF = 0x0a;
4
+ type JsonlChunkResult = {
5
+ values: unknown[];
6
+ error: unknown;
7
+ read: number;
8
+ done: boolean;
9
+ };
10
+
11
+ function parseJsonlChunkCompat(input: Uint8Array, beg?: number, end?: number): JsonlChunkResult;
12
+ function parseJsonlChunkCompat(input: string): JsonlChunkResult;
13
+ function parseJsonlChunkCompat(input: Uint8Array | string, beg?: number, end?: number): JsonlChunkResult {
14
+ if (typeof input === "string") {
15
+ const { values, error, read, done } = Bun.JSONL.parseChunk(input);
16
+ return { values, error, read, done };
17
+ }
18
+ const start = beg ?? 0;
19
+ const stop = end ?? input.length;
20
+ const { values, error, read, done } = Bun.JSONL.parseChunk(input, start, stop);
21
+ return { values, error, read, done };
22
+ }
23
+
24
+ export async function* readLines(stream: ReadableStream<Uint8Array>, signal?: AbortSignal): AsyncGenerator<Uint8Array> {
25
+ const buffer = new ConcatSink();
26
+ const source = createAbortableStream(stream, signal);
27
+ try {
28
+ for await (const chunk of source) {
29
+ for (const line of buffer.appendAndFlushLines(chunk)) {
30
+ yield line;
31
+ }
32
+ }
33
+ if (!buffer.isEmpty) {
34
+ const tail = buffer.flush();
35
+ if (tail) {
36
+ buffer.clear();
37
+ yield tail;
38
+ }
39
+ }
40
+ } catch (err) {
41
+ // Abort errors are expected — just stop the generator.
42
+ if (signal?.aborted) return;
43
+ throw err;
44
+ }
45
+ }
46
+
47
+ export async function* readJsonl<T>(stream: ReadableStream<Uint8Array>, signal?: AbortSignal): AsyncGenerator<T> {
48
+ const buffer = new ConcatSink();
49
+ const source = createAbortableStream(stream, signal);
50
+ try {
51
+ for await (const chunk of source) {
52
+ yield* buffer.pullJSONL<T>(chunk, 0, chunk.length);
53
+ }
54
+ if (!buffer.isEmpty) {
55
+ const tail = buffer.flush();
56
+ if (tail) {
57
+ buffer.clear();
58
+ const { values, error, done } = parseJsonlChunkCompat(tail, 0, tail.length);
59
+ if (values.length > 0) {
60
+ yield* values as T[];
61
+ }
62
+ if (error) throw error;
63
+ if (!done) {
64
+ throw new Error("JSONL stream ended unexpectedly");
65
+ }
66
+ }
67
+ }
68
+ } catch (err) {
69
+ // Abort errors are expected — just stop the generator.
70
+ if (signal?.aborted) return;
71
+ throw err;
72
+ }
73
+ }
74
+
75
+ // =============================================================================
76
+ // SSE (Server-Sent Events)
77
+ // =============================================================================
78
+
79
+ class ConcatSink {
80
+ #space?: Buffer;
81
+ #length = 0;
82
+
83
+ #ensureCapacity(size: number): Buffer {
84
+ const space = this.#space;
85
+ if (space && space.length >= size) return space;
86
+ const nextSize = space ? Math.max(size, space.length * 2) : size;
87
+ const next = Buffer.allocUnsafe(nextSize);
88
+ if (space && this.#length > 0) {
89
+ space.copy(next, 0, 0, this.#length);
90
+ }
91
+ this.#space = next;
92
+ return next;
93
+ }
94
+
95
+ append(chunk: Uint8Array) {
96
+ const n = chunk.length;
97
+ if (!n) return;
98
+ const offset = this.#length;
99
+ const space = this.#ensureCapacity(offset + n);
100
+ space.set(chunk, offset);
101
+ this.#length += n;
102
+ }
103
+
104
+ reset(chunk: Uint8Array) {
105
+ const n = chunk.length;
106
+ if (!n) {
107
+ this.#length = 0;
108
+ return;
109
+ }
110
+ const space = this.#ensureCapacity(n);
111
+ space.set(chunk, 0);
112
+ this.#length = n;
113
+ }
114
+
115
+ get isEmpty(): boolean {
116
+ return this.#length === 0;
117
+ }
118
+
119
+ flush(): Uint8Array | undefined {
120
+ if (!this.#length) return undefined;
121
+ return this.#space!.subarray(0, this.#length);
122
+ }
123
+
124
+ clear() {
125
+ this.#length = 0;
126
+ }
127
+
128
+ *appendAndFlushLines(chunk: Uint8Array) {
129
+ let pos = 0;
130
+ while (pos < chunk.length) {
131
+ const nl = chunk.indexOf(LF, pos);
132
+ if (nl === -1) {
133
+ this.append(chunk.subarray(pos));
134
+ return;
135
+ }
136
+ const suffix = chunk.subarray(pos, nl);
137
+ pos = nl + 1;
138
+ if (this.isEmpty) {
139
+ yield suffix;
140
+ } else {
141
+ this.append(suffix);
142
+ const payload = this.flush();
143
+ if (payload) {
144
+ yield payload;
145
+ this.clear();
146
+ }
147
+ }
148
+ }
149
+ }
150
+ *pullJSONL<T>(chunk: Uint8Array, beg: number, end: number) {
151
+ if (this.isEmpty) {
152
+ const { values, error, read, done } = parseJsonlChunkCompat(chunk, beg, end);
153
+ if (values.length > 0) {
154
+ yield* values as T[];
155
+ }
156
+ if (error) throw error;
157
+ if (done) return;
158
+ this.reset(chunk.subarray(read, end));
159
+ return;
160
+ }
161
+
162
+ const offset = this.#length;
163
+ const n = end - beg;
164
+ const total = offset + n;
165
+ const space = this.#ensureCapacity(total);
166
+ space.set(chunk.subarray(beg, end), offset);
167
+ this.#length = total;
168
+
169
+ const { values, error, read, done } = parseJsonlChunkCompat(space.subarray(0, total), 0, total);
170
+ if (values.length > 0) {
171
+ yield* values as T[];
172
+ }
173
+ if (error) throw error;
174
+ if (done) {
175
+ this.#length = 0;
176
+ return;
177
+ }
178
+ const rem = total - read;
179
+ if (rem < total) {
180
+ space.copyWithin(0, read, total);
181
+ }
182
+ this.#length = rem;
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Stream parsed JSON objects from SSE `data:` lines.
188
+ *
189
+ * Thin wrapper over {@link readSseEvents}: yields one parsed JSON value per
190
+ * dispatched SSE event, skipping events with empty `data` and stopping at the
191
+ * OpenAI-style `[DONE]` sentinel. If your consumer doesn't care about `event:`
192
+ * names or doesn't need a custom parse step, use this; otherwise call
193
+ * `readSseEvents` directly.
194
+ *
195
+ * @example
196
+ * ```ts
197
+ * for await (const obj of readSseJson(response.body!)) {
198
+ * console.log(obj);
199
+ * }
200
+ * ```
201
+ */
202
+ export type SseEventObserver = (event: ServerSentEvent) => void;
203
+
204
+ function notifySseEventObserver(observer: SseEventObserver | undefined, event: ServerSentEvent): void {
205
+ if (!observer) return;
206
+ try {
207
+ observer(event);
208
+ } catch {
209
+ // Diagnostic observers must never perturb provider stream consumption.
210
+ }
211
+ }
212
+
213
+ export async function* readSseJson<T>(
214
+ stream: ReadableStream<Uint8Array>,
215
+ signal?: AbortSignal,
216
+ onEvent?: SseEventObserver,
217
+ ): AsyncGenerator<T> {
218
+ for await (const sse of readSseEvents(stream, signal)) {
219
+ notifySseEventObserver(onEvent, sse);
220
+ const data = sse.data;
221
+ if (data === "" || data === "[DONE]") {
222
+ if (data === "[DONE]") return;
223
+ continue;
224
+ }
225
+ yield JSON.parse(data) as T;
226
+ }
227
+ }
228
+
229
+ /**
230
+ * A single Server-Sent Event dispatched on a blank-line boundary.
231
+ *
232
+ * - `event` is the value of the most recent `event:` field, or `null` if none.
233
+ * - `data` is the concatenation (joined by `\n`) of every `data:` field in the
234
+ * event, exactly as required by the SSE spec.
235
+ * - `raw` is the list of decoded non-empty lines that made up the event,
236
+ * preserved for diagnostic context (error reporting, debugging). The
237
+ * dispatching blank line is not included.
238
+ */
239
+ export interface ServerSentEvent {
240
+ event: string | null;
241
+ data: string;
242
+ raw: string[];
243
+ }
244
+
245
+ interface SseEventState {
246
+ event: string | null;
247
+ // `data` accumulates across multiple `data:` lines per the SSE spec, joined
248
+ // by `\n`. We keep the running string here and append as lines arrive instead
249
+ // of buffering an array and joining at flush. `null` means "no data: field
250
+ // seen yet" (distinct from a `data:` field with an empty value).
251
+ data: string | null;
252
+ raw: string[];
253
+ }
254
+
255
+ // Single decoder reused for all line decodes. Safe because lines are split on
256
+ // LF (0x0a) which is always a single-byte ASCII char in UTF-8 and never appears
257
+ // inside a multi-byte sequence — so each line is itself a complete UTF-8 run.
258
+ const SSE_LINE_DECODER = new TextDecoder("utf-8");
259
+
260
+ function decodeSseLineBytes(line: Uint8Array, end: number): string {
261
+ return end === line.length ? SSE_LINE_DECODER.decode(line) : SSE_LINE_DECODER.decode(line.subarray(0, end));
262
+ }
263
+
264
+ function flushSseEvent(state: SseEventState): ServerSentEvent | null {
265
+ if (state.event === null && state.data === null) {
266
+ state.raw = [];
267
+ return null;
268
+ }
269
+ const event: ServerSentEvent = {
270
+ event: state.event,
271
+ data: state.data ?? "",
272
+ raw: state.raw,
273
+ };
274
+ state.event = null;
275
+ state.data = null;
276
+ state.raw = [];
277
+ return event;
278
+ }
279
+
280
+ function pushSseLine(line: Uint8Array, state: SseEventState): ServerSentEvent | null {
281
+ // `appendAndFlushLines` splits on LF only; strip a trailing CR so CRLF sources
282
+ // don't leak `\r` into field values.
283
+ let end = line.length;
284
+ if (end > 0 && line[end - 1] === 0x0d /* '\r' */) end--;
285
+ if (end === 0) return flushSseEvent(state);
286
+
287
+ // Comment line: keep in `raw` for diagnostic context, skip parsing.
288
+ if (line[0] === 0x3a /* ':' */) {
289
+ state.raw.push(decodeSseLineBytes(line, end));
290
+ return null;
291
+ }
292
+
293
+ const text = decodeSseLineBytes(line, end);
294
+ state.raw.push(text);
295
+
296
+ const colon = text.indexOf(":");
297
+ const fieldName = colon === -1 ? text : text.slice(0, colon);
298
+ let value = colon === -1 ? "" : text.slice(colon + 1);
299
+ if (value.charCodeAt(0) === 0x20 /* ' ' */) value = value.slice(1);
300
+
301
+ if (fieldName === "event") {
302
+ state.event = value;
303
+ } else if (fieldName === "data") {
304
+ if (state.data === null) {
305
+ state.data = value;
306
+ } else {
307
+ state.data += "\n";
308
+ state.data += value;
309
+ }
310
+ }
311
+ // `id` and `retry` are intentionally ignored — the providers we consume
312
+ // don't use them, and the underlying transport handles reconnects itself.
313
+ return null;
314
+ }
315
+
316
+ /**
317
+ * Stream raw Server-Sent Events from an HTTP response body.
318
+ *
319
+ * Yields one `ServerSentEvent` per blank-line dispatch. The consumer is
320
+ * responsible for parsing `data` (e.g. JSON, plain text, error envelope).
321
+ * Use `readSseJson` instead when every event is a single `data:` JSON object
322
+ * and you don't need access to the `event:` field.
323
+ *
324
+ * Internally backed by a Buffer-based line reader (`ConcatSink`) so chunk
325
+ * concatenation is O(n) and never triggers per-line string slicing of the
326
+ * accumulated buffer.
327
+ *
328
+ * @example
329
+ * ```ts
330
+ * for await (const sse of readSseEvents(response.body!)) {
331
+ * if (sse.event === "ping") continue;
332
+ * const obj = JSON.parse(sse.data);
333
+ * }
334
+ * ```
335
+ */
336
+ export async function* readSseEvents(
337
+ stream: ReadableStream<Uint8Array>,
338
+ signal?: AbortSignal,
339
+ ): AsyncGenerator<ServerSentEvent> {
340
+ const lineBuffer = new ConcatSink();
341
+ const state: SseEventState = { event: null, data: null, raw: [] };
342
+ const source = createAbortableStream(stream, signal);
343
+ try {
344
+ for await (const chunk of source) {
345
+ for (const line of lineBuffer.appendAndFlushLines(chunk)) {
346
+ const event = pushSseLine(line, state);
347
+ if (event) yield event;
348
+ }
349
+ }
350
+ // Treat any trailing partial line (no terminating LF) as a complete line.
351
+ if (!lineBuffer.isEmpty) {
352
+ const tail = lineBuffer.flush();
353
+ if (tail) {
354
+ lineBuffer.clear();
355
+ const event = pushSseLine(tail, state);
356
+ if (event) yield event;
357
+ }
358
+ }
359
+ // Real services don't always close on a blank line — flush any pending event.
360
+ const trailing = flushSseEvent(state);
361
+ if (trailing) yield trailing;
362
+ } catch (err) {
363
+ if (signal?.aborted) return;
364
+ throw err;
365
+ }
366
+ }
367
+
368
+ /**
369
+ * Parse a complete JSONL string, skipping malformed lines instead of throwing.
370
+ *
371
+ * Uses `Bun.JSONL.parseChunk` internally. On parse errors, the malformed
372
+ * region is skipped up to the next newline and parsing continues.
373
+ *
374
+ * @example
375
+ * ```ts
376
+ * const entries = parseJsonlLenient<MyType>(fileContents);
377
+ * ```
378
+ */
379
+ export function parseJsonlLenient<T>(buffer: string): T[] {
380
+ let entries: T[] | undefined;
381
+
382
+ while (buffer.length > 0) {
383
+ const { values, error, read, done } = parseJsonlChunkCompat(buffer);
384
+ if (values.length > 0) {
385
+ const ext = values as T[];
386
+ if (!entries) {
387
+ entries = ext;
388
+ } else {
389
+ entries.push(...ext);
390
+ }
391
+ }
392
+ if (error) {
393
+ const nextNewline = buffer.indexOf("\n", read);
394
+ if (nextNewline === -1) break;
395
+ buffer = buffer.substring(nextNewline + 1);
396
+ continue;
397
+ }
398
+ if (read === 0) break;
399
+ buffer = buffer.substring(read);
400
+ if (done) break;
401
+ }
402
+ return entries ?? [];
403
+ }