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