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