@lovelaces-io/storyteller 0.1.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,19 +21,79 @@ 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
- colorizeJsonSections: () => colorizeJsonSections,
26
28
  consoleAudience: () => consoleAudience,
27
- countBrackets: () => countBrackets,
28
29
  dbAudience: () => dbAudience,
30
+ formatDuration: () => formatDuration,
29
31
  formatOrigin: () => formatOrigin,
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
 
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");
95
+ }
96
+
37
97
  // src/utils.ts
38
98
  var ANSI = {
39
99
  reset: "\x1B[0m",
@@ -41,18 +101,24 @@ var ANSI = {
41
101
  yellow: "\x1B[33m",
42
102
  red: "\x1B[38;2;250;128;114m",
43
103
  grayLight: "\x1B[37m",
44
- grayDark: "\x1B[37m"
104
+ grayDark: "\x1B[90m"
45
105
  };
46
106
  function getLevelColor(level) {
47
- if (level === "tell") return ANSI.green;
48
- if (level === "warn") return ANSI.yellow;
107
+ if (level === "Information") return ANSI.green;
108
+ if (level === "Warning") return ANSI.yellow;
49
109
  return ANSI.red;
50
110
  }
51
111
  function formatOrigin(origin) {
52
112
  if (!origin?.where) return;
53
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
+ }
54
117
  const whereRecord = origin.where;
55
- const parts = [whereRecord.app, whereRecord.service, whereRecord.page, whereRecord.component].filter(Boolean).map(String);
118
+ const priorityKeys = ["app", "service", "page", "component"];
119
+ const priorityParts = priorityKeys.filter((key) => whereRecord[key] != null).map((key) => String(whereRecord[key]));
120
+ const extraParts = Object.entries(whereRecord).filter(([key, value]) => !priorityKeys.includes(key) && value != null).map(([_, value]) => String(value));
121
+ const parts = [...priorityParts, ...extraParts];
56
122
  return parts.length ? parts.join(" / ") : void 0;
57
123
  }
58
124
  function colorizeJsonSections(json, colors) {
@@ -79,192 +145,482 @@ function countBrackets(line) {
79
145
  const closeCount = (line.match(/\]/g) || []).length;
80
146
  return openCount - closeCount;
81
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
+ }
82
183
 
83
184
  // src/audiences/consoleAudience.ts
84
- function consoleAudience() {
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);
85
197
  return {
86
198
  name: "console",
87
- hear: (event) => {
88
- const prefix = "Storyteller";
89
- const style = event.level === "tell" ? "color:#16a34a;font-weight:600" : event.level === "warn" ? "color:#f59e0b;font-weight:600" : "color:#dc2626;font-weight:600";
90
- const header = `${prefix}: ${event.title}`;
91
- console.groupCollapsed(`%c${header}`, style);
92
- const payload = JSON.stringify(event, null, 2);
93
- const coloredPayload = event.level === "oops" ? `${ANSI.red}${payload}${ANSI.reset}` : payload;
94
- if (event.level === "tell") {
95
- console.log(header, payload);
96
- } else if (event.level === "warn") {
97
- console.warn(header, payload);
98
- } else {
99
- console.error(header, coloredPayload);
199
+ hears: ["note", "story"],
200
+ hear: (emission) => {
201
+ if (emission.kind === "note") {
202
+ printNote(emission, colors);
203
+ return;
100
204
  }
101
- console.groupEnd();
205
+ printStory(emission);
102
206
  }
103
207
  };
104
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
+ }
105
248
 
106
- // src/storyteller.ts
107
- var AudienceRegistry = class {
108
- members = /* @__PURE__ */ new Map();
109
- /** Register an audience member, replacing any existing member with the same name */
110
- add(member) {
111
- this.members.set(member.name, member);
112
- return this;
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)}]`;
113
287
  }
114
- /** Remove an audience member by name */
115
- remove(name) {
116
- this.members.delete(name);
117
- return this;
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) };
118
316
  }
119
- /** Return all registered audience members */
120
- getAll() {
121
- return [...this.members.values()];
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);
122
323
  }
123
- /** Return only the audience members matching the given names */
124
- getOnly(names) {
125
- return names.map((name) => this.members.get(name)).filter(Boolean);
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
+ }
126
333
  }
127
- };
128
- var Storyteller = class {
129
- audience = new AudienceRegistry();
130
- origin;
131
- notes = [];
132
- constructor(options) {
133
- this.origin = options?.origin;
134
- this.audience.add(consoleAudience());
135
- options?.audiences?.forEach((audience) => this.audience.add(audience));
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));
136
337
  }
137
- /** Add a timestamped note with optional context (who, what, where, error) */
138
- note(text, data = {}) {
139
- this.notes.push({
140
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
141
- note: text,
142
- ...data.who ? { who: data.who } : {},
143
- ...data.what ? { what: data.what } : {},
144
- ...data.where ? { where: data.where } : {},
145
- ...data.error ? { error: normalizeError(data.error) } : {}
146
- });
147
- return this;
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);
148
345
  }
149
- /** Clear all accumulated notes without emitting a story */
150
- reset() {
151
- this.notes = [];
152
- return this;
346
+ if (valueType === "number") {
347
+ return Number.isFinite(value) ? value : String(value);
153
348
  }
154
- /** Generate a formatted summary of current notes without emitting or clearing them */
155
- summarize(options = {}) {
156
- const {
157
- title = "Story preview",
158
- level = "tell",
159
- error,
160
- ...summaryOptions
161
- } = options;
162
- const event = {
163
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
164
- level,
165
- title,
166
- ...this.origin ? { origin: this.origin } : {},
167
- notes: [...this.notes],
168
- ...error ? { error: normalizeError(error) } : {}
169
- };
170
- return summarizeStory(event, summaryOptions);
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"}]`;
171
356
  }
172
- /** Emit a story at the "tell" level (success / informational) */
173
- tell(title) {
174
- return this.createDelivery("tell", title);
357
+ const objectValue = value;
358
+ const existingPath = ancestors.get(objectValue);
359
+ if (existingPath !== void 0) {
360
+ return `[Circular \u2192 ${existingPath}]`;
175
361
  }
176
- /** Emit a story at the "warn" level (something was off) */
177
- warn(title) {
178
- return this.createDelivery("warn", title);
362
+ if (depth > options.maxDepth) {
363
+ return { "@truncated": { kind: "depth", depth: options.maxDepth } };
179
364
  }
180
- /** Emit a story at the "oops" level (something broke) with an optional error */
181
- oops(title, error) {
182
- return this.createDelivery("oops", title, error);
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);
183
375
  }
184
- /** Build a story event and schedule delivery, returning a handle to override the audience list */
185
- createDelivery(level, title, error) {
186
- const event = this.buildEvent(level, title, error);
187
- let delivered = false;
188
- let defaultCancelled = false;
189
- queueMicrotask(() => {
190
- if (delivered || defaultCancelled) return;
191
- delivered = true;
192
- void this.deliver(event);
193
- });
194
- return {
195
- to: (...names) => {
196
- defaultCancelled = true;
197
- if (delivered) return;
198
- delivered = true;
199
- void this.deliver(event, { only: names });
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;
200
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 } } : {}
201
411
  };
202
412
  }
203
- /** Assemble the story event from current notes and clear notes for the next story */
204
- buildEvent(level, title, error) {
205
- const now = (/* @__PURE__ */ new Date()).toISOString();
206
- const collectedNotes = [...this.notes];
207
- this.notes = [];
208
- const event = {
209
- timestamp: now,
210
- level,
211
- title,
212
- ...this.origin ? { origin: this.origin } : {},
213
- notes: collectedNotes,
214
- ...error ? { error: normalizeError(error) } : {}
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 } } : {}
215
429
  };
216
- const eventWithSummary = event;
217
- Object.defineProperty(eventWithSummary, "summarize", {
218
- value: (options) => summarizeStory(event, options),
219
- enumerable: false
220
- });
221
- return eventWithSummary;
222
430
  }
223
- /** Deliver a story event to matching audience members */
224
- async deliver(event, options) {
225
- const targets = options?.only?.length ? this.audience.getOnly(options.only) : this.audience.getAll();
226
- await Promise.allSettled(
227
- targets.filter((member) => member.accepts ? member.accepts(event) : true).map((member) => member.hear(event))
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)
228
466
  );
467
+ preview = [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join(" ");
468
+ } catch {
469
+ preview = "";
229
470
  }
230
- };
231
- function normalizeError(rawError) {
232
- if (rawError instanceof Error) {
233
- const normalized = {
234
- name: rawError.name,
235
- message: rawError.message
236
- };
237
- if (rawError.stack !== void 0) {
238
- normalized.stack = rawError.stack;
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;
239
509
  }
240
- const cause = rawError.cause;
241
- if (cause !== void 0) {
242
- normalized.cause = cause;
510
+ let propertyValue;
511
+ try {
512
+ propertyValue = value[key];
513
+ } catch (failure) {
514
+ result[key] = `[Unreadable: ${describeFailure(failure)}]`;
515
+ kept += 1;
516
+ continue;
243
517
  }
244
- return normalized;
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 };
245
531
  }
246
- return { message: String(rawError) };
532
+ return result;
247
533
  }
248
- function calculateNoteDuration(notes) {
249
- if (notes.length <= 1) {
250
- return {
251
- durationMs: void 0
252
- };
534
+ function redactOrNormalize(key, value, options, depth, path, ancestors) {
535
+ if (options.redact && options.redactKeys.has(normalizeKeyForMatching(key))) {
536
+ return REDACTED;
253
537
  }
254
- const startTime = Date.parse(notes[0].timestamp);
255
- const endTime = Date.parse(notes[notes.length - 1].timestamp);
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);
256
580
  return {
257
- durationMs: Number.isFinite(startTime) && Number.isFinite(endTime) ? Math.max(0, endTime - startTime) : void 0
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
+ }
258
588
  };
259
589
  }
260
- function summarizeStory(story, options = {}) {
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
+ }
614
+
615
+ // src/formatting.ts
616
+ function formatStory(story, options = {}) {
261
617
  const {
262
618
  timezone = Intl.DateTimeFormat().resolvedOptions().timeZone,
263
619
  locale = "en-US",
264
- verbosity = "normal",
265
- maxNotes = 50,
620
+ detail = "normal",
621
+ noteLimit = 50,
266
622
  showData = true,
267
- colorize = true
623
+ colors = true
268
624
  } = options;
269
625
  const dateTimeFormatter = new Intl.DateTimeFormat(locale, {
270
626
  timeZone: timezone,
@@ -284,15 +640,14 @@ function summarizeStory(story, options = {}) {
284
640
  const orderedNotes = [...story.notes].sort(
285
641
  (noteA, noteB) => Date.parse(noteA.timestamp) - Date.parse(noteB.timestamp)
286
642
  );
287
- const noteTiming = calculateNoteDuration(orderedNotes);
288
- const originLabel = formatOrigin(story.origin);
289
- const duration = noteTiming.durationMs != null ? formatDuration(noteTiming.durationMs) : void 0;
290
- const slicedNotes = orderedNotes.slice(0, maxNotes);
291
- const summaryNotes = slicedNotes.map((note) => ({
643
+ const durationMs = story.durationMs ?? calculateNoteDuration(orderedNotes);
644
+ const duration = durationMs != null ? formatDuration(durationMs) : void 0;
645
+ const slicedNotes = orderedNotes.slice(0, noteLimit);
646
+ const reportNotes = slicedNotes.map((note) => ({
292
647
  timestamp: note.timestamp,
293
648
  when: timeFormatter.format(new Date(note.timestamp)),
294
649
  note: note.note,
295
- text: formatNoteText(note, verbosity),
650
+ text: formatNoteText(note, detail),
296
651
  ...note.who ? { who: note.who } : {},
297
652
  ...note.what ? { what: note.what } : {},
298
653
  ...note.where ? { where: note.where } : {},
@@ -302,18 +657,28 @@ function summarizeStory(story, options = {}) {
302
657
  title: story.title,
303
658
  level: story.level,
304
659
  when: dateTimeFormatter.format(new Date(story.timestamp)),
305
- ...noteTiming.durationMs != null ? { durationMs: noteTiming.durationMs } : {},
660
+ ...durationMs != null ? { durationMs } : {},
306
661
  ...duration ? { duration } : {},
307
662
  ...story.origin ? { origin: story.origin } : {},
308
- notes: summaryNotes,
663
+ notes: reportNotes,
309
664
  ...story.error ? { error: story.error } : {}
310
665
  };
666
+ const lines = buildReportText(story, data, reportNotes, orderedNotes, {
667
+ colors,
668
+ detail,
669
+ showData,
670
+ duration
671
+ });
672
+ return { text: lines.join("\n"), data };
673
+ }
674
+ function buildReportText(story, data, reportNotes, orderedNotes, options) {
311
675
  const levelColor = getLevelColor(story.level);
312
- const label = (text) => colorize ? `${levelColor}${text}${ANSI.reset}` : text;
676
+ const label = (text) => options.colors ? `${levelColor}${text}${ANSI.reset}` : text;
677
+ const originLabel = formatOrigin(story.origin);
313
678
  const lines = [];
314
679
  lines.push(`${label("Story")}: ${story.title}`);
315
680
  lines.push(`${label("Level")}: ${story.level}`);
316
- lines.push(`${label("Time")}: ${data.when}${duration ? ` (${duration})` : ""}`);
681
+ lines.push(`${label("Time")}: ${data.when}${options.duration ? ` (${options.duration})` : ""}`);
317
682
  if (originLabel) {
318
683
  lines.push(`${label("Origin")}: ${originLabel}`);
319
684
  }
@@ -321,19 +686,19 @@ function summarizeStory(story, options = {}) {
321
686
  const errorLine = [story.error.name, story.error.message].filter(Boolean).join(": ");
322
687
  if (errorLine) lines.push(`${label("Error")}: ${errorLine}`);
323
688
  }
324
- if (verbosity !== "brief" && summaryNotes.length) {
689
+ if (options.detail !== "brief" && reportNotes.length) {
325
690
  lines.push(`${label("Notes")}:`);
326
- for (const note of summaryNotes) {
691
+ for (const note of reportNotes) {
327
692
  lines.push(` ${note.when} \u2014 ${note.text}`);
328
693
  }
329
- if (orderedNotes.length > summaryNotes.length) {
330
- lines.push(` \u2026 (${orderedNotes.length - summaryNotes.length} more)`);
694
+ if (orderedNotes.length > reportNotes.length) {
695
+ lines.push(` \u2026 (${orderedNotes.length - reportNotes.length} more)`);
331
696
  }
332
697
  }
333
- if (showData) {
698
+ if (options.showData) {
334
699
  lines.push(`${label("Data")}:`);
335
700
  const json = JSON.stringify(data, null, 2);
336
- if (colorize) {
701
+ if (options.colors) {
337
702
  const colored = colorizeJsonSections(json, {
338
703
  base: ANSI.grayLight,
339
704
  notes: ANSI.grayDark,
@@ -344,7 +709,13 @@ function summarizeStory(story, options = {}) {
344
709
  lines.push(...json.split("\n"));
345
710
  }
346
711
  }
347
- return { text: lines.join("\n"), data };
712
+ return lines;
713
+ }
714
+ function calculateNoteDuration(notes) {
715
+ if (notes.length <= 1) return void 0;
716
+ const startTime = Date.parse(notes[0].timestamp);
717
+ const endTime = Date.parse(notes[notes.length - 1].timestamp);
718
+ return Number.isFinite(startTime) && Number.isFinite(endTime) ? Math.max(0, endTime - startTime) : void 0;
348
719
  }
349
720
  function formatDuration(milliseconds) {
350
721
  if (milliseconds < 1e3) return `${milliseconds}ms`;
@@ -354,6 +725,7 @@ function formatDuration(milliseconds) {
354
725
  const remainingSeconds = Math.round(seconds % 60).toString().padStart(2, "0");
355
726
  return `${minutes}:${remainingSeconds}m`;
356
727
  }
728
+ var summarizeStory = formatStory;
357
729
  function formatNoteText(note, verbosity) {
358
730
  if (verbosity !== "full") return note.note;
359
731
  const details = [];
@@ -362,13 +734,16 @@ function formatNoteText(note, verbosity) {
362
734
  if (typeof what === "string") {
363
735
  details.push(`what=${what}`);
364
736
  } else if (what) {
365
- if (what.field) details.push(`field=${String(what.field)}`);
366
- if (what.status) details.push(`status=${String(what.status)}`);
737
+ for (const [key, value] of Object.entries(what)) {
738
+ if (value != null) details.push(`${key}=${String(value)}`);
739
+ }
367
740
  }
368
741
  if (typeof where === "string") {
369
742
  details.push(`where=${where}`);
370
743
  } else if (where) {
371
- if (where.component) details.push(`component=${String(where.component)}`);
744
+ for (const [key, value] of Object.entries(where)) {
745
+ if (value != null) details.push(`${key}=${String(value)}`);
746
+ }
372
747
  }
373
748
  if (note.error) {
374
749
  const errorLine = [note.error.name, note.error.message].filter(Boolean).join(": ");
@@ -377,11 +752,453 @@ function formatNoteText(note, verbosity) {
377
752
  return details.length ? `${note.note} (${details.join(" ")})` : note.note;
378
753
  }
379
754
 
755
+ // src/storyteller.ts
756
+ var AudienceRegistry = class {
757
+ members = /* @__PURE__ */ new Map();
758
+ /** Register an audience member, replacing any existing member with the same name */
759
+ add(member) {
760
+ this.members.set(member.name, member);
761
+ return this;
762
+ }
763
+ /** Remove an audience member by name */
764
+ remove(name) {
765
+ this.members.delete(name);
766
+ return this;
767
+ }
768
+ /** Return all registered audience members */
769
+ getAll() {
770
+ return [...this.members.values()];
771
+ }
772
+ /** Return only the audience members matching the given names */
773
+ getOnly(names) {
774
+ return names.map((name) => this.members.get(name)).filter(Boolean);
775
+ }
776
+ /** Check if an audience member is registered by name */
777
+ has(name) {
778
+ return this.members.has(name);
779
+ }
780
+ /** List the names of all registered audience members */
781
+ names() {
782
+ return [...this.members.keys()];
783
+ }
784
+ };
785
+ var Storyteller = class _Storyteller {
786
+ audience;
787
+ origin;
788
+ parentStoryId;
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;
802
+ constructor(options) {
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
+ }
822
+ options?.audiences?.forEach((audience) => this.audience.add(audience));
823
+ }
824
+ /**
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
889
+ * @param data - Optional context: who did it, what was involved, where it happened, any error
890
+ * @returns `this` for chaining
891
+ *
892
+ * @example
893
+ * ```ts
894
+ * story.report("Card charged", { what: { amount: "$42" }, where: "stripe" });
895
+ * story.report(await response.json());
896
+ * ```
897
+ */
898
+ report(input, data = {}) {
899
+ const described = describeInput(input);
900
+ const level = toStoryLevel(data.level);
901
+ const note = {
902
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
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
+ }
916
+ return this;
917
+ }
918
+ /** Clear all accumulated notes without emitting a story, and start a new story id */
919
+ reset() {
920
+ this.notes = [];
921
+ this.startNewStory();
922
+ return this;
923
+ }
924
+ /** Preview the current notes as a formatted report without emitting or clearing them */
925
+ summarize(options = {}) {
926
+ const {
927
+ title = "Story preview",
928
+ level = "Information",
929
+ error,
930
+ ...reportOptions
931
+ } = options;
932
+ const event = {
933
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
934
+ level,
935
+ title,
936
+ storyId: this.storyId,
937
+ ...this.parentStoryId !== void 0 ? { parentStoryId: this.parentStoryId } : {},
938
+ ...this.origin ? { origin: this.origin } : {},
939
+ notes: [...this.notes],
940
+ ...error !== void 0 ? { error: normalizeError(error) } : {}
941
+ };
942
+ return formatStory(event, reportOptions);
943
+ }
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. */
962
+ tell(title) {
963
+ warnDeprecated("tell", "finish");
964
+ return this.createDelivery("Information", title);
965
+ }
966
+ /** @deprecated Use `finish(title, { level: "warn" })`. Removed at 1.0. */
967
+ warn(title) {
968
+ warnDeprecated("warn", 'finish(title, { level: "warn" })');
969
+ return this.createDelivery("Warning", title);
970
+ }
971
+ /** @deprecated Use `finish(title, { level: "oops", error })`. Removed at 1.0. */
972
+ 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 } : {});
993
+ }
994
+ /** Build a story event and schedule delivery, returning a handle to override the audience list */
995
+ createDelivery(level, title, error) {
996
+ const event = this.buildEvent(level, title, error);
997
+ let delivered = false;
998
+ let defaultCancelled = false;
999
+ queueMicrotask(() => {
1000
+ if (delivered || defaultCancelled) return;
1001
+ delivered = true;
1002
+ void this.deliver(event);
1003
+ });
1004
+ return {
1005
+ to: (...names) => {
1006
+ defaultCancelled = true;
1007
+ if (delivered) return;
1008
+ delivered = true;
1009
+ void this.deliver(event, { only: names });
1010
+ }
1011
+ };
1012
+ }
1013
+ /** Assemble the story event from current notes and start a fresh story */
1014
+ buildEvent(level, title, error) {
1015
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1016
+ const storyId = this.storyId;
1017
+ const droppedEmissions = this.droppedEmissions;
1018
+ const sortedNotes = [...this.notes].sort(
1019
+ (noteA, noteB) => Date.parse(noteA.timestamp) - Date.parse(noteB.timestamp) || (noteA.sequence ?? 0) - (noteB.sequence ?? 0)
1020
+ );
1021
+ this.notes = [];
1022
+ this.startNewStory();
1023
+ const durationMs = calculateNoteDuration2(sortedNotes).durationMs;
1024
+ const event = {
1025
+ timestamp: now,
1026
+ level,
1027
+ title,
1028
+ storyId,
1029
+ ...this.parentStoryId !== void 0 ? { parentStoryId: this.parentStoryId } : {},
1030
+ ...this.origin ? { origin: this.origin } : {},
1031
+ notes: sortedNotes,
1032
+ ...durationMs != null ? { durationMs } : {},
1033
+ ...droppedEmissions ? { droppedEmissions } : {},
1034
+ ...error !== void 0 ? { error: normalizeError(error) } : {}
1035
+ };
1036
+ const eventWithSummary = event;
1037
+ Object.defineProperty(eventWithSummary, "kind", {
1038
+ value: "story",
1039
+ enumerable: true
1040
+ });
1041
+ Object.defineProperty(eventWithSummary, "summarize", {
1042
+ value: (options) => formatStory(event, options),
1043
+ enumerable: false
1044
+ });
1045
+ return eventWithSummary;
1046
+ }
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;
1056
+ const targets = options?.only?.length ? this.audience.getOnly(options.only) : this.audience.getAll();
1057
+ await Promise.all(
1058
+ targets.filter((member) => hearsKind(member, emission.kind)).filter((member) => this.acceptsSafely(member, emission)).map((member) => this.hearSafely(member, emission))
1059
+ );
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
+ }
1100
+ };
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();
1139
+ }
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 };
1173
+ }
1174
+ }
1175
+ const typeName = normalized["@type"];
1176
+ return {
1177
+ text: typeof typeName === "string" ? typeName : "Object",
1178
+ what: normalized
1179
+ };
1180
+ }
1181
+ function calculateNoteDuration2(notes) {
1182
+ if (notes.length <= 1) {
1183
+ return {
1184
+ durationMs: void 0
1185
+ };
1186
+ }
1187
+ const startTime = Date.parse(notes[0].timestamp);
1188
+ const endTime = Date.parse(notes[notes.length - 1].timestamp);
1189
+ return {
1190
+ durationMs: Number.isFinite(startTime) && Number.isFinite(endTime) ? Math.max(0, endTime - startTime) : void 0
1191
+ };
1192
+ }
1193
+
380
1194
  // src/useStoryteller.ts
381
1195
  var sharedInstance;
382
1196
  function useStoryteller(options = {}) {
383
1197
  if (!sharedInstance || options.reset) {
384
- 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
+ });
385
1202
  return sharedInstance;
386
1203
  }
387
1204
  return sharedInstance;
@@ -391,9 +1208,11 @@ function useStoryteller(options = {}) {
391
1208
  function dbAudience(insert) {
392
1209
  return {
393
1210
  name: "db",
394
- accepts: (event) => event.level === "warn" || event.level === "oops",
395
- hear: async (event) => {
396
- 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);
397
1216
  }
398
1217
  };
399
1218
  }
@@ -403,10 +1222,10 @@ function writeStoryReport(stories, options = {}) {
403
1222
  const {
404
1223
  timezone = Intl.DateTimeFormat().resolvedOptions().timeZone,
405
1224
  locale = "en-US",
406
- verbosity = "normal",
407
- maxNotesPerStory = 50,
1225
+ detail = "normal",
1226
+ noteLimit = 50,
408
1227
  showData = true,
409
- colorize = true
1228
+ colors = true
410
1229
  } = options;
411
1230
  if (!stories.length) {
412
1231
  return "Storyteller Report\n\n(no stories)\n";
@@ -443,17 +1262,17 @@ function writeStoryReport(stories, options = {}) {
443
1262
  for (const [day, dayStories] of storiesByDay) {
444
1263
  lines.push(day);
445
1264
  for (const story of dayStories) {
446
- const summary = summarizeStory(story, {
1265
+ const report = formatStory(story, {
447
1266
  timezone,
448
1267
  locale,
449
- verbosity,
450
- maxNotes: maxNotesPerStory,
451
- colorize
1268
+ detail,
1269
+ noteLimit,
1270
+ colors
452
1271
  });
453
- const { data } = summary;
1272
+ const { data } = report;
454
1273
  const originLabel = formatOrigin(story.origin);
455
1274
  const levelColor = getLevelColor(story.level);
456
- const label = (text) => colorize ? `${levelColor}${text}${ANSI.reset}` : text;
1275
+ const label = (text) => colors ? `${levelColor}${text}${ANSI.reset}` : text;
457
1276
  const duration = data.duration ? ` (${data.duration})` : "";
458
1277
  lines.push(`${label("Story")}: ${story.title}`);
459
1278
  lines.push(`${label("Level")}: ${story.level}`);
@@ -468,10 +1287,10 @@ function writeStoryReport(stories, options = {}) {
468
1287
  ].filter(Boolean).join(": ");
469
1288
  if (errorLine) lines.push(`${label("Error")}: ${errorLine}`);
470
1289
  }
471
- if (verbosity !== "brief" && data.notes.length) {
1290
+ if (detail !== "brief" && data.notes.length) {
472
1291
  lines.push(` ${label("Notes")}:`);
473
- for (const summaryNote of data.notes) {
474
- lines.push(` ${summaryNote.when} \u2014 ${summaryNote.text}`);
1292
+ for (const reportNote of data.notes) {
1293
+ lines.push(` ${reportNote.when} \u2014 ${reportNote.text}`);
475
1294
  }
476
1295
  if (story.notes.length > data.notes.length) {
477
1296
  lines.push(
@@ -482,7 +1301,7 @@ function writeStoryReport(stories, options = {}) {
482
1301
  if (showData) {
483
1302
  lines.push(`${label("Data")}:`);
484
1303
  const json = JSON.stringify(data, null, 2);
485
- if (colorize) {
1304
+ if (colors) {
486
1305
  const colored = colorizeJsonSections(json, {
487
1306
  base: ANSI.grayLight,
488
1307
  notes: ANSI.grayDark,
@@ -501,14 +1320,27 @@ function writeStoryReport(stories, options = {}) {
501
1320
  // Annotate the CommonJS export names for ESM import in node:
502
1321
  0 && (module.exports = {
503
1322
  ANSI,
1323
+ AudienceRegistry,
1324
+ DEFAULT_REDACT_KEYS,
1325
+ REDACTED,
504
1326
  Storyteller,
505
- colorizeJsonSections,
506
1327
  consoleAudience,
507
- countBrackets,
508
1328
  dbAudience,
1329
+ formatDuration,
509
1330
  formatOrigin,
1331
+ formatStory,
510
1332
  getLevelColor,
1333
+ meetsLevel,
1334
+ ndjsonAudience,
1335
+ normalizeError,
1336
+ normalizeValue,
1337
+ readEnvironmentValue,
1338
+ resolveColors,
1339
+ resolveMinimumLevel,
1340
+ resolveOutputFormat,
1341
+ summarizeContext,
511
1342
  summarizeStory,
1343
+ toStoryLevel,
512
1344
  useStoryteller,
513
1345
  writeStoryReport
514
1346
  });