@lovelaces-io/storyteller 0.2.0 → 0.3.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.
package/dist/index.js CHANGED
@@ -1,23 +1,48 @@
1
- // src/audiences/consoleAudience.ts
2
- function consoleAudience() {
3
- return {
4
- name: "console",
5
- hear: (event) => {
6
- const prefix = "Storyteller";
7
- const style = event.level === "Information" ? "color:#16a34a;font-weight:600" : event.level === "Warning" ? "color:#f59e0b;font-weight:600" : "color:#dc2626;font-weight:600";
8
- const header = `${prefix}: ${event.title}`;
9
- console.groupCollapsed(`%c${header}`, style);
10
- const payload = JSON.stringify(event, null, 2);
11
- if (event.level === "Information") {
12
- console.log(header, payload);
13
- } else if (event.level === "Warning") {
14
- console.warn(header, payload);
15
- } else {
16
- console.error(header, payload);
17
- }
18
- console.groupEnd();
19
- }
20
- };
1
+ // src/environment.ts
2
+ var LEVEL_RANK = {
3
+ Information: 0,
4
+ Warning: 1,
5
+ Error: 2
6
+ };
7
+ var LEVEL_ALIASES = {
8
+ info: "Information",
9
+ information: "Information",
10
+ tell: "Information",
11
+ warn: "Warning",
12
+ warning: "Warning",
13
+ oops: "Error",
14
+ error: "Error"
15
+ };
16
+ function toStoryLevel(input) {
17
+ if (!input) return "Information";
18
+ return LEVEL_ALIASES[String(input).toLowerCase()] ?? "Information";
19
+ }
20
+ function readEnvironmentValue(name) {
21
+ try {
22
+ const runtime = globalThis;
23
+ const value = runtime.process?.env?.[name];
24
+ return typeof value === "string" && value.length ? value.trim() : void 0;
25
+ } catch {
26
+ return void 0;
27
+ }
28
+ }
29
+ function resolveMinimumLevel(requested) {
30
+ const value = requested ?? readEnvironmentValue("STORYTELLER_LEVEL");
31
+ if (!value) return "Information";
32
+ return LEVEL_ALIASES[String(value).toLowerCase()] ?? "Information";
33
+ }
34
+ function meetsLevel(level, minimum) {
35
+ return LEVEL_RANK[level] >= LEVEL_RANK[minimum];
36
+ }
37
+ function resolveOutputFormat(requested) {
38
+ const value = requested ?? readEnvironmentValue("STORYTELLER_FORMAT");
39
+ return value === "ndjson" ? "ndjson" : "text";
40
+ }
41
+ function resolveColors(requested) {
42
+ if (requested !== void 0) return requested;
43
+ const value = readEnvironmentValue("STORYTELLER_COLOR");
44
+ if (value === void 0) return true;
45
+ return !(value === "0" || value.toLowerCase() === "false");
21
46
  }
22
47
 
23
48
  // src/utils.ts
@@ -37,6 +62,9 @@ function getLevelColor(level) {
37
62
  function formatOrigin(origin) {
38
63
  if (!origin?.where) return;
39
64
  if (typeof origin.where === "string") return origin.where;
65
+ if (typeof origin.where !== "object" || Array.isArray(origin.where)) {
66
+ return String(origin.where);
67
+ }
40
68
  const whereRecord = origin.where;
41
69
  const priorityKeys = ["app", "service", "page", "component"];
42
70
  const priorityParts = priorityKeys.filter((key) => whereRecord[key] != null).map((key) => String(whereRecord[key]));
@@ -68,6 +96,472 @@ function countBrackets(line) {
68
96
  const closeCount = (line.match(/\]/g) || []).length;
69
97
  return openCount - closeCount;
70
98
  }
99
+ var CONTEXT_LINE_LIMIT = 120;
100
+ function summarizeContext(note) {
101
+ const parts = [];
102
+ appendContextParts(parts, note.what);
103
+ appendContextParts(parts, note.where);
104
+ if (note.error) {
105
+ const errorLine = [note.error.name, note.error.message].filter(Boolean).join(": ");
106
+ if (errorLine && errorLine !== note.note) parts.push(errorLine);
107
+ }
108
+ if (!parts.length) return void 0;
109
+ const joined = parts.join(" ");
110
+ const text = joined.length > CONTEXT_LINE_LIMIT ? `${joined.slice(0, CONTEXT_LINE_LIMIT)}\u2026` : joined;
111
+ return `{${text}}`;
112
+ }
113
+ function appendContextParts(parts, value) {
114
+ if (value == null) return;
115
+ if (typeof value !== "object") {
116
+ parts.push(String(value));
117
+ return;
118
+ }
119
+ if (Array.isArray(value)) {
120
+ parts.push(`[${value.length}]`);
121
+ return;
122
+ }
123
+ for (const [key, entry] of Object.entries(value)) {
124
+ if (entry == null) continue;
125
+ if (key.startsWith("@")) continue;
126
+ parts.push(`${key}=${typeof entry === "object" ? summarizeNested(entry) : String(entry)}`);
127
+ }
128
+ }
129
+ function summarizeNested(value) {
130
+ if (Array.isArray(value)) return `[${value.length}]`;
131
+ if (value && typeof value === "object") return `{${Object.keys(value).length}}`;
132
+ return String(value);
133
+ }
134
+
135
+ // src/audiences/consoleAudience.ts
136
+ var LEVEL_LABELS = {
137
+ Information: "info",
138
+ Warning: "warn",
139
+ Error: "oops"
140
+ };
141
+ var LEVEL_STYLES = {
142
+ Information: "color:#16a34a;font-weight:600",
143
+ Warning: "color:#f59e0b;font-weight:600",
144
+ Error: "color:#dc2626;font-weight:600"
145
+ };
146
+ function consoleAudience(options = {}) {
147
+ const colors = resolveColors(options.colors);
148
+ return {
149
+ name: "console",
150
+ hears: ["note", "story"],
151
+ hear: (emission) => {
152
+ if (emission.kind === "note") {
153
+ printNote(emission, colors);
154
+ return;
155
+ }
156
+ printStory(emission);
157
+ }
158
+ };
159
+ }
160
+ function printNote(note, colors) {
161
+ const time = readClockTime(note.timestamp);
162
+ const label = LEVEL_LABELS[note.level];
163
+ const origin = formatOrigin(note.origin);
164
+ const context = summarizeContext(note);
165
+ const head = colors ? `${getLevelColor(note.level)}${label}${ANSI.reset}` : label;
166
+ const line = [
167
+ colors ? `${ANSI.grayDark}${time}${ANSI.reset}` : time,
168
+ head,
169
+ origin ? colors ? `${ANSI.grayDark}${origin}${ANSI.reset}` : origin : void 0,
170
+ note.note,
171
+ context ? colors ? `${ANSI.grayDark}${context}${ANSI.reset}` : context : void 0
172
+ ].filter(Boolean).join(" ");
173
+ if (note.level === "Information") {
174
+ console.log(line);
175
+ } else if (note.level === "Warning") {
176
+ console.warn(line);
177
+ } else {
178
+ console.error(line);
179
+ }
180
+ }
181
+ function printStory(event) {
182
+ const prefix = "Storyteller";
183
+ const header = `${prefix}: ${event.title}`;
184
+ console.groupCollapsed(`%c${header}`, LEVEL_STYLES[event.level]);
185
+ const payload = JSON.stringify(event, null, 2);
186
+ if (event.level === "Information") {
187
+ console.log(header, payload);
188
+ } else if (event.level === "Warning") {
189
+ console.warn(header, payload);
190
+ } else {
191
+ console.error(header, payload);
192
+ }
193
+ console.groupEnd();
194
+ }
195
+ function readClockTime(timestamp) {
196
+ const timePart = timestamp.slice(11, 19);
197
+ return timePart.length === 8 ? timePart : timestamp;
198
+ }
199
+
200
+ // src/normalize.ts
201
+ var REDACTED = "[redacted]";
202
+ var DEFAULT_REDACT_KEYS = [
203
+ "password",
204
+ "passphrase",
205
+ "token",
206
+ "secret",
207
+ "apiKey",
208
+ "accessKey",
209
+ "authorization",
210
+ "auth",
211
+ "cookie",
212
+ "sessionId",
213
+ "privateKey",
214
+ "clientSecret",
215
+ "refreshToken"
216
+ ];
217
+ var DEFAULT_MAX_DEPTH = 6;
218
+ var DEFAULT_MAX_ARRAY_LENGTH = 100;
219
+ var DEFAULT_MAX_PROPERTIES = 100;
220
+ var DEFAULT_MAX_STRING_LENGTH = 8e3;
221
+ var MAX_CAUSE_DEPTH = 5;
222
+ var BINARY_PREVIEW_BYTES = 16;
223
+ function normalizeValue(input, options = {}) {
224
+ const resolved = {
225
+ maxDepth: options.maxDepth ?? DEFAULT_MAX_DEPTH,
226
+ maxArrayLength: options.maxArrayLength ?? DEFAULT_MAX_ARRAY_LENGTH,
227
+ maxProperties: options.maxProperties ?? DEFAULT_MAX_PROPERTIES,
228
+ maxStringLength: options.maxStringLength ?? DEFAULT_MAX_STRING_LENGTH,
229
+ redactKeys: new Set(
230
+ (options.redactKeys ?? DEFAULT_REDACT_KEYS).map(normalizeKeyForMatching)
231
+ ),
232
+ redact: options.redact ?? true
233
+ };
234
+ try {
235
+ return normalizeUnknown(input, resolved, 0, "$", /* @__PURE__ */ new Map());
236
+ } catch (failure) {
237
+ return `[Unreadable: ${describeFailure(failure)}]`;
238
+ }
239
+ }
240
+ function normalizeError(rawError, options = {}) {
241
+ const resolved = {
242
+ maxDepth: options.maxDepth ?? DEFAULT_MAX_DEPTH,
243
+ maxArrayLength: options.maxArrayLength ?? DEFAULT_MAX_ARRAY_LENGTH,
244
+ maxProperties: options.maxProperties ?? DEFAULT_MAX_PROPERTIES,
245
+ maxStringLength: options.maxStringLength ?? DEFAULT_MAX_STRING_LENGTH,
246
+ redactKeys: new Set(
247
+ (options.redactKeys ?? DEFAULT_REDACT_KEYS).map(normalizeKeyForMatching)
248
+ ),
249
+ redact: options.redact ?? true
250
+ };
251
+ return normalizeErrorInternal(rawError, resolved, 0);
252
+ }
253
+ function normalizeErrorInternal(rawError, options, causeDepth) {
254
+ if (!(rawError instanceof Error)) {
255
+ if (isPlainRecord(rawError)) {
256
+ const record = rawError;
257
+ const message = typeof record["message"] === "string" ? record["message"] : void 0;
258
+ const name = typeof record["name"] === "string" ? record["name"] : void 0;
259
+ if (message !== void 0 || name !== void 0) {
260
+ return {
261
+ ...name !== void 0 ? { name } : {},
262
+ ...message !== void 0 ? { message } : {}
263
+ };
264
+ }
265
+ }
266
+ return { message: safeStringify(rawError, options.maxStringLength) };
267
+ }
268
+ const normalized = {
269
+ name: rawError.name,
270
+ message: rawError.message
271
+ };
272
+ if (rawError.stack !== void 0) {
273
+ normalized.stack = truncateString(rawError.stack, options.maxStringLength);
274
+ }
275
+ const cause = rawError.cause;
276
+ if (cause !== void 0) {
277
+ if (causeDepth >= MAX_CAUSE_DEPTH) {
278
+ normalized.cause = { "@truncated": { kind: "causeChain" } };
279
+ } else if (cause instanceof Error) {
280
+ normalized.cause = normalizeErrorInternal(cause, options, causeDepth + 1);
281
+ } else {
282
+ normalized.cause = normalizeUnknown(cause, options, 0, "$.cause", /* @__PURE__ */ new Map());
283
+ }
284
+ }
285
+ const aggregated = rawError.errors;
286
+ if (Array.isArray(aggregated)) {
287
+ normalized.errors = aggregated.slice(0, options.maxArrayLength).map((member) => normalizeErrorInternal(member, options, causeDepth + 1));
288
+ }
289
+ return normalized;
290
+ }
291
+ function normalizeUnknown(value, options, depth, path, ancestors) {
292
+ if (value === null) return null;
293
+ const valueType = typeof value;
294
+ if (valueType === "string") {
295
+ return truncateString(value, options.maxStringLength);
296
+ }
297
+ if (valueType === "number") {
298
+ return Number.isFinite(value) ? value : String(value);
299
+ }
300
+ if (valueType === "boolean") return value;
301
+ if (valueType === "undefined") return null;
302
+ if (valueType === "bigint") return `${String(value)}n`;
303
+ if (valueType === "symbol") return String(value);
304
+ if (valueType === "function") {
305
+ const name = value.name;
306
+ return `[Function: ${name ? name : "anonymous"}]`;
307
+ }
308
+ const objectValue = value;
309
+ const existingPath = ancestors.get(objectValue);
310
+ if (existingPath !== void 0) {
311
+ return `[Circular \u2192 ${existingPath}]`;
312
+ }
313
+ if (depth > options.maxDepth) {
314
+ return { "@truncated": { kind: "depth", depth: options.maxDepth } };
315
+ }
316
+ const wellKnown = normalizeWellKnown(objectValue, options, depth, path, ancestors);
317
+ if (wellKnown !== void 0) return wellKnown;
318
+ ancestors.set(objectValue, path);
319
+ try {
320
+ if (Array.isArray(objectValue)) {
321
+ return normalizeArray(objectValue, options, depth, path, ancestors);
322
+ }
323
+ return normalizeObject(objectValue, options, depth, path, ancestors);
324
+ } finally {
325
+ ancestors.delete(objectValue);
326
+ }
327
+ }
328
+ function normalizeWellKnown(value, options, depth, path, ancestors) {
329
+ if (value instanceof Error) {
330
+ return normalizeErrorInternal(value, options, 0);
331
+ }
332
+ if (value instanceof Date) {
333
+ const time = value.getTime();
334
+ return Number.isNaN(time) ? "[Invalid Date]" : value.toISOString();
335
+ }
336
+ if (value instanceof RegExp) return String(value);
337
+ if (value instanceof URL) return value.href;
338
+ if (value instanceof Map) {
339
+ const entries = {};
340
+ let index = 0;
341
+ let omitted = 0;
342
+ for (const [entryKey, entryValue] of value) {
343
+ if (index >= options.maxProperties) {
344
+ omitted += 1;
345
+ continue;
346
+ }
347
+ const keyLabel = safeStringify(entryKey, options.maxStringLength);
348
+ entries[keyLabel] = redactOrNormalize(
349
+ keyLabel,
350
+ entryValue,
351
+ options,
352
+ depth + 1,
353
+ `${path}.${keyLabel}`,
354
+ ancestors
355
+ );
356
+ index += 1;
357
+ }
358
+ return {
359
+ "@type": "Map",
360
+ entries,
361
+ ...omitted ? { "@truncated": { kind: "mapEntries", omitted } } : {}
362
+ };
363
+ }
364
+ if (value instanceof Set) {
365
+ const values = [];
366
+ let omitted = 0;
367
+ for (const member of value) {
368
+ if (values.length >= options.maxArrayLength) {
369
+ omitted += 1;
370
+ continue;
371
+ }
372
+ values.push(
373
+ normalizeUnknown(member, options, depth + 1, `${path}[${values.length}]`, ancestors)
374
+ );
375
+ }
376
+ return {
377
+ "@type": "Set",
378
+ values,
379
+ ...omitted ? { "@truncated": { kind: "setValues", omitted } } : {}
380
+ };
381
+ }
382
+ if (value instanceof WeakMap) return "[WeakMap]";
383
+ if (value instanceof WeakSet) return "[WeakSet]";
384
+ if (value instanceof Promise) return "[Promise]";
385
+ if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer) {
386
+ return describeBinary(value);
387
+ }
388
+ const converted = callToJson(value);
389
+ if (converted !== void 0) {
390
+ return normalizeUnknown(converted, options, depth, path, ancestors);
391
+ }
392
+ return void 0;
393
+ }
394
+ function callToJson(value) {
395
+ let toJson;
396
+ try {
397
+ toJson = value.toJSON;
398
+ } catch {
399
+ return void 0;
400
+ }
401
+ if (typeof toJson !== "function") return void 0;
402
+ try {
403
+ return toJson.call(value);
404
+ } catch (failure) {
405
+ return `[Unreadable: ${describeFailure(failure)}]`;
406
+ }
407
+ }
408
+ function describeBinary(value) {
409
+ const typeName = readConstructorName(value) ?? "ArrayBuffer";
410
+ const byteLength = value.byteLength;
411
+ let preview;
412
+ try {
413
+ const bytes = value instanceof ArrayBuffer ? new Uint8Array(value, 0, Math.min(BINARY_PREVIEW_BYTES, byteLength)) : new Uint8Array(
414
+ value.buffer,
415
+ value.byteOffset,
416
+ Math.min(BINARY_PREVIEW_BYTES, value.byteLength)
417
+ );
418
+ preview = [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join(" ");
419
+ } catch {
420
+ preview = "";
421
+ }
422
+ return {
423
+ "@type": typeName,
424
+ byteLength,
425
+ ...preview ? { preview } : {},
426
+ ...byteLength > BINARY_PREVIEW_BYTES ? { "@truncated": { kind: "bytes", omitted: byteLength - BINARY_PREVIEW_BYTES } } : {}
427
+ };
428
+ }
429
+ function normalizeArray(value, options, depth, path, ancestors) {
430
+ const kept = [];
431
+ const limit = Math.min(value.length, options.maxArrayLength);
432
+ for (let index = 0; index < limit; index += 1) {
433
+ kept.push(
434
+ normalizeUnknown(value[index], options, depth + 1, `${path}[${index}]`, ancestors)
435
+ );
436
+ }
437
+ if (value.length > limit) {
438
+ kept.push({ "@truncated": { kind: "array", omitted: value.length - limit } });
439
+ }
440
+ return kept;
441
+ }
442
+ function normalizeObject(value, options, depth, path, ancestors) {
443
+ const result = {};
444
+ const className = readConstructorName(value);
445
+ if (className && className !== "Object") {
446
+ result["@type"] = className;
447
+ }
448
+ let keys;
449
+ try {
450
+ keys = Object.keys(value);
451
+ } catch {
452
+ return `[Unreadable: keys could not be listed]`;
453
+ }
454
+ let kept = 0;
455
+ let omitted = 0;
456
+ for (const key of keys) {
457
+ if (kept >= options.maxProperties) {
458
+ omitted += 1;
459
+ continue;
460
+ }
461
+ let propertyValue;
462
+ try {
463
+ propertyValue = value[key];
464
+ } catch (failure) {
465
+ result[key] = `[Unreadable: ${describeFailure(failure)}]`;
466
+ kept += 1;
467
+ continue;
468
+ }
469
+ if (propertyValue === void 0) continue;
470
+ result[key] = redactOrNormalize(
471
+ key,
472
+ propertyValue,
473
+ options,
474
+ depth + 1,
475
+ `${path}.${key}`,
476
+ ancestors
477
+ );
478
+ kept += 1;
479
+ }
480
+ if (omitted) {
481
+ result["@truncated"] = { kind: "properties", omitted };
482
+ }
483
+ return result;
484
+ }
485
+ function redactOrNormalize(key, value, options, depth, path, ancestors) {
486
+ if (options.redact && options.redactKeys.has(normalizeKeyForMatching(key))) {
487
+ return REDACTED;
488
+ }
489
+ return normalizeUnknown(value, options, depth, path, ancestors);
490
+ }
491
+ function normalizeKeyForMatching(key) {
492
+ return key.replace(/[^a-zA-Z0-9]/g, "").toLowerCase();
493
+ }
494
+ function readConstructorName(value) {
495
+ try {
496
+ const prototype = Object.getPrototypeOf(value);
497
+ if (prototype === null) return void 0;
498
+ const name = prototype.constructor?.name;
499
+ return typeof name === "string" && name.length ? name : void 0;
500
+ } catch {
501
+ return void 0;
502
+ }
503
+ }
504
+ function isPlainRecord(value) {
505
+ return typeof value === "object" && value !== null && !Array.isArray(value);
506
+ }
507
+ function truncateString(value, maxLength) {
508
+ if (value.length <= maxLength) return value;
509
+ return `${value.slice(0, maxLength)}\u2026[+${value.length - maxLength} chars]`;
510
+ }
511
+ function safeStringify(value, maxLength) {
512
+ try {
513
+ return truncateString(String(value), maxLength);
514
+ } catch {
515
+ return "[unstringifiable]";
516
+ }
517
+ }
518
+ function describeFailure(failure) {
519
+ if (failure instanceof Error && failure.message) return failure.message;
520
+ try {
521
+ return String(failure);
522
+ } catch {
523
+ return "unknown error";
524
+ }
525
+ }
526
+
527
+ // src/audiences/ndjsonAudience.ts
528
+ function ndjsonAudience(options = {}) {
529
+ const writer = options.stream ?? createDefaultWriter();
530
+ const minimumLevel = resolveMinimumLevel(options.level);
531
+ return {
532
+ name: options.name ?? "ndjson",
533
+ hears: ["note", "story"],
534
+ accepts: (emission) => meetsLevel(emission.level, minimumLevel),
535
+ hear: (emission) => {
536
+ writer.write(`${serializeEmission(emission)}
537
+ `);
538
+ }
539
+ };
540
+ }
541
+ function serializeEmission(emission) {
542
+ try {
543
+ return JSON.stringify(emission);
544
+ } catch {
545
+ try {
546
+ return JSON.stringify(normalizeValue(emission));
547
+ } catch {
548
+ return JSON.stringify({
549
+ kind: emission.kind,
550
+ level: emission.level,
551
+ error: "[Unserializable emission]"
552
+ });
553
+ }
554
+ }
555
+ }
556
+ function createDefaultWriter() {
557
+ const runtime = globalThis;
558
+ const write = runtime.process?.stdout?.write;
559
+ if (typeof write === "function") {
560
+ const stdout = runtime.process.stdout;
561
+ return { write: (chunk) => write.call(stdout, chunk) };
562
+ }
563
+ return { write: (chunk) => console.log(chunk.replace(/\n$/, "")) };
564
+ }
71
565
 
72
566
  // src/formatting.ts
73
567
  function formatStory(story, options = {}) {
@@ -239,40 +733,143 @@ var AudienceRegistry = class {
239
733
  return [...this.members.keys()];
240
734
  }
241
735
  };
242
- var Storyteller = class {
243
- audience = new AudienceRegistry();
736
+ var Storyteller = class _Storyteller {
737
+ audience;
244
738
  origin;
739
+ parentStoryId;
245
740
  notes = [];
741
+ narration;
742
+ minimumLevel;
743
+ onAudienceError;
744
+ maxInFlight;
745
+ /** Deliveries currently awaiting each audience, keyed by audience name */
746
+ inFlight = /* @__PURE__ */ new Map();
747
+ /** Emissions dropped for back-pressure since the current story began */
748
+ droppedEmissions = 0;
749
+ /** Identifies the story currently being collected; regenerated after each telling */
750
+ storyId = createStoryId();
751
+ /** Position of the next note within the current story */
752
+ nextSequence = 0;
246
753
  constructor(options) {
247
- this.origin = options?.origin;
248
- this.audience.add(consoleAudience());
754
+ const normalizedOrigin = normalizeOrigin(options?.origin);
755
+ if (normalizedOrigin) {
756
+ this.origin = normalizedOrigin;
757
+ }
758
+ if (options?.parentStoryId !== void 0) {
759
+ this.parentStoryId = options.parentStoryId;
760
+ }
761
+ this.narration = resolveNarration(options?.narration);
762
+ this.minimumLevel = resolveMinimumLevel(options?.level);
763
+ this.onAudienceError = options?.onAudienceError ?? reportAudienceErrorToConsole;
764
+ this.maxInFlight = options?.maxInFlight ?? DEFAULT_MAX_IN_FLIGHT;
765
+ if (options?.audience) {
766
+ this.audience = options.audience;
767
+ } else {
768
+ this.audience = new AudienceRegistry();
769
+ this.audience.add(
770
+ resolveOutputFormat(options?.format) === "ndjson" ? ndjsonAudience({ level: this.minimumLevel }) : consoleAudience()
771
+ );
772
+ }
249
773
  options?.audiences?.forEach((audience) => this.audience.add(audience));
250
774
  }
251
775
  /**
252
- * Add a timestamped note to the current story.
253
- * @param text - What happened
776
+ * Switch between collected and live narration at runtime.
777
+ * Takes effect on the next note; already-buffered notes are not replayed.
778
+ *
779
+ * @param narration - `collected` to buffer, `live` to emit each note as it happens
780
+ * @returns `this` for chaining
781
+ */
782
+ narrate(narration) {
783
+ this.narration = resolveNarration(narration);
784
+ return this;
785
+ }
786
+ /** The id of the story currently being collected */
787
+ get currentStoryId() {
788
+ return this.storyId;
789
+ }
790
+ /**
791
+ * Start a chapter: a child storyteller whose stories are linked back to this
792
+ * one by `parentStoryId`.
793
+ *
794
+ * Real work nests — an agent spawns subtasks, a batch runs per-item operations.
795
+ * A chapter keeps each of those a complete story in its own right while leaving
796
+ * the run reconstructable as a tree.
797
+ *
798
+ * The child shares this storyteller's audience registry, so audiences added
799
+ * later reach it too, and inherits narration, level and delivery settings.
800
+ * Its stories are separate records — a chapter is not folded into the parent's
801
+ * notes.
802
+ *
803
+ * @param options - Origin to merge over the parent's, and any setting to override
804
+ * @returns A child Storyteller
805
+ *
806
+ * @example
807
+ * ```ts
808
+ * for (const account of accounts) {
809
+ * const chapter = story.chapter({ origin: { what: account.id } });
810
+ * chapter.report("Fetching invoices");
811
+ * chapter.finish(`Synced ${account.id}`);
812
+ * }
813
+ * ```
814
+ */
815
+ chapter(options = {}) {
816
+ const mergedOrigin = { ...this.origin, ...options.origin };
817
+ return new _Storyteller({
818
+ audience: this.audience,
819
+ // Captured now, so a parent that finishes first does not orphan its chapters
820
+ parentStoryId: this.storyId,
821
+ ...Object.keys(mergedOrigin).length ? { origin: mergedOrigin } : {},
822
+ narration: options.narration ?? this.narration,
823
+ level: options.level ?? this.minimumLevel,
824
+ onAudienceError: options.onAudienceError ?? this.onAudienceError,
825
+ maxInFlight: options.maxInFlight ?? this.maxInFlight
826
+ });
827
+ }
828
+ /**
829
+ * Report a beat of the current story.
830
+ *
831
+ * In collected narration the beat is buffered and leaves with the story. In live
832
+ * narration it is emitted the moment you call this, so whoever is tuned in sees
833
+ * the work as it happens.
834
+ *
835
+ * Accepts anything, not just a string — pass an error, an API response, a Map, a
836
+ * class instance — and the value is normalized into a storable shape with the note
837
+ * text derived from it.
838
+ *
839
+ * @param input - What happened: a message, or any value to describe
254
840
  * @param data - Optional context: who did it, what was involved, where it happened, any error
255
841
  * @returns `this` for chaining
256
842
  *
257
843
  * @example
258
844
  * ```ts
259
- * story.note("Card charged", { what: { amount: "$42" }, where: "stripe" });
845
+ * story.report("Card charged", { what: { amount: "$42" }, where: "stripe" });
846
+ * story.report(await response.json());
260
847
  * ```
261
848
  */
262
- note(text, data = {}) {
263
- this.notes.push({
849
+ report(input, data = {}) {
850
+ const described = describeInput(input);
851
+ const level = toStoryLevel(data.level);
852
+ const note = {
264
853
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
265
- note: text,
266
- ...data.who ? { who: data.who } : {},
267
- ...data.what ? { what: data.what } : {},
268
- ...data.where ? { where: data.where } : {},
269
- ...data.error ? { error: normalizeError(data.error) } : {}
270
- });
854
+ sequence: this.nextSequence,
855
+ note: described.text,
856
+ ...level !== "Information" ? { level } : {},
857
+ ...data.who !== void 0 ? { who: normalizeValue(data.who) } : {},
858
+ ...data.what !== void 0 ? { what: normalizeValue(data.what) } : described.what !== void 0 ? { what: described.what } : {},
859
+ ...data.where !== void 0 ? { where: normalizeValue(data.where) } : {},
860
+ ...data.error !== void 0 ? { error: normalizeError(data.error) } : described.error !== void 0 ? { error: described.error } : {}
861
+ };
862
+ this.nextSequence += 1;
863
+ this.notes.push(note);
864
+ if (this.narration === "live" || data.live) {
865
+ this.emitNote(note, level, data.to);
866
+ }
271
867
  return this;
272
868
  }
273
- /** Clear all accumulated notes without emitting a story */
869
+ /** Clear all accumulated notes without emitting a story, and start a new story id */
274
870
  reset() {
275
871
  this.notes = [];
872
+ this.startNewStory();
276
873
  return this;
277
874
  }
278
875
  /** Preview the current notes as a formatted report without emitting or clearing them */
@@ -287,27 +884,67 @@ var Storyteller = class {
287
884
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
288
885
  level,
289
886
  title,
887
+ storyId: this.storyId,
888
+ ...this.parentStoryId !== void 0 ? { parentStoryId: this.parentStoryId } : {},
290
889
  ...this.origin ? { origin: this.origin } : {},
291
890
  notes: [...this.notes],
292
- ...error ? { error: normalizeError(error) } : {}
891
+ ...error !== void 0 ? { error: normalizeError(error) } : {}
293
892
  };
294
893
  return formatStory(event, reportOptions);
295
894
  }
296
- /** Tell a success story — everything went well */
895
+ /**
896
+ * Finish the story: emit everything collected so far as one record, and start fresh.
897
+ *
898
+ * @param title - What the story was about
899
+ * @param options - Level, and the error that ended it
900
+ * @returns A one-shot handle whose `.to()` overrides the audience list — call it
901
+ * synchronously, delivery happens on the next microtask
902
+ *
903
+ * @example
904
+ * ```ts
905
+ * story.finish("Sync complete");
906
+ * story.finish("Sync failed", { level: "oops", error }).to("db");
907
+ * ```
908
+ */
909
+ finish(title, options = {}) {
910
+ return this.createDelivery(toStoryLevel(options.level), title, options.error);
911
+ }
912
+ /** @deprecated Use `finish(title)`. Removed at 1.0. */
297
913
  tell(title) {
298
- return this.createDelivery("tell", title);
914
+ warnDeprecated("tell", "finish");
915
+ return this.createDelivery("Information", title);
299
916
  }
300
- /** Tell a cautionary story — something was off but it's handled */
917
+ /** @deprecated Use `finish(title, { level: "warn" })`. Removed at 1.0. */
301
918
  warn(title) {
302
- return this.createDelivery("warn", title);
919
+ warnDeprecated("warn", 'finish(title, { level: "warn" })');
920
+ return this.createDelivery("Warning", title);
303
921
  }
304
- /** Tell an error story — something broke. Pass the error for automatic normalization. */
922
+ /** @deprecated Use `finish(title, { level: "oops", error })`. Removed at 1.0. */
305
923
  oops(title, error) {
306
- return this.createDelivery("oops", title, error);
924
+ warnDeprecated("oops", 'finish(title, { level: "oops", error })');
925
+ return this.createDelivery("Error", title, error);
926
+ }
927
+ /** @deprecated Use `report()`. Removed at 1.0. */
928
+ note(input, data = {}) {
929
+ warnDeprecated("note", "report");
930
+ return this.report(input, data);
931
+ }
932
+ /** Emit a single note to the audiences listening for notes */
933
+ emitNote(note, level, only) {
934
+ const emission = {
935
+ ...note,
936
+ kind: "note",
937
+ storyId: this.storyId,
938
+ ...this.parentStoryId !== void 0 ? { parentStoryId: this.parentStoryId } : {},
939
+ sequence: note.sequence ?? 0,
940
+ level,
941
+ ...this.origin ? { origin: this.origin } : {}
942
+ };
943
+ void this.deliver(emission, only ? { only } : {});
307
944
  }
308
945
  /** Build a story event and schedule delivery, returning a handle to override the audience list */
309
- createDelivery(method, title, error) {
310
- const event = this.buildEvent(method, title, error);
946
+ createDelivery(level, title, error) {
947
+ const event = this.buildEvent(level, title, error);
311
948
  let delivered = false;
312
949
  let defaultCancelled = false;
313
950
  queueMicrotask(() => {
@@ -324,60 +961,173 @@ var Storyteller = class {
324
961
  }
325
962
  };
326
963
  }
327
- /** Assemble the story event from current notes and clear notes for the next story */
328
- buildEvent(method, title, error) {
964
+ /** Assemble the story event from current notes and start a fresh story */
965
+ buildEvent(level, title, error) {
329
966
  const now = (/* @__PURE__ */ new Date()).toISOString();
330
- const level = methodToLevel(method);
967
+ const storyId = this.storyId;
968
+ const droppedEmissions = this.droppedEmissions;
331
969
  const sortedNotes = [...this.notes].sort(
332
- (noteA, noteB) => Date.parse(noteA.timestamp) - Date.parse(noteB.timestamp)
970
+ (noteA, noteB) => Date.parse(noteA.timestamp) - Date.parse(noteB.timestamp) || (noteA.sequence ?? 0) - (noteB.sequence ?? 0)
333
971
  );
334
972
  this.notes = [];
973
+ this.startNewStory();
335
974
  const durationMs = calculateNoteDuration2(sortedNotes).durationMs;
336
975
  const event = {
337
976
  timestamp: now,
338
977
  level,
339
978
  title,
979
+ storyId,
980
+ ...this.parentStoryId !== void 0 ? { parentStoryId: this.parentStoryId } : {},
340
981
  ...this.origin ? { origin: this.origin } : {},
341
982
  notes: sortedNotes,
342
983
  ...durationMs != null ? { durationMs } : {},
343
- ...error ? { error: normalizeError(error) } : {}
984
+ ...droppedEmissions ? { droppedEmissions } : {},
985
+ ...error !== void 0 ? { error: normalizeError(error) } : {}
344
986
  };
345
987
  const eventWithSummary = event;
988
+ Object.defineProperty(eventWithSummary, "kind", {
989
+ value: "story",
990
+ enumerable: true
991
+ });
346
992
  Object.defineProperty(eventWithSummary, "summarize", {
347
993
  value: (options) => formatStory(event, options),
348
994
  enumerable: false
349
995
  });
350
996
  return eventWithSummary;
351
997
  }
352
- /** Deliver a story event to matching audience members */
353
- async deliver(event, options) {
998
+ /** Begin a new story: fresh id, sequence back to zero */
999
+ startNewStory() {
1000
+ this.storyId = createStoryId();
1001
+ this.nextSequence = 0;
1002
+ this.droppedEmissions = 0;
1003
+ }
1004
+ /** Deliver an emission to the audience members listening for its kind */
1005
+ async deliver(emission, options) {
1006
+ if (!meetsLevel(emission.level, this.minimumLevel)) return;
354
1007
  const targets = options?.only?.length ? this.audience.getOnly(options.only) : this.audience.getAll();
355
- await Promise.allSettled(
356
- targets.filter((member) => member.accepts ? member.accepts(event) : true).map((member) => member.hear(event))
1008
+ await Promise.all(
1009
+ targets.filter((member) => hearsKind(member, emission.kind)).filter((member) => this.acceptsSafely(member, emission)).map((member) => this.hearSafely(member, emission))
357
1010
  );
358
1011
  }
1012
+ /** Run an audience's accepts() without letting a throw from it lose the emission */
1013
+ acceptsSafely(member, emission) {
1014
+ if (!member.accepts) return true;
1015
+ try {
1016
+ return member.accepts(emission);
1017
+ } catch (error) {
1018
+ this.handleAudienceError(error, member, emission);
1019
+ return false;
1020
+ }
1021
+ }
1022
+ /**
1023
+ * Hand an emission to one audience, keeping its failures and its slowness
1024
+ * contained: a throw is reported rather than swallowed, and a backlog is dropped
1025
+ * rather than grown without limit.
1026
+ */
1027
+ async hearSafely(member, emission) {
1028
+ const pending = this.inFlight.get(member.name) ?? 0;
1029
+ if (pending >= this.maxInFlight) {
1030
+ this.droppedEmissions += 1;
1031
+ return;
1032
+ }
1033
+ this.inFlight.set(member.name, pending + 1);
1034
+ try {
1035
+ await member.hear(emission);
1036
+ } catch (error) {
1037
+ this.handleAudienceError(error, member, emission);
1038
+ } finally {
1039
+ const remaining = (this.inFlight.get(member.name) ?? 1) - 1;
1040
+ if (remaining > 0) this.inFlight.set(member.name, remaining);
1041
+ else this.inFlight.delete(member.name);
1042
+ }
1043
+ }
1044
+ /** Report an audience failure without ever letting it reach caller code */
1045
+ handleAudienceError(error, member, emission) {
1046
+ try {
1047
+ this.onAudienceError(error, member, emission);
1048
+ } catch {
1049
+ }
1050
+ }
359
1051
  };
360
- function methodToLevel(method) {
361
- if (method === "tell") return "Information";
362
- if (method === "warn") return "Warning";
363
- return "Error";
364
- }
365
- function normalizeError(rawError) {
366
- if (rawError instanceof Error) {
367
- const normalized = {
368
- name: rawError.name,
369
- message: rawError.message
370
- };
371
- if (rawError.stack !== void 0) {
372
- normalized.stack = rawError.stack;
1052
+ var warnedDeprecations = /* @__PURE__ */ new Set();
1053
+ function warnDeprecated(oldName, replacement) {
1054
+ if (warnedDeprecations.has(oldName)) return;
1055
+ if (readEnvironmentValue("STORYTELLER_DEPRECATION_WARNINGS") !== "1") return;
1056
+ warnedDeprecations.add(oldName);
1057
+ console.warn(
1058
+ `Storyteller: ${oldName}() is deprecated and will be removed at 1.0 \u2014 use ${replacement}.`
1059
+ );
1060
+ }
1061
+ var DEFAULT_MAX_IN_FLIGHT = 1e3;
1062
+ var AUDIENCE_ERROR_THROTTLE_MS = 5e3;
1063
+ var lastReportedAudienceError = /* @__PURE__ */ new Map();
1064
+ function reportAudienceErrorToConsole(error, member, emission) {
1065
+ const now = Date.now();
1066
+ const lastReported = lastReportedAudienceError.get(member.name);
1067
+ if (lastReported !== void 0 && now - lastReported < AUDIENCE_ERROR_THROTTLE_MS) {
1068
+ return;
1069
+ }
1070
+ lastReportedAudienceError.set(member.name, now);
1071
+ const reason = error instanceof Error ? error.message : String(error);
1072
+ console.error(
1073
+ `Storyteller: audience "${member.name}" failed to hear a ${emission.kind} \u2014 ${reason}`
1074
+ );
1075
+ }
1076
+ function hearsKind(member, kind) {
1077
+ const kinds = member.hears ?? ["story"];
1078
+ return kinds.includes(kind);
1079
+ }
1080
+ function resolveNarration(requested) {
1081
+ const value = requested ?? readEnvironmentValue("STORYTELLER_NARRATION");
1082
+ if (value === "live" || value === "both") return "live";
1083
+ return "collected";
1084
+ }
1085
+ function createStoryId() {
1086
+ try {
1087
+ const runtimeCrypto = globalThis.crypto;
1088
+ if (typeof runtimeCrypto?.randomUUID === "function") {
1089
+ return runtimeCrypto.randomUUID();
373
1090
  }
374
- const cause = rawError.cause;
375
- if (cause !== void 0) {
376
- normalized.cause = cause;
1091
+ } catch {
1092
+ }
1093
+ return `story-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
1094
+ }
1095
+ function normalizeOrigin(origin) {
1096
+ if (!origin) return void 0;
1097
+ const normalized = {
1098
+ ...origin.who !== void 0 ? { who: normalizeValue(origin.who) } : {},
1099
+ ...origin.what !== void 0 ? { what: normalizeValue(origin.what) } : {},
1100
+ ...origin.where !== void 0 ? { where: normalizeValue(origin.where) } : {}
1101
+ };
1102
+ return Object.keys(normalized).length ? normalized : void 0;
1103
+ }
1104
+ function describeInput(input) {
1105
+ if (typeof input === "string") return { text: input };
1106
+ if (input instanceof Error) {
1107
+ const error = normalizeError(input);
1108
+ const label = [error.name, error.message].filter(Boolean).join(": ");
1109
+ return { text: label || "Error", error };
1110
+ }
1111
+ if (input === null) return { text: "null" };
1112
+ if (input === void 0) return { text: "undefined" };
1113
+ const normalized = normalizeValue(input);
1114
+ if (typeof normalized !== "object" || normalized === null) {
1115
+ return { text: String(normalized), what: normalized };
1116
+ }
1117
+ if (Array.isArray(normalized)) {
1118
+ return { text: `Array(${normalized.length})`, what: normalized };
1119
+ }
1120
+ for (const key of ["message", "title", "name", "summary", "event"]) {
1121
+ const candidate = normalized[key];
1122
+ if (typeof candidate === "string" && candidate.length) {
1123
+ return { text: candidate, what: normalized };
377
1124
  }
378
- return normalized;
379
1125
  }
380
- return { message: String(rawError) };
1126
+ const typeName = normalized["@type"];
1127
+ return {
1128
+ text: typeof typeName === "string" ? typeName : "Object",
1129
+ what: normalized
1130
+ };
381
1131
  }
382
1132
  function calculateNoteDuration2(notes) {
383
1133
  if (notes.length <= 1) {
@@ -396,7 +1146,10 @@ function calculateNoteDuration2(notes) {
396
1146
  var sharedInstance;
397
1147
  function useStoryteller(options = {}) {
398
1148
  if (!sharedInstance || options.reset) {
399
- sharedInstance = new Storyteller({ origin: options.origin });
1149
+ sharedInstance = new Storyteller({
1150
+ ...options.origin !== void 0 ? { origin: options.origin } : {},
1151
+ ...options.narration !== void 0 ? { narration: options.narration } : {}
1152
+ });
400
1153
  return sharedInstance;
401
1154
  }
402
1155
  return sharedInstance;
@@ -406,9 +1159,11 @@ function useStoryteller(options = {}) {
406
1159
  function dbAudience(insert) {
407
1160
  return {
408
1161
  name: "db",
409
- accepts: (event) => event.level === "Warning" || event.level === "Error",
410
- hear: async (event) => {
411
- await insert(event);
1162
+ hears: ["story"],
1163
+ accepts: (emission) => emission.kind === "story" && (emission.level === "Warning" || emission.level === "Error"),
1164
+ hear: async (emission) => {
1165
+ if (emission.kind !== "story") return;
1166
+ await insert(emission);
412
1167
  }
413
1168
  };
414
1169
  }
@@ -515,6 +1270,9 @@ function writeStoryReport(stories, options = {}) {
515
1270
  }
516
1271
  export {
517
1272
  ANSI,
1273
+ AudienceRegistry,
1274
+ DEFAULT_REDACT_KEYS,
1275
+ REDACTED,
518
1276
  Storyteller,
519
1277
  consoleAudience,
520
1278
  dbAudience,
@@ -522,7 +1280,17 @@ export {
522
1280
  formatOrigin,
523
1281
  formatStory,
524
1282
  getLevelColor,
1283
+ meetsLevel,
1284
+ ndjsonAudience,
1285
+ normalizeError,
1286
+ normalizeValue,
1287
+ readEnvironmentValue,
1288
+ resolveColors,
1289
+ resolveMinimumLevel,
1290
+ resolveOutputFormat,
1291
+ summarizeContext,
525
1292
  summarizeStory,
1293
+ toStoryLevel,
526
1294
  useStoryteller,
527
1295
  writeStoryReport
528
1296
  };