@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.d.cts CHANGED
@@ -1,14 +1,158 @@
1
- type StoryLevel = "tell" | "warn" | "oops";
2
- type StoryContextValue = Record<string, unknown> | string;
1
+ /**
2
+ * Which default audience a storyteller registers.
3
+ *
4
+ * - `text` — colorized console output for a person watching
5
+ * - `ndjson` — one JSON object per line for a program reading
6
+ */
7
+ type OutputFormat = "text" | "ndjson";
8
+ /**
9
+ * A level written any of the ways people and agents actually write it.
10
+ * `report("...", { level: "warn" })` should not be a type error.
11
+ */
12
+ type LevelInput = StoryLevel | "info" | "information" | "warn" | "warning" | "oops" | "error";
13
+ /**
14
+ * Resolve any accepted level spelling to a stored level label.
15
+ *
16
+ * @param input - A level in any accepted spelling
17
+ * @returns The canonical StoryLevel, defaulting to Information
18
+ */
19
+ declare function toStoryLevel(input?: LevelInput): StoryLevel;
20
+ /**
21
+ * Read an environment variable, tolerating runtimes that have no environment at all.
22
+ *
23
+ * @param name - Variable name
24
+ * @returns The trimmed value, or undefined when unset or unavailable
25
+ */
26
+ declare function readEnvironmentValue(name: string): string | undefined;
27
+ /**
28
+ * Resolve the minimum level to deliver, from an explicit option then `STORYTELLER_LEVEL`.
29
+ *
30
+ * @param requested - Explicit level, if the caller set one
31
+ * @returns The threshold level, defaulting to Information (deliver everything)
32
+ */
33
+ declare function resolveMinimumLevel(requested?: StoryLevel | string): StoryLevel;
34
+ /**
35
+ * Check whether a level clears the configured minimum.
36
+ *
37
+ * @param level - The emission's level
38
+ * @param minimum - The configured threshold
39
+ */
40
+ declare function meetsLevel(level: StoryLevel, minimum: StoryLevel): boolean;
41
+ /**
42
+ * Resolve which default audience to register, from an explicit option then
43
+ * `STORYTELLER_FORMAT`.
44
+ *
45
+ * Deliberately not inferred from whether stdout is a TTY: output that silently
46
+ * changes shape when a process is piped is a debugging afternoon nobody asked for.
47
+ *
48
+ * @param requested - Explicit format, if the caller set one
49
+ * @returns The format, defaulting to text
50
+ */
51
+ declare function resolveOutputFormat(requested?: OutputFormat): OutputFormat;
52
+ /**
53
+ * Resolve whether to colorize, from an explicit option then `STORYTELLER_COLOR`.
54
+ *
55
+ * @param requested - Explicit choice, if the caller set one
56
+ * @returns Whether colors should be used, defaulting to true
57
+ */
58
+ declare function resolveColors(requested?: boolean): boolean;
59
+
60
+ /** A value that survives JSON.stringify with no loss and no throwing */
61
+ type JsonValue = string | number | boolean | null | JsonValue[] | {
62
+ [key: string]: JsonValue;
63
+ };
64
+ type NormalizeOptions = {
65
+ /** How many levels deep to descend before replacing the value with a truncation marker */
66
+ maxDepth?: number;
67
+ /** How many array entries to keep before truncating */
68
+ maxArrayLength?: number;
69
+ /** How many object properties to keep before truncating */
70
+ maxProperties?: number;
71
+ /** How many characters of a string to keep before truncating */
72
+ maxStringLength?: number;
73
+ /** Property names whose values are replaced with the redaction marker */
74
+ redactKeys?: string[];
75
+ /** Set false to keep secret-shaped values as-is */
76
+ redact?: boolean;
77
+ };
78
+ /** Marker written in place of a value that matched a redacted key name */
79
+ declare const REDACTED = "[redacted]";
80
+ /**
81
+ * Property names whose values are replaced with {@link REDACTED}.
82
+ * Matching ignores case and separators, so `apiKey`, `api_key` and `API-KEY` all match.
83
+ */
84
+ declare const DEFAULT_REDACT_KEYS: string[];
85
+ /**
86
+ * Convert any value into a JSON-safe structure suitable for a story record.
87
+ *
88
+ * Handles the shapes real code actually holds — errors, dates, maps, sets, class
89
+ * instances, binary buffers, circular references, throwing getters — and never throws,
90
+ * so a hostile object logged by a caller cannot break the delivery pipeline.
91
+ *
92
+ * Data dropped for size is replaced with an explicit `@truncated` marker rather than
93
+ * disappearing silently, so a consumer can tell the difference between "this was empty"
94
+ * and "this was too big".
95
+ *
96
+ * @param input - Any value
97
+ * @param options - Depth, size and redaction limits
98
+ * @returns A value that JSON.stringify can always serialize
99
+ *
100
+ * @example
101
+ * ```ts
102
+ * normalizeValue({ user: new Map([["id", 1]]), apiKey: "sk-live-abc" });
103
+ * // { user: { "@type": "Map", entries: { id: 1 } }, apiKey: "[redacted]" }
104
+ * ```
105
+ */
106
+ declare function normalizeValue(input: unknown, options?: NormalizeOptions): JsonValue;
107
+ /**
108
+ * Convert an unknown thrown value into a serializable StoryError,
109
+ * following the `cause` chain and collecting AggregateError members.
110
+ *
111
+ * @param rawError - Any thrown or rejected value
112
+ * @param options - Depth, size and redaction limits applied to attached data
113
+ * @returns A StoryError safe to store and serialize
114
+ */
115
+ declare function normalizeError(rawError: unknown, options?: NormalizeOptions): StoryError;
116
+
117
+ /** Human-readable level labels stored in story records */
118
+ type StoryLevel = "Information" | "Warning" | "Error";
119
+ /**
120
+ * A stored context value. Always JSON-safe — whatever the caller passed in has
121
+ * already been through the normalizer by the time it reaches a record.
122
+ */
123
+ type StoryContextValue = JsonValue;
124
+ /** Context accepted from callers. Anything goes; the normalizer makes it storable. */
125
+ type StoryContextInput = unknown;
3
126
  type StoryError = {
4
127
  name?: string;
5
128
  message?: string;
6
129
  stack?: string;
7
- cause?: unknown;
130
+ cause?: JsonValue;
131
+ /** Members of an AggregateError */
132
+ errors?: StoryError[];
133
+ };
134
+ /** Origin as stored on a record */
135
+ type StoryOrigin = {
136
+ who?: StoryContextValue;
137
+ what?: StoryContextValue;
138
+ where?: StoryContextValue;
139
+ };
140
+ /** Origin as accepted from callers */
141
+ type StoryOriginInput = {
142
+ who?: StoryContextInput;
143
+ what?: StoryContextInput;
144
+ where?: StoryContextInput;
8
145
  };
9
146
  type StoryNote = {
10
147
  timestamp: string;
148
+ /**
149
+ * Position within the story, assigned when the note is taken. Gap-free from 0.
150
+ * Optional so records written before sequencing existed still typecheck.
151
+ */
152
+ sequence?: number;
11
153
  note: string;
154
+ /** Omitted when the note carries the story's default Information level */
155
+ level?: StoryLevel;
12
156
  who?: StoryContextValue;
13
157
  what?: StoryContextValue;
14
158
  where?: StoryContextValue;
@@ -18,28 +162,45 @@ type StoryEventBase = {
18
162
  timestamp: string;
19
163
  level: StoryLevel;
20
164
  title: string;
21
- origin?: {
22
- who?: StoryContextValue;
23
- what?: StoryContextValue;
24
- where?: StoryContextValue;
25
- };
165
+ /**
166
+ * Correlates every note emission with the story it belongs to. Always set on
167
+ * events this library builds; optional so older stored records still typecheck.
168
+ */
169
+ storyId?: string;
170
+ /**
171
+ * The story this one is a chapter of. Absent on a top-level story.
172
+ * Following this field reconstructs the tree of a nested run.
173
+ */
174
+ parentStoryId?: string;
175
+ origin?: StoryOrigin;
26
176
  notes: StoryNote[];
177
+ durationMs?: number;
178
+ /**
179
+ * How many emissions were dropped for back-pressure while this story was being
180
+ * collected. Present only when something was actually lost, so the loss shows up
181
+ * in the record instead of vanishing.
182
+ */
183
+ droppedEmissions?: number;
27
184
  error?: StoryError;
28
185
  };
29
- type StorySummaryOptions = {
186
+ type ReportOptions = {
30
187
  timezone?: string;
31
188
  locale?: string;
32
- verbosity?: "brief" | "normal" | "full";
33
- maxNotes?: number;
189
+ detail?: "brief" | "normal" | "full";
190
+ noteLimit?: number;
34
191
  showData?: boolean;
35
- colorize?: boolean;
192
+ colors?: boolean;
36
193
  };
37
- type StoryPreviewOptions = StorySummaryOptions & {
194
+ /** @deprecated Use ReportOptions instead */
195
+ type StorySummaryOptions = ReportOptions;
196
+ type PreviewOptions = ReportOptions & {
38
197
  title?: string;
39
198
  level?: StoryLevel;
40
199
  error?: unknown;
41
200
  };
42
- type StorySummaryNote = {
201
+ /** @deprecated Use PreviewOptions instead */
202
+ type StoryPreviewOptions = PreviewOptions;
203
+ type ReportNote = {
43
204
  timestamp: string;
44
205
  when: string;
45
206
  note: string;
@@ -49,34 +210,146 @@ type StorySummaryNote = {
49
210
  where?: StoryContextValue;
50
211
  error?: StoryError;
51
212
  };
52
- type StorySummaryData = {
213
+ /** @deprecated Use ReportNote instead */
214
+ type StorySummaryNote = ReportNote;
215
+ type StoryReport = {
53
216
  title: string;
54
217
  level: StoryLevel;
55
218
  when: string;
56
219
  durationMs?: number;
57
220
  duration?: string;
58
221
  origin?: StoryEventBase["origin"];
59
- notes: StorySummaryNote[];
222
+ notes: ReportNote[];
60
223
  error?: StoryError;
61
224
  };
62
- type StorySummary = {
225
+ /** @deprecated Use StoryReport instead */
226
+ type StorySummaryData = StoryReport;
227
+ type FormattedReport = {
63
228
  text: string;
64
- data: StorySummaryData;
229
+ data: StoryReport;
65
230
  };
231
+ /** @deprecated Use FormattedReport instead */
232
+ type StorySummary = FormattedReport;
66
233
  type StoryEvent = StoryEventBase & {
67
- summarize: (options?: StorySummaryOptions) => StorySummary;
234
+ kind: "story";
235
+ summarize: (options?: ReportOptions) => FormattedReport;
236
+ };
237
+ /** @deprecated Use StoryEvent — the story-shaped emission */
238
+ type StoryEmission = StoryEvent;
239
+ /** The two things an audience can hear */
240
+ type EmissionKind = "note" | "story";
241
+ /**
242
+ * A single beat, delivered the moment it happens when narration is live.
243
+ *
244
+ * `storyId` and `sequence` are what make streaming lossless: a consumer holding
245
+ * the beats of a story can order and group them back into the record that
246
+ * collected narration would have produced.
247
+ */
248
+ type NoteEmission = StoryNote & {
249
+ kind: "note";
250
+ storyId: string;
251
+ parentStoryId?: string;
252
+ sequence: number;
253
+ level: StoryLevel;
254
+ origin?: StoryOrigin;
68
255
  };
256
+ type Emission = NoteEmission | StoryEvent;
69
257
  type AudienceMember = {
70
258
  name: string;
71
- accepts?: (event: StoryEvent) => boolean;
72
- hear: (event: StoryEvent) => void | Promise<void>;
259
+ /**
260
+ * Which emission kinds this audience wants. Defaults to `["story"]`, so an
261
+ * audience written before live narration existed keeps hearing only stories.
262
+ */
263
+ hears?: EmissionKind[];
264
+ /**
265
+ * Declared with method syntax deliberately. TypeScript checks method parameters
266
+ * bivariantly, so an audience written as `hear: (event: StoryEvent) => void`
267
+ * still compiles. That would be unsound if such an audience could receive a note
268
+ * emission — it cannot, because `hears` defaults to stories only.
269
+ */
270
+ accepts?(emission: Emission): boolean;
271
+ hear(emission: Emission): void | Promise<void>;
73
272
  };
273
+ /**
274
+ * How a storyteller narrates.
275
+ *
276
+ * - `collected` — beats are buffered and leave as one story record (the default)
277
+ * - `live` — each beat is emitted as it happens, and the story still lands at the end
278
+ *
279
+ * Live narration adds emissions, it never removes them: a consumer that only wants
280
+ * beats says so with `hears: ["note"]` rather than by silencing the record.
281
+ */
282
+ type Narration = "collected" | "live";
283
+ /** @deprecated `both` is now the behavior of `live` — beats stream and the story still lands */
284
+ type NarrationInput = Narration | "both";
74
285
  type NoteData = {
75
- who?: StoryContextValue;
76
- what?: StoryContextValue;
77
- where?: StoryContextValue;
286
+ who?: StoryContextInput;
287
+ what?: StoryContextInput;
288
+ where?: StoryContextInput;
78
289
  error?: unknown;
290
+ /** Level for this beat alone. Defaults to Information. */
291
+ level?: LevelInput;
292
+ /** Emit this beat immediately even when narration is collected */
293
+ live?: boolean;
294
+ /** Deliver this beat only to the named audiences */
295
+ to?: string[];
79
296
  };
297
+ type FinishOptions = {
298
+ /** Defaults to Information */
299
+ level?: LevelInput;
300
+ /** The error that ended the story, normalized onto the record */
301
+ error?: unknown;
302
+ };
303
+ type ChapterOptions = {
304
+ /** Merged over the parent's origin */
305
+ origin?: StoryOriginInput;
306
+ /** Defaults to the parent's setting */
307
+ narration?: NarrationInput;
308
+ /** Defaults to the parent's setting */
309
+ level?: LevelInput;
310
+ /** Defaults to the parent's handler */
311
+ onAudienceError?: AudienceErrorHandler;
312
+ /** Defaults to the parent's bound */
313
+ maxInFlight?: number;
314
+ };
315
+ type StorytellerOptions = {
316
+ origin?: StoryOriginInput;
317
+ audiences?: AudienceMember[];
318
+ /** Defaults to `STORYTELLER_NARRATION`, then `collected` */
319
+ narration?: NarrationInput;
320
+ /**
321
+ * Which default audience to register: colorized text for a person, NDJSON for a
322
+ * program. Defaults to `STORYTELLER_FORMAT`, then `text`.
323
+ */
324
+ format?: OutputFormat;
325
+ /**
326
+ * Share another storyteller's audience registry instead of creating one.
327
+ * Audiences added to it later reach this storyteller too. When given, no
328
+ * default audience is registered — the registry already has whatever it has.
329
+ */
330
+ audience?: AudienceRegistry;
331
+ /** The story this one is a chapter of. Set by `chapter()`. */
332
+ parentStoryId?: string;
333
+ /**
334
+ * Drop emissions below this level before they reach any audience.
335
+ * Defaults to `STORYTELLER_LEVEL`, then Information (deliver everything).
336
+ */
337
+ level?: LevelInput;
338
+ /**
339
+ * Called when an audience throws or rejects. Without one, a single throttled
340
+ * warning per audience goes to the console — a logging library that loses
341
+ * records in silence is worse than one that complains.
342
+ */
343
+ onAudienceError?: AudienceErrorHandler;
344
+ /**
345
+ * Cap on deliveries in flight to a single audience at once. Live narration is
346
+ * fire-and-forget, so a slow audience would otherwise grow an unbounded queue.
347
+ * Past the cap, emissions are dropped and counted on the closing story.
348
+ */
349
+ maxInFlight?: number;
350
+ };
351
+ /** Called when an audience member throws or rejects while hearing an emission */
352
+ type AudienceErrorHandler = (error: unknown, member: AudienceMember, emission: Emission) => void;
80
353
  /** Manages the set of audience members that receive story events */
81
354
  declare class AudienceRegistry {
82
355
  private members;
@@ -88,64 +361,277 @@ declare class AudienceRegistry {
88
361
  getAll(): AudienceMember[];
89
362
  /** Return only the audience members matching the given names */
90
363
  getOnly(names: string[]): AudienceMember[];
364
+ /** Check if an audience member is registered by name */
365
+ has(name: string): boolean;
366
+ /** List the names of all registered audience members */
367
+ names(): string[];
91
368
  }
92
- /** Core logging class that collects timestamped notes and emits them as structured story events */
369
+ /**
370
+ * Collects timestamped notes and emits them as one structured story — and, when
371
+ * narration is live, emits each note the moment it is taken.
372
+ *
373
+ * @example
374
+ * ```ts
375
+ * const story = new Storyteller({ origin: { who: "api-server" }, narration: "live" });
376
+ * story.report("Request received", { what: { path: "/checkout" } });
377
+ * story.report("Validated cart");
378
+ * story.finish("Checkout started");
379
+ * ```
380
+ */
93
381
  declare class Storyteller {
94
382
  readonly audience: AudienceRegistry;
95
383
  private readonly origin?;
384
+ private readonly parentStoryId?;
96
385
  private notes;
97
- constructor(options?: {
98
- origin?: StoryEventBase["origin"];
99
- audiences?: AudienceMember[];
100
- });
101
- /** Add a timestamped note with optional context (who, what, where, error) */
102
- note(text: string, data?: NoteData): this;
103
- /** Clear all accumulated notes without emitting a story */
386
+ private narration;
387
+ private readonly minimumLevel;
388
+ private readonly onAudienceError;
389
+ private readonly maxInFlight;
390
+ /** Deliveries currently awaiting each audience, keyed by audience name */
391
+ private readonly inFlight;
392
+ /** Emissions dropped for back-pressure since the current story began */
393
+ private droppedEmissions;
394
+ /** Identifies the story currently being collected; regenerated after each telling */
395
+ private storyId;
396
+ /** Position of the next note within the current story */
397
+ private nextSequence;
398
+ constructor(options?: StorytellerOptions);
399
+ /**
400
+ * Switch between collected and live narration at runtime.
401
+ * Takes effect on the next note; already-buffered notes are not replayed.
402
+ *
403
+ * @param narration - `collected` to buffer, `live` to emit each note as it happens
404
+ * @returns `this` for chaining
405
+ */
406
+ narrate(narration: NarrationInput): this;
407
+ /** The id of the story currently being collected */
408
+ get currentStoryId(): string;
409
+ /**
410
+ * Start a chapter: a child storyteller whose stories are linked back to this
411
+ * one by `parentStoryId`.
412
+ *
413
+ * Real work nests — an agent spawns subtasks, a batch runs per-item operations.
414
+ * A chapter keeps each of those a complete story in its own right while leaving
415
+ * the run reconstructable as a tree.
416
+ *
417
+ * The child shares this storyteller's audience registry, so audiences added
418
+ * later reach it too, and inherits narration, level and delivery settings.
419
+ * Its stories are separate records — a chapter is not folded into the parent's
420
+ * notes.
421
+ *
422
+ * @param options - Origin to merge over the parent's, and any setting to override
423
+ * @returns A child Storyteller
424
+ *
425
+ * @example
426
+ * ```ts
427
+ * for (const account of accounts) {
428
+ * const chapter = story.chapter({ origin: { what: account.id } });
429
+ * chapter.report("Fetching invoices");
430
+ * chapter.finish(`Synced ${account.id}`);
431
+ * }
432
+ * ```
433
+ */
434
+ chapter(options?: ChapterOptions): Storyteller;
435
+ /**
436
+ * Report a beat of the current story.
437
+ *
438
+ * In collected narration the beat is buffered and leaves with the story. In live
439
+ * narration it is emitted the moment you call this, so whoever is tuned in sees
440
+ * the work as it happens.
441
+ *
442
+ * Accepts anything, not just a string — pass an error, an API response, a Map, a
443
+ * class instance — and the value is normalized into a storable shape with the note
444
+ * text derived from it.
445
+ *
446
+ * @param input - What happened: a message, or any value to describe
447
+ * @param data - Optional context: who did it, what was involved, where it happened, any error
448
+ * @returns `this` for chaining
449
+ *
450
+ * @example
451
+ * ```ts
452
+ * story.report("Card charged", { what: { amount: "$42" }, where: "stripe" });
453
+ * story.report(await response.json());
454
+ * ```
455
+ */
456
+ report(input: unknown, data?: NoteData): this;
457
+ /** Clear all accumulated notes without emitting a story, and start a new story id */
104
458
  reset(): this;
105
- /** Generate a formatted summary of current notes without emitting or clearing them */
106
- summarize(options?: StoryPreviewOptions): StorySummary;
107
- /** Emit a story at the "tell" level (success / informational) */
459
+ /** Preview the current notes as a formatted report without emitting or clearing them */
460
+ summarize(options?: PreviewOptions): FormattedReport;
461
+ /**
462
+ * Finish the story: emit everything collected so far as one record, and start fresh.
463
+ *
464
+ * @param title - What the story was about
465
+ * @param options - Level, and the error that ended it
466
+ * @returns A one-shot handle whose `.to()` overrides the audience list — call it
467
+ * synchronously, delivery happens on the next microtask
468
+ *
469
+ * @example
470
+ * ```ts
471
+ * story.finish("Sync complete");
472
+ * story.finish("Sync failed", { level: "oops", error }).to("db");
473
+ * ```
474
+ */
475
+ finish(title: string, options?: FinishOptions): {
476
+ to: (...names: string[]) => void;
477
+ };
478
+ /** @deprecated Use `finish(title)`. Removed at 1.0. */
108
479
  tell(title: string): {
109
480
  to: (...names: string[]) => void;
110
481
  };
111
- /** Emit a story at the "warn" level (something was off) */
482
+ /** @deprecated Use `finish(title, { level: "warn" })`. Removed at 1.0. */
112
483
  warn(title: string): {
113
484
  to: (...names: string[]) => void;
114
485
  };
115
- /** Emit a story at the "oops" level (something broke) with an optional error */
486
+ /** @deprecated Use `finish(title, { level: "oops", error })`. Removed at 1.0. */
116
487
  oops(title: string, error?: unknown): {
117
488
  to: (...names: string[]) => void;
118
489
  };
490
+ /** @deprecated Use `report()`. Removed at 1.0. */
491
+ note(input: unknown, data?: NoteData): this;
492
+ /** Emit a single note to the audiences listening for notes */
493
+ private emitNote;
119
494
  /** Build a story event and schedule delivery, returning a handle to override the audience list */
120
495
  private createDelivery;
121
- /** Assemble the story event from current notes and clear notes for the next story */
496
+ /** Assemble the story event from current notes and start a fresh story */
122
497
  private buildEvent;
123
- /** Deliver a story event to matching audience members */
498
+ /** Begin a new story: fresh id, sequence back to zero */
499
+ private startNewStory;
500
+ /** Deliver an emission to the audience members listening for its kind */
124
501
  private deliver;
502
+ /** Run an audience's accepts() without letting a throw from it lose the emission */
503
+ private acceptsSafely;
504
+ /**
505
+ * Hand an emission to one audience, keeping its failures and its slowness
506
+ * contained: a throw is reported rather than swallowed, and a backlog is dropped
507
+ * rather than grown without limit.
508
+ */
509
+ private hearSafely;
510
+ /** Report an audience failure without ever letting it reach caller code */
511
+ private handleAudienceError;
125
512
  }
126
- /** Generate a formatted, human-readable summary from a story event */
127
- declare function summarizeStory(story: StoryEventBase, options?: StorySummaryOptions): StorySummary;
513
+
514
+ /**
515
+ * Format a story event into a human-readable report with optional colors.
516
+ *
517
+ * @param story - The story event to format
518
+ * @param options - Formatting options (timezone, locale, detail level, colors)
519
+ * @returns A FormattedReport with both text (for display) and data (structured)
520
+ *
521
+ * @example
522
+ * ```ts
523
+ * const report = formatStory(event, { colors: false, detail: "brief" });
524
+ * console.log(report.text);
525
+ * ```
526
+ */
527
+ declare function formatStory(story: StoryEventBase, options?: ReportOptions): FormattedReport;
528
+ /** Convert milliseconds into a human-readable duration string */
529
+ declare function formatDuration(milliseconds: number): string;
530
+ /** @deprecated Use formatStory instead */
531
+ declare const summarizeStory: typeof formatStory;
128
532
 
129
533
  type StorytellerSharedOptions = {
130
- origin?: StoryEventBase["origin"];
534
+ origin?: StoryOriginInput;
535
+ narration?: NarrationInput;
131
536
  reset?: boolean;
132
537
  };
133
- /** Return a shared singleton Storyteller instance for cross-component or cross-service logging */
538
+ /**
539
+ * Get or create a shared Storyteller instance for cross-component logging.
540
+ * First call creates the instance; subsequent calls return the same one.
541
+ *
542
+ * @param options.origin - Origin context for the shared instance
543
+ * @param options.reset - Create a fresh instance (useful in tests)
544
+ *
545
+ * @example
546
+ * ```ts
547
+ * // Same instance everywhere in your app
548
+ * const story = useStoryteller({ origin: { who: "worker" } });
549
+ * ```
550
+ */
134
551
  declare function useStoryteller(options?: StorytellerSharedOptions): Storyteller;
135
552
 
136
- /** Create an audience that logs stories to the browser console with color-coded grouped output */
137
- declare function consoleAudience(): AudienceMember;
553
+ type ConsoleAudienceOptions = {
554
+ /** Set false to strip ANSI colors from live note lines. Defaults to `STORYTELLER_COLOR`. */
555
+ colors?: boolean;
556
+ };
557
+ /**
558
+ * Create an audience that prints to the console: one compact line per note when
559
+ * narration is live, and a color-coded grouped record when a story is told.
560
+ *
561
+ * Registered by default on every Storyteller instance. It listens for notes as well
562
+ * as stories, so switching a storyteller to live narration shows something immediately
563
+ * without registering anything extra.
564
+ *
565
+ * @param options - Rendering options for live note lines
566
+ *
567
+ * @example
568
+ * ```ts
569
+ * // Already included — but you can re-add after removing:
570
+ * story.audience.add(consoleAudience());
571
+ * ```
572
+ */
573
+ declare function consoleAudience(options?: ConsoleAudienceOptions): AudienceMember;
138
574
 
139
- /** Create an audience that persists warn and oops stories to a database via the provided insert function */
575
+ /**
576
+ * Create an audience that stores warn and oops stories via your insert function.
577
+ * Tell-level events are filtered out to reduce noise — only warnings and errors are persisted.
578
+ *
579
+ * Hears stories only. Live notes are not persisted: the story record already contains
580
+ * every note, so storing both would double-write the same content.
581
+ *
582
+ * Note: if the insert function throws, the error is silently caught by the delivery
583
+ * pipeline (Promise.allSettled). Wrap your insert with try/catch to handle failures.
584
+ *
585
+ * @param insert - Function that receives the story event and stores it
586
+ *
587
+ * @example
588
+ * ```ts
589
+ * story.audience.add(
590
+ * dbAudience(async (event) => {
591
+ * await db.insert("story_logs", event);
592
+ * })
593
+ * );
594
+ * ```
595
+ */
140
596
  declare function dbAudience(insert: (event: StoryEvent) => Promise<void> | void): AudienceMember;
141
597
 
598
+ /** Anything that can take a line of text — a Node stream, or your own sink */
599
+ type LineWriter = {
600
+ write: (chunk: string) => unknown;
601
+ };
602
+ type NdjsonAudienceOptions = {
603
+ /** Where lines go. Defaults to stdout in Node, console.log elsewhere. */
604
+ stream?: LineWriter;
605
+ /** Register under a different name, e.g. to run two streams at once */
606
+ name?: string;
607
+ /** Minimum level to write. Defaults to `STORYTELLER_LEVEL`, then everything. */
608
+ level?: StoryLevel;
609
+ };
610
+ /**
611
+ * Create an audience that writes one JSON object per line — every note and every
612
+ * story, nothing else on the channel.
613
+ *
614
+ * This is the format to give a program: a log shipper, `jq`, or an agent reading
615
+ * another process's output. Each line parses on its own, and `storyId` plus
616
+ * `sequence` let a reader group streamed notes back into their story.
617
+ *
618
+ * @param options - Stream, name and level threshold
619
+ *
620
+ * @example
621
+ * ```ts
622
+ * story.audience.remove("console");
623
+ * story.audience.add(ndjsonAudience({ stream: process.stderr }));
624
+ * ```
625
+ */
626
+ declare function ndjsonAudience(options?: NdjsonAudienceOptions): AudienceMember;
627
+
142
628
  type StoryReportOptions = {
143
629
  timezone?: string;
144
630
  locale?: string;
145
- verbosity?: "brief" | "normal" | "full";
146
- maxNotesPerStory?: number;
631
+ detail?: "brief" | "normal" | "full";
632
+ noteLimit?: number;
147
633
  showData?: boolean;
148
- colorize?: boolean;
634
+ colors?: boolean;
149
635
  };
150
636
  /** Generate a formatted report from an array of story events, grouped by day */
151
637
  declare function writeStoryReport(stories: StoryEventBase[], options?: StoryReportOptions): string;
@@ -162,14 +648,20 @@ declare const ANSI: {
162
648
  /** Map a story level to its corresponding ANSI terminal color */
163
649
  declare function getLevelColor(level: StoryLevel): string;
164
650
  /** Format an origin context into a human-readable path like "app / page / component" */
165
- declare function formatOrigin(origin?: StoryEventBase["origin"]): string | undefined;
166
- /** Colorize JSON output, dimming the notes section for visual hierarchy */
167
- declare function colorizeJsonSections(json: string, colors: {
168
- base: string;
169
- notes: string;
170
- reset: string;
171
- }): string[];
172
- /** Count the net bracket depth change in a line (opening brackets minus closing brackets) */
173
- declare function countBrackets(line: string): number;
651
+ declare function formatOrigin(origin?: StoryOrigin): string | undefined;
652
+ /**
653
+ * Condense a note's context into a short inline summary for one-line output.
654
+ * The full values are always available on the story record and the NDJSON stream,
655
+ * so this can afford to be lossy in favor of staying readable.
656
+ *
657
+ * @param note - The note's context fields
658
+ * @returns A brace-wrapped summary, or undefined when there is no context
659
+ */
660
+ declare function summarizeContext(note: {
661
+ note?: string;
662
+ what?: JsonValue;
663
+ where?: JsonValue;
664
+ error?: StoryError;
665
+ }): string | undefined;
174
666
 
175
- export { ANSI, type AudienceMember, type StoryContextValue, type StoryError, type StoryEvent, type StoryEventBase, type StoryLevel, type StoryNote, type StoryPreviewOptions, type StoryReportOptions, type StorySummary, type StorySummaryData, type StorySummaryNote, type StorySummaryOptions, Storyteller, colorizeJsonSections, consoleAudience, countBrackets, dbAudience, formatOrigin, getLevelColor, summarizeStory, useStoryteller, writeStoryReport };
667
+ export { ANSI, type AudienceErrorHandler, type AudienceMember, AudienceRegistry, type ChapterOptions, type ConsoleAudienceOptions, DEFAULT_REDACT_KEYS, type Emission, type EmissionKind, type FinishOptions, type FormattedReport, type JsonValue, type LevelInput, type LineWriter, type Narration, type NarrationInput, type NdjsonAudienceOptions, type NormalizeOptions, type NoteData, type NoteEmission, type OutputFormat, type PreviewOptions, REDACTED, type ReportNote, type ReportOptions, type StoryContextInput, type StoryContextValue, type StoryEmission, type StoryError, type StoryEvent, type StoryEventBase, type StoryLevel, type StoryNote, type StoryOrigin, type StoryOriginInput, type StoryPreviewOptions, type StoryReport, type StoryReportOptions, type StorySummary, type StorySummaryData, type StorySummaryNote, type StorySummaryOptions, Storyteller, type StorytellerOptions, consoleAudience, dbAudience, formatDuration, formatOrigin, formatStory, getLevelColor, meetsLevel, ndjsonAudience, normalizeError, normalizeValue, readEnvironmentValue, resolveColors, resolveMinimumLevel, resolveOutputFormat, summarizeContext, summarizeStory, toStoryLevel, useStoryteller, writeStoryReport };