@lovelaces-io/storyteller 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Lovelaces
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,140 @@
1
+ # Storyteller
2
+
3
+ Lightweight TypeScript logging library that treats logs as **stories** — grouped notes emitted as a single structured event.
4
+
5
+ Zero dependencies. ~24 kB packed. TypeScript-first.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ npm install @lovelaces-io/storyteller
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```ts
16
+ import { Storyteller } from "@lovelaces-io/storyteller";
17
+
18
+ const story = new Storyteller({
19
+ origin: { where: { app: "checkout", page: "Payment" } },
20
+ });
21
+
22
+ // Collect notes as things happen
23
+ story.note("User submitted payment", {
24
+ who: { id: "user:413" },
25
+ what: { amount: 49.99, currency: "USD" },
26
+ });
27
+
28
+ story.note("Charging card", {
29
+ what: "stripe:charge",
30
+ where: { service: "payments" },
31
+ });
32
+
33
+ // Tell the story when it's done
34
+ story.tell("Payment completed");
35
+ ```
36
+
37
+ Notes are bundled into one structured event, delivered to your audiences, and cleared for the next story.
38
+
39
+ ## Three Levels
40
+
41
+ ```ts
42
+ story.tell("Payment completed"); // success
43
+ story.warn("Payment slow but succeeded"); // something was off
44
+ story.oops("Payment failed", new Error()); // something broke
45
+ ```
46
+
47
+ ## Context on Every Note
48
+
49
+ Every note can carry `who`, `what`, `where`, and `error`:
50
+
51
+ ```ts
52
+ story.note("Write failed", {
53
+ who: { id: "user:99" },
54
+ what: { field: "email" },
55
+ where: "primary-db",
56
+ error: new Error("db timeout"),
57
+ });
58
+ ```
59
+
60
+ ## Audiences
61
+
62
+ Stories are delivered to **audiences**. Console is included by default. Add your own:
63
+
64
+ ```ts
65
+ import { dbAudience } from "@lovelaces-io/storyteller";
66
+
67
+ // Persist warn and oops events to your database
68
+ story.audience.add(
69
+ dbAudience(async (event) => await db.insert("logs", event))
70
+ );
71
+
72
+ // Target specific audiences per story
73
+ story.oops("Critical failure", error).to("console", "db");
74
+ ```
75
+
76
+ ## Summaries
77
+
78
+ Generate a formatted summary without emitting:
79
+
80
+ ```ts
81
+ const summary = story.summarize({
82
+ title: "Dashboard status",
83
+ level: "tell",
84
+ verbosity: "full",
85
+ });
86
+
87
+ console.log(summary.text);
88
+ ```
89
+
90
+ ```
91
+ Story: Dashboard status
92
+ Level: tell
93
+ Time: Mar 22, 2026, 3:42:18 PM (12ms)
94
+ Origin: checkout / Payment
95
+ Notes:
96
+ 3:42:18 PM — User submitted payment
97
+ 3:42:18 PM — Charging card
98
+ ```
99
+
100
+ ## Shared Instance
101
+
102
+ Use `useStoryteller()` for cross-component logging into the same story:
103
+
104
+ ```ts
105
+ import { useStoryteller } from "@lovelaces-io/storyteller";
106
+
107
+ // Same instance everywhere
108
+ const story = useStoryteller({ origin: { where: { app: "admin" } } });
109
+ ```
110
+
111
+ ## Structured Output
112
+
113
+ Every story is a typed, serializable JSON object — designed for humans and machines:
114
+
115
+ ```json
116
+ {
117
+ "timestamp": "2026-03-22T14:15:03.421Z",
118
+ "level": "oops",
119
+ "title": "Payment failed",
120
+ "origin": { "where": { "app": "checkout", "page": "Payment" } },
121
+ "notes": [
122
+ {
123
+ "timestamp": "2026-03-22T14:15:02.218Z",
124
+ "note": "User submitted payment",
125
+ "who": { "id": "user:413" }
126
+ }
127
+ ],
128
+ "error": { "name": "Error", "message": "gateway timeout" }
129
+ }
130
+ ```
131
+
132
+ ## Docs
133
+
134
+ - [API Reference](docs/API.md) — full signatures and examples
135
+ - [How It Works](docs/HOW-IT-WORKS.md) — narrative guide with real-world scenarios
136
+ - [Changelog](CHANGELOG.md)
137
+
138
+ ## License
139
+
140
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,515 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ ANSI: () => ANSI,
24
+ Storyteller: () => Storyteller,
25
+ colorizeJsonSections: () => colorizeJsonSections,
26
+ consoleAudience: () => consoleAudience,
27
+ countBrackets: () => countBrackets,
28
+ dbAudience: () => dbAudience,
29
+ formatOrigin: () => formatOrigin,
30
+ getLevelColor: () => getLevelColor,
31
+ summarizeStory: () => summarizeStory,
32
+ useStoryteller: () => useStoryteller,
33
+ writeStoryReport: () => writeStoryReport
34
+ });
35
+ module.exports = __toCommonJS(index_exports);
36
+
37
+ // src/utils.ts
38
+ var ANSI = {
39
+ reset: "\x1B[0m",
40
+ green: "\x1B[32m",
41
+ yellow: "\x1B[33m",
42
+ red: "\x1B[38;2;250;128;114m",
43
+ grayLight: "\x1B[37m",
44
+ grayDark: "\x1B[37m"
45
+ };
46
+ function getLevelColor(level) {
47
+ if (level === "tell") return ANSI.green;
48
+ if (level === "warn") return ANSI.yellow;
49
+ return ANSI.red;
50
+ }
51
+ function formatOrigin(origin) {
52
+ if (!origin?.where) return;
53
+ if (typeof origin.where === "string") return origin.where;
54
+ const whereRecord = origin.where;
55
+ const parts = [whereRecord.app, whereRecord.service, whereRecord.page, whereRecord.component].filter(Boolean).map(String);
56
+ return parts.length ? parts.join(" / ") : void 0;
57
+ }
58
+ function colorizeJsonSections(json, colors) {
59
+ const lines = json.split("\n");
60
+ let insideNotes = false;
61
+ let bracketDepth = 0;
62
+ return lines.map((line) => {
63
+ if (!insideNotes && line.includes('"notes": [')) {
64
+ insideNotes = true;
65
+ bracketDepth = countBrackets(line);
66
+ return `${colors.notes}${line}${colors.reset}`;
67
+ }
68
+ if (insideNotes) {
69
+ const colored = `${colors.notes}${line}${colors.reset}`;
70
+ bracketDepth += countBrackets(line);
71
+ if (bracketDepth <= 0) insideNotes = false;
72
+ return colored;
73
+ }
74
+ return `${colors.base}${line}${colors.reset}`;
75
+ });
76
+ }
77
+ function countBrackets(line) {
78
+ const openCount = (line.match(/\[/g) || []).length;
79
+ const closeCount = (line.match(/\]/g) || []).length;
80
+ return openCount - closeCount;
81
+ }
82
+
83
+ // src/audiences/consoleAudience.ts
84
+ function consoleAudience() {
85
+ return {
86
+ 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);
100
+ }
101
+ console.groupEnd();
102
+ }
103
+ };
104
+ }
105
+
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;
113
+ }
114
+ /** Remove an audience member by name */
115
+ remove(name) {
116
+ this.members.delete(name);
117
+ return this;
118
+ }
119
+ /** Return all registered audience members */
120
+ getAll() {
121
+ return [...this.members.values()];
122
+ }
123
+ /** Return only the audience members matching the given names */
124
+ getOnly(names) {
125
+ return names.map((name) => this.members.get(name)).filter(Boolean);
126
+ }
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));
136
+ }
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;
148
+ }
149
+ /** Clear all accumulated notes without emitting a story */
150
+ reset() {
151
+ this.notes = [];
152
+ return this;
153
+ }
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);
171
+ }
172
+ /** Emit a story at the "tell" level (success / informational) */
173
+ tell(title) {
174
+ return this.createDelivery("tell", title);
175
+ }
176
+ /** Emit a story at the "warn" level (something was off) */
177
+ warn(title) {
178
+ return this.createDelivery("warn", title);
179
+ }
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);
183
+ }
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 });
200
+ }
201
+ };
202
+ }
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) } : {}
215
+ };
216
+ const eventWithSummary = event;
217
+ Object.defineProperty(eventWithSummary, "summarize", {
218
+ value: (options) => summarizeStory(event, options),
219
+ enumerable: false
220
+ });
221
+ return eventWithSummary;
222
+ }
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))
228
+ );
229
+ }
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;
239
+ }
240
+ const cause = rawError.cause;
241
+ if (cause !== void 0) {
242
+ normalized.cause = cause;
243
+ }
244
+ return normalized;
245
+ }
246
+ return { message: String(rawError) };
247
+ }
248
+ function calculateNoteDuration(notes) {
249
+ if (notes.length <= 1) {
250
+ return {
251
+ durationMs: void 0
252
+ };
253
+ }
254
+ const startTime = Date.parse(notes[0].timestamp);
255
+ const endTime = Date.parse(notes[notes.length - 1].timestamp);
256
+ return {
257
+ durationMs: Number.isFinite(startTime) && Number.isFinite(endTime) ? Math.max(0, endTime - startTime) : void 0
258
+ };
259
+ }
260
+ function summarizeStory(story, options = {}) {
261
+ const {
262
+ timezone = Intl.DateTimeFormat().resolvedOptions().timeZone,
263
+ locale = "en-US",
264
+ verbosity = "normal",
265
+ maxNotes = 50,
266
+ showData = true,
267
+ colorize = true
268
+ } = options;
269
+ const dateTimeFormatter = new Intl.DateTimeFormat(locale, {
270
+ timeZone: timezone,
271
+ year: "numeric",
272
+ month: "short",
273
+ day: "2-digit",
274
+ hour: "numeric",
275
+ minute: "2-digit",
276
+ second: "2-digit"
277
+ });
278
+ const timeFormatter = new Intl.DateTimeFormat(locale, {
279
+ timeZone: timezone,
280
+ hour: "numeric",
281
+ minute: "2-digit",
282
+ second: "2-digit"
283
+ });
284
+ const orderedNotes = [...story.notes].sort(
285
+ (noteA, noteB) => Date.parse(noteA.timestamp) - Date.parse(noteB.timestamp)
286
+ );
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) => ({
292
+ timestamp: note.timestamp,
293
+ when: timeFormatter.format(new Date(note.timestamp)),
294
+ note: note.note,
295
+ text: formatNoteText(note, verbosity),
296
+ ...note.who ? { who: note.who } : {},
297
+ ...note.what ? { what: note.what } : {},
298
+ ...note.where ? { where: note.where } : {},
299
+ ...note.error ? { error: note.error } : {}
300
+ }));
301
+ const data = {
302
+ title: story.title,
303
+ level: story.level,
304
+ when: dateTimeFormatter.format(new Date(story.timestamp)),
305
+ ...noteTiming.durationMs != null ? { durationMs: noteTiming.durationMs } : {},
306
+ ...duration ? { duration } : {},
307
+ ...story.origin ? { origin: story.origin } : {},
308
+ notes: summaryNotes,
309
+ ...story.error ? { error: story.error } : {}
310
+ };
311
+ const levelColor = getLevelColor(story.level);
312
+ const label = (text) => colorize ? `${levelColor}${text}${ANSI.reset}` : text;
313
+ const lines = [];
314
+ lines.push(`${label("Story")}: ${story.title}`);
315
+ lines.push(`${label("Level")}: ${story.level}`);
316
+ lines.push(`${label("Time")}: ${data.when}${duration ? ` (${duration})` : ""}`);
317
+ if (originLabel) {
318
+ lines.push(`${label("Origin")}: ${originLabel}`);
319
+ }
320
+ if (story.error) {
321
+ const errorLine = [story.error.name, story.error.message].filter(Boolean).join(": ");
322
+ if (errorLine) lines.push(`${label("Error")}: ${errorLine}`);
323
+ }
324
+ if (verbosity !== "brief" && summaryNotes.length) {
325
+ lines.push(`${label("Notes")}:`);
326
+ for (const note of summaryNotes) {
327
+ lines.push(` ${note.when} \u2014 ${note.text}`);
328
+ }
329
+ if (orderedNotes.length > summaryNotes.length) {
330
+ lines.push(` \u2026 (${orderedNotes.length - summaryNotes.length} more)`);
331
+ }
332
+ }
333
+ if (showData) {
334
+ lines.push(`${label("Data")}:`);
335
+ const json = JSON.stringify(data, null, 2);
336
+ if (colorize) {
337
+ const colored = colorizeJsonSections(json, {
338
+ base: ANSI.grayLight,
339
+ notes: ANSI.grayDark,
340
+ reset: ANSI.reset
341
+ });
342
+ lines.push(...colored);
343
+ } else {
344
+ lines.push(...json.split("\n"));
345
+ }
346
+ }
347
+ return { text: lines.join("\n"), data };
348
+ }
349
+ function formatDuration(milliseconds) {
350
+ if (milliseconds < 1e3) return `${milliseconds}ms`;
351
+ const seconds = milliseconds / 1e3;
352
+ if (seconds < 60) return `${seconds.toFixed(1)}s`;
353
+ const minutes = Math.floor(seconds / 60);
354
+ const remainingSeconds = Math.round(seconds % 60).toString().padStart(2, "0");
355
+ return `${minutes}:${remainingSeconds}m`;
356
+ }
357
+ function formatNoteText(note, verbosity) {
358
+ if (verbosity !== "full") return note.note;
359
+ const details = [];
360
+ const what = note.what;
361
+ const where = note.where;
362
+ if (typeof what === "string") {
363
+ details.push(`what=${what}`);
364
+ } else if (what) {
365
+ if (what.field) details.push(`field=${String(what.field)}`);
366
+ if (what.status) details.push(`status=${String(what.status)}`);
367
+ }
368
+ if (typeof where === "string") {
369
+ details.push(`where=${where}`);
370
+ } else if (where) {
371
+ if (where.component) details.push(`component=${String(where.component)}`);
372
+ }
373
+ if (note.error) {
374
+ const errorLine = [note.error.name, note.error.message].filter(Boolean).join(": ");
375
+ if (errorLine) details.push(`error=${errorLine}`);
376
+ }
377
+ return details.length ? `${note.note} (${details.join(" ")})` : note.note;
378
+ }
379
+
380
+ // src/useStoryteller.ts
381
+ var sharedInstance;
382
+ function useStoryteller(options = {}) {
383
+ if (!sharedInstance || options.reset) {
384
+ sharedInstance = new Storyteller({ origin: options.origin });
385
+ return sharedInstance;
386
+ }
387
+ return sharedInstance;
388
+ }
389
+
390
+ // src/audiences/dbAudience.ts
391
+ function dbAudience(insert) {
392
+ return {
393
+ name: "db",
394
+ accepts: (event) => event.level === "warn" || event.level === "oops",
395
+ hear: async (event) => {
396
+ await insert(event);
397
+ }
398
+ };
399
+ }
400
+
401
+ // src/report/writeStoryReport.ts
402
+ function writeStoryReport(stories, options = {}) {
403
+ const {
404
+ timezone = Intl.DateTimeFormat().resolvedOptions().timeZone,
405
+ locale = "en-US",
406
+ verbosity = "normal",
407
+ maxNotesPerStory = 50,
408
+ showData = true,
409
+ colorize = true
410
+ } = options;
411
+ if (!stories.length) {
412
+ return "Storyteller Report\n\n(no stories)\n";
413
+ }
414
+ const sorted = [...stories].sort(
415
+ (storyA, storyB) => Date.parse(storyA.timestamp) - Date.parse(storyB.timestamp)
416
+ );
417
+ const dateFormatter = new Intl.DateTimeFormat(locale, {
418
+ timeZone: timezone,
419
+ year: "numeric",
420
+ month: "short",
421
+ day: "2-digit"
422
+ });
423
+ const firstStory = sorted[0];
424
+ const lastStory = sorted[sorted.length - 1];
425
+ if (!firstStory || !lastStory) {
426
+ return "Storyteller Report\n\n(no stories)\n";
427
+ }
428
+ const lines = [];
429
+ lines.push(`Storyteller Report (${timezone})`);
430
+ lines.push(
431
+ `Range: ${dateFormatter.format(new Date(firstStory.timestamp))} \u2013 ${dateFormatter.format(
432
+ new Date(lastStory.timestamp)
433
+ )}`
434
+ );
435
+ lines.push("");
436
+ const storiesByDay = /* @__PURE__ */ new Map();
437
+ for (const story of sorted) {
438
+ const dayKey = dateFormatter.format(new Date(story.timestamp));
439
+ const dayEvents = storiesByDay.get(dayKey) ?? [];
440
+ dayEvents.push(story);
441
+ storiesByDay.set(dayKey, dayEvents);
442
+ }
443
+ for (const [day, dayStories] of storiesByDay) {
444
+ lines.push(day);
445
+ for (const story of dayStories) {
446
+ const summary = summarizeStory(story, {
447
+ timezone,
448
+ locale,
449
+ verbosity,
450
+ maxNotes: maxNotesPerStory,
451
+ colorize
452
+ });
453
+ const { data } = summary;
454
+ const originLabel = formatOrigin(story.origin);
455
+ const levelColor = getLevelColor(story.level);
456
+ const label = (text) => colorize ? `${levelColor}${text}${ANSI.reset}` : text;
457
+ const duration = data.duration ? ` (${data.duration})` : "";
458
+ lines.push(`${label("Story")}: ${story.title}`);
459
+ lines.push(`${label("Level")}: ${story.level}`);
460
+ lines.push(`${label("Time")}: ${data.when}${duration}`);
461
+ if (originLabel) {
462
+ lines.push(`${label("Origin")}: ${originLabel}`);
463
+ }
464
+ if (data.error) {
465
+ const errorLine = [
466
+ data.error.name,
467
+ data.error.message
468
+ ].filter(Boolean).join(": ");
469
+ if (errorLine) lines.push(`${label("Error")}: ${errorLine}`);
470
+ }
471
+ if (verbosity !== "brief" && data.notes.length) {
472
+ lines.push(` ${label("Notes")}:`);
473
+ for (const summaryNote of data.notes) {
474
+ lines.push(` ${summaryNote.when} \u2014 ${summaryNote.text}`);
475
+ }
476
+ if (story.notes.length > data.notes.length) {
477
+ lines.push(
478
+ ` \u2026 (${story.notes.length - data.notes.length} more)`
479
+ );
480
+ }
481
+ }
482
+ if (showData) {
483
+ lines.push(`${label("Data")}:`);
484
+ const json = JSON.stringify(data, null, 2);
485
+ if (colorize) {
486
+ const colored = colorizeJsonSections(json, {
487
+ base: ANSI.grayLight,
488
+ notes: ANSI.grayDark,
489
+ reset: ANSI.reset
490
+ });
491
+ lines.push(...colored);
492
+ } else {
493
+ lines.push(...json.split("\n"));
494
+ }
495
+ }
496
+ lines.push("");
497
+ }
498
+ }
499
+ return lines.join("\n").trim() + "\n";
500
+ }
501
+ // Annotate the CommonJS export names for ESM import in node:
502
+ 0 && (module.exports = {
503
+ ANSI,
504
+ Storyteller,
505
+ colorizeJsonSections,
506
+ consoleAudience,
507
+ countBrackets,
508
+ dbAudience,
509
+ formatOrigin,
510
+ getLevelColor,
511
+ summarizeStory,
512
+ useStoryteller,
513
+ writeStoryReport
514
+ });
515
+ //# sourceMappingURL=index.cjs.map