@lovelaces-io/storyteller 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -18,34 +18,45 @@ var __copyProps = (to, from, except, desc) => {
18
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
19
 
20
20
  // src/index.ts
21
- var index_exports = {};
22
- __export(index_exports, {
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
23
  ANSI: () => ANSI,
24
24
  AudienceRegistry: () => AudienceRegistry,
25
25
  DEFAULT_REDACT_KEYS: () => DEFAULT_REDACT_KEYS,
26
26
  REDACTED: () => REDACTED,
27
+ StoryQueryBuilder: () => StoryQueryBuilder,
27
28
  Storyteller: () => Storyteller,
29
+ applyQuery: () => applyQuery,
30
+ canonicalRow: () => canonicalRow,
28
31
  consoleAudience: () => consoleAudience,
29
32
  dbAudience: () => dbAudience,
33
+ flattenOrigin: () => flattenOrigin,
30
34
  formatDuration: () => formatDuration,
31
35
  formatOrigin: () => formatOrigin,
32
36
  formatStory: () => formatStory,
33
37
  getLevelColor: () => getLevelColor,
38
+ matchesQuery: () => matchesQuery,
34
39
  meetsLevel: () => meetsLevel,
40
+ memoryStore: () => memoryStore,
35
41
  ndjsonAudience: () => ndjsonAudience,
36
42
  normalizeError: () => normalizeError,
37
43
  normalizeValue: () => normalizeValue,
44
+ parseDuration: () => parseDuration,
38
45
  readEnvironmentValue: () => readEnvironmentValue,
39
46
  resolveColors: () => resolveColors,
40
47
  resolveMinimumLevel: () => resolveMinimumLevel,
41
48
  resolveOutputFormat: () => resolveOutputFormat,
49
+ storeAudience: () => storeAudience,
50
+ stories: () => stories,
51
+ storySearchText: () => storySearchText,
42
52
  summarizeContext: () => summarizeContext,
43
53
  summarizeStory: () => summarizeStory,
54
+ toStoredStory: () => toStoredStory,
44
55
  toStoryLevel: () => toStoryLevel,
45
56
  useStoryteller: () => useStoryteller,
46
57
  writeStoryReport: () => writeStoryReport
47
58
  });
48
- module.exports = __toCommonJS(index_exports);
59
+ module.exports = __toCommonJS(src_exports);
49
60
 
50
61
  // src/environment.ts
51
62
  var LEVEL_RANK = {
@@ -246,8 +257,138 @@ function readClockTime(timestamp) {
246
257
  return timePart.length === 8 ? timePart : timestamp;
247
258
  }
248
259
 
249
- // src/normalize.ts
260
+ // src/redaction.ts
250
261
  var REDACTED = "[redacted]";
262
+ var RULE_GROUPS = [
263
+ {
264
+ // Vendor key formats
265
+ anchor: /sk[-_]|rk_|gh[opusr]_|github_pat_|glpat-|npm_|xox[abprs]-|AIza|SG\.|AKIA|ASIA|eyJ|PRIVATE KEY/,
266
+ rules: [
267
+ // Stripe secret and restricted keys
268
+ { pattern: /\b(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{8,}\b/g, replacement: REDACTED },
269
+ // OpenAI, Anthropic and similar `sk-` keys
270
+ { pattern: /\bsk-(?:[A-Za-z0-9]+-)*[A-Za-z0-9_-]{16,}/g, replacement: REDACTED },
271
+ // GitHub tokens, classic and fine-grained
272
+ { pattern: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{16,}\b/g, replacement: REDACTED },
273
+ { pattern: /\bgithub_pat_[A-Za-z0-9_]{20,}\b/g, replacement: REDACTED },
274
+ // GitLab, npm, Slack, Google, SendGrid, AWS access key ids
275
+ { pattern: /\bglpat-[A-Za-z0-9_-]{16,}\b/g, replacement: REDACTED },
276
+ { pattern: /\bnpm_[A-Za-z0-9]{30,}\b/g, replacement: REDACTED },
277
+ { pattern: /\bxox[abprs]-[A-Za-z0-9-]{10,}\b/g, replacement: REDACTED },
278
+ { pattern: /\bAIza[0-9A-Za-z_-]{35}\b/g, replacement: REDACTED },
279
+ { pattern: /\bSG\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\b/g, replacement: REDACTED },
280
+ { pattern: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g, replacement: REDACTED },
281
+ // JSON Web Tokens
282
+ { pattern: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g, replacement: REDACTED },
283
+ // PEM private keys, whole block
284
+ { pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, replacement: REDACTED }
285
+ ]
286
+ },
287
+ {
288
+ // Authorization header values: keep the scheme, drop the credential
289
+ anchor: /\b(?:Bearer|Basic|Token|Digest) /,
290
+ rules: [{ pattern: /\b(Bearer|Basic|Token|Digest)\s+[A-Za-z0-9._~+/=-]{12,}/g, replacement: `$1 ${REDACTED}` }]
291
+ },
292
+ {
293
+ // URLs: a password in the userinfo, a secret in the query string
294
+ anchor: /:\/\/|[?&]/,
295
+ rules: [
296
+ { pattern: /(:\/\/[^\s/:@]+:)[^\s@/]+(@)/g, replacement: `$1${REDACTED}$2` },
297
+ {
298
+ pattern: /([?&](?:token|access_token|refresh_token|id_token|api_key|apikey|api-key|key|secret|client_secret|password|passwd|pwd|signature|sig|auth|authorization)=)[^&\s#]+/gi,
299
+ replacement: `$1${REDACTED}`
300
+ }
301
+ ]
302
+ }
303
+ ];
304
+ var STRONG_KEY_HINT = /password|passwd|passphrase|secret|apikey|privatekey|credential|authorization|accesskey|sessionid|cookie|refreshtoken|accesstoken|idtoken|clientkey|signingkey|encryptionkey|masterkey|servicekey/;
305
+ var WEAK_KEY_HINT = /token|key|auth|bearer|cert|pwd/;
306
+ var keyHints = /* @__PURE__ */ new Map();
307
+ var KEY_HINT_CACHE_LIMIT = 4096;
308
+ function hintFor(key) {
309
+ const cached = keyHints.get(key);
310
+ if (cached !== void 0) return cached;
311
+ const reduced = normalizeKeyForMatching(key);
312
+ const hint = STRONG_KEY_HINT.test(reduced) ? "strong" : WEAK_KEY_HINT.test(reduced) ? "weak" : "none";
313
+ if (keyHints.size >= KEY_HINT_CACHE_LIMIT) keyHints.clear();
314
+ keyHints.set(key, hint);
315
+ return hint;
316
+ }
317
+ var MIN_STRONG_LENGTH = 4;
318
+ var MIN_ENTROPY_LENGTH = 16;
319
+ var MIN_ENTROPY_BITS = 3;
320
+ var MIN_STRICT_LENGTH = 32;
321
+ function normalizeKeyForMatching(key) {
322
+ return key.replace(/[^a-zA-Z0-9]/g, "").toLowerCase();
323
+ }
324
+ function entropyOf(text) {
325
+ const counts = /* @__PURE__ */ new Map();
326
+ for (const char of text) counts.set(char, (counts.get(char) ?? 0) + 1);
327
+ let bits = 0;
328
+ for (const count of counts.values()) {
329
+ const p = count / text.length;
330
+ bits -= p * Math.log2(p);
331
+ }
332
+ return bits;
333
+ }
334
+ function looksRandom(value, minimumLength) {
335
+ if (value.length < minimumLength || /\s/.test(value)) return false;
336
+ if (/^[0-9a-f-]+$/i.test(value) || /^[0-9.-]+$/.test(value)) return false;
337
+ return entropyOf(value) >= MIN_ENTROPY_BITS;
338
+ }
339
+ function keyLooksSecret(key, value) {
340
+ if (typeof value !== "string") return false;
341
+ const hint = hintFor(key);
342
+ if (hint === "strong") return value.length >= MIN_STRONG_LENGTH;
343
+ if (hint === "weak") return looksRandom(value, MIN_ENTROPY_LENGTH);
344
+ return false;
345
+ }
346
+ var ANY_ANCHOR = new RegExp(RULE_GROUPS.map((group) => group.anchor.source).join("|"));
347
+ function redactString(value, strictness = "balanced") {
348
+ if (strictness === "off" || value.length < 8) return value;
349
+ let result = value;
350
+ if (!ANY_ANCHOR.test(value)) return strictness === "strict" ? redactRandomRuns(value) : value;
351
+ for (const group of RULE_GROUPS) {
352
+ if (!group.anchor.test(result)) continue;
353
+ for (const { pattern, replacement } of group.rules) {
354
+ pattern.lastIndex = 0;
355
+ result = result.replace(pattern, replacement);
356
+ }
357
+ }
358
+ return strictness === "strict" ? redactRandomRuns(result) : result;
359
+ }
360
+ function redactRandomRuns(text) {
361
+ return text.replace(
362
+ /(?<![A-Za-z0-9+/=_-])[A-Za-z0-9+/_-]{32,}={0,2}(?![A-Za-z0-9+/=_-])/g,
363
+ (run) => looksRandom(run, MIN_STRICT_LENGTH) && characterClasses(run) >= 3 ? REDACTED : run
364
+ );
365
+ }
366
+ function characterClasses(text) {
367
+ return [/[a-z]/, /[A-Z]/, /[0-9]/, /[^A-Za-z0-9]/].filter((test) => test.test(text)).length;
368
+ }
369
+ function redactJson(value, options = {}) {
370
+ const strictness = options.strictness ?? "balanced";
371
+ const keys = options.redactKeys;
372
+ const walk = (node) => {
373
+ if (typeof node === "string") return redactString(node, strictness);
374
+ if (Array.isArray(node)) return node.map(walk);
375
+ if (node && typeof node === "object") {
376
+ const out = {};
377
+ for (const [key, child] of Object.entries(node)) {
378
+ if (keys?.has(normalizeKeyForMatching(key)) || strictness !== "off" && keyLooksSecret(key, child)) {
379
+ out[key] = REDACTED;
380
+ } else {
381
+ out[key] = walk(child);
382
+ }
383
+ }
384
+ return out;
385
+ }
386
+ return node;
387
+ };
388
+ return walk(value);
389
+ }
390
+
391
+ // src/normalize.ts
251
392
  var DEFAULT_REDACT_KEYS = [
252
393
  "password",
253
394
  "passphrase",
@@ -278,7 +419,8 @@ function normalizeValue(input, options = {}) {
278
419
  redactKeys: new Set(
279
420
  (options.redactKeys ?? DEFAULT_REDACT_KEYS).map(normalizeKeyForMatching)
280
421
  ),
281
- redact: options.redact ?? true
422
+ redact: options.redact ?? true,
423
+ redactValues: options.redactValues ?? "balanced"
282
424
  };
283
425
  try {
284
426
  return normalizeUnknown(input, resolved, 0, "$", /* @__PURE__ */ new Map());
@@ -295,7 +437,8 @@ function normalizeError(rawError, options = {}) {
295
437
  redactKeys: new Set(
296
438
  (options.redactKeys ?? DEFAULT_REDACT_KEYS).map(normalizeKeyForMatching)
297
439
  ),
298
- redact: options.redact ?? true
440
+ redact: options.redact ?? true,
441
+ redactValues: options.redactValues ?? "balanced"
299
442
  };
300
443
  return normalizeErrorInternal(rawError, resolved, 0);
301
444
  }
@@ -303,7 +446,7 @@ function normalizeErrorInternal(rawError, options, causeDepth) {
303
446
  if (!(rawError instanceof Error)) {
304
447
  if (isPlainRecord(rawError)) {
305
448
  const record = rawError;
306
- const message = typeof record["message"] === "string" ? record["message"] : void 0;
449
+ const message = typeof record["message"] === "string" ? redactText(record["message"], options) : void 0;
307
450
  const name = typeof record["name"] === "string" ? record["name"] : void 0;
308
451
  if (message !== void 0 || name !== void 0) {
309
452
  return {
@@ -312,14 +455,14 @@ function normalizeErrorInternal(rawError, options, causeDepth) {
312
455
  };
313
456
  }
314
457
  }
315
- return { message: safeStringify(rawError, options.maxStringLength) };
458
+ return { message: redactText(safeStringify(rawError, options.maxStringLength), options) };
316
459
  }
317
460
  const normalized = {
318
461
  name: rawError.name,
319
- message: rawError.message
462
+ message: redactText(rawError.message, options)
320
463
  };
321
464
  if (rawError.stack !== void 0) {
322
- normalized.stack = truncateString(rawError.stack, options.maxStringLength);
465
+ normalized.stack = truncateString(redactText(rawError.stack, options), options.maxStringLength);
323
466
  }
324
467
  const cause = rawError.cause;
325
468
  if (cause !== void 0) {
@@ -341,7 +484,7 @@ function normalizeUnknown(value, options, depth, path, ancestors) {
341
484
  if (value === null) return null;
342
485
  const valueType = typeof value;
343
486
  if (valueType === "string") {
344
- return truncateString(value, options.maxStringLength);
487
+ return truncateString(redactText(value, options), options.maxStringLength);
345
488
  }
346
489
  if (valueType === "number") {
347
490
  return Number.isFinite(value) ? value : String(value);
@@ -535,10 +678,13 @@ function redactOrNormalize(key, value, options, depth, path, ancestors) {
535
678
  if (options.redact && options.redactKeys.has(normalizeKeyForMatching(key))) {
536
679
  return REDACTED;
537
680
  }
681
+ if (options.redact && options.redactValues !== "off" && keyLooksSecret(key, value)) {
682
+ return REDACTED;
683
+ }
538
684
  return normalizeUnknown(value, options, depth, path, ancestors);
539
685
  }
540
- function normalizeKeyForMatching(key) {
541
- return key.replace(/[^a-zA-Z0-9]/g, "").toLowerCase();
686
+ function redactText(value, options) {
687
+ return options.redact ? redactString(value, options.redactValues) : value;
542
688
  }
543
689
  function readConstructorName(value) {
544
690
  try {
@@ -1216,8 +1362,286 @@ function dbAudience(insert) {
1216
1362
  };
1217
1363
  }
1218
1364
 
1365
+ // src/store/storyStore.ts
1366
+ var DEFAULT_KEY_SET = new Set(DEFAULT_REDACT_KEYS.map(normalizeKeyForMatching));
1367
+ function toStoredStory(event, options = {}) {
1368
+ const { timestamp, level, title, storyId, parentStoryId, origin, notes, durationMs, droppedEmissions, error } = event;
1369
+ const strictness = options.redactValues ?? "balanced";
1370
+ const scrub = (value) => strictness === "off" ? clone(value) : redactJson(clone(value), { redactKeys: DEFAULT_KEY_SET, strictness });
1371
+ const record = {
1372
+ timestamp,
1373
+ level,
1374
+ title: scrub(title),
1375
+ storyId: storyId ?? generateStoryId(),
1376
+ notes: scrub(notes)
1377
+ };
1378
+ if (parentStoryId !== void 0) record.parentStoryId = parentStoryId;
1379
+ if (origin !== void 0) record.origin = scrub(origin);
1380
+ if (durationMs !== void 0) record.durationMs = durationMs;
1381
+ if (droppedEmissions !== void 0) record.droppedEmissions = droppedEmissions;
1382
+ if (error !== void 0) record.error = scrub(error);
1383
+ return record;
1384
+ }
1385
+ function clone(value) {
1386
+ return JSON.parse(JSON.stringify(value));
1387
+ }
1388
+ function generateStoryId() {
1389
+ const random = globalThis.crypto?.randomUUID;
1390
+ if (random) return random.call(globalThis.crypto);
1391
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
1392
+ }
1393
+ function storySearchText(story) {
1394
+ const parts = [story.title];
1395
+ for (const note of story.notes) {
1396
+ parts.push(note.note);
1397
+ for (const value of [note.who, note.what, note.where]) {
1398
+ if (typeof value === "string") parts.push(value);
1399
+ }
1400
+ if (note.error?.message) parts.push(note.error.message);
1401
+ }
1402
+ if (story.error?.message) parts.push(story.error.message);
1403
+ return parts.join("\n");
1404
+ }
1405
+ function flattenOrigin(origin) {
1406
+ const flat = {};
1407
+ if (!origin) return flat;
1408
+ for (const key of ["who", "what", "where"]) {
1409
+ const value = origin[key];
1410
+ if (value === void 0 || value === null) continue;
1411
+ flat[key] = typeof value === "object" ? JSON.stringify(value) : String(value);
1412
+ }
1413
+ return flat;
1414
+ }
1415
+ function canonicalRow(event) {
1416
+ const story = toStoredStory(event);
1417
+ const origin = flattenOrigin(story.origin);
1418
+ return {
1419
+ story_id: story.storyId,
1420
+ parent_story_id: story.parentStoryId ?? null,
1421
+ timestamp: story.timestamp,
1422
+ level: story.level,
1423
+ title: story.title,
1424
+ origin_who: origin.who ?? null,
1425
+ origin_what: origin.what ?? null,
1426
+ origin_where: origin.where ?? null,
1427
+ duration_ms: story.durationMs ?? null,
1428
+ error_message: story.error?.message ?? null,
1429
+ notes: JSON.stringify(story.notes),
1430
+ search_text: storySearchText(story),
1431
+ record: JSON.stringify(story)
1432
+ };
1433
+ }
1434
+ function matchesQuery(story, query = {}) {
1435
+ if (query.since && Date.parse(story.timestamp) < query.since.getTime()) return false;
1436
+ if (query.until && Date.parse(story.timestamp) >= query.until.getTime()) return false;
1437
+ if (query.level !== void 0) {
1438
+ const levels = Array.isArray(query.level) ? query.level : [query.level];
1439
+ if (!levels.includes(story.level)) return false;
1440
+ }
1441
+ if (query.minimumLevel && !meetsLevel(story.level, query.minimumLevel)) return false;
1442
+ if (query.parentStoryId !== void 0 && story.parentStoryId !== query.parentStoryId) return false;
1443
+ if (query.slowerThanMs !== void 0 && !((story.durationMs ?? -1) > query.slowerThanMs)) return false;
1444
+ if (query.failed !== void 0) {
1445
+ const failed = story.level === "Error" || story.error !== void 0;
1446
+ if (failed !== query.failed) return false;
1447
+ }
1448
+ if (query.about) {
1449
+ if (!storySearchText(story).toLowerCase().includes(query.about.toLowerCase())) return false;
1450
+ }
1451
+ if (query.from) {
1452
+ const origin = Object.values(flattenOrigin(story.origin)).join("\n").toLowerCase();
1453
+ if (!origin.includes(query.from.toLowerCase())) return false;
1454
+ }
1455
+ return true;
1456
+ }
1457
+ function applyQuery(stories2, query = {}) {
1458
+ const matched = [];
1459
+ let position = 0;
1460
+ for (const story of stories2) {
1461
+ if (matchesQuery(story, query)) matched.push({ story, position });
1462
+ position++;
1463
+ }
1464
+ const direction = query.order === "oldest" ? 1 : -1;
1465
+ matched.sort((a, b) => {
1466
+ const byTime = Date.parse(a.story.timestamp) - Date.parse(b.story.timestamp);
1467
+ return (byTime !== 0 ? byTime : a.position - b.position) * direction;
1468
+ });
1469
+ const offset = Math.max(0, query.offset ?? 0);
1470
+ const end = query.limit !== void 0 ? offset + Math.max(0, query.limit) : void 0;
1471
+ return matched.slice(offset, end).map((entry) => entry.story);
1472
+ }
1473
+
1474
+ // src/store/memoryStore.ts
1475
+ function memoryStore(options = {}) {
1476
+ const capacity = Math.max(1, options.capacity ?? 1e4);
1477
+ const stories2 = /* @__PURE__ */ new Map();
1478
+ return {
1479
+ get size() {
1480
+ return stories2.size;
1481
+ },
1482
+ clear() {
1483
+ stories2.clear();
1484
+ },
1485
+ async append(event) {
1486
+ const record = toStoredStory(event);
1487
+ stories2.delete(record.storyId);
1488
+ stories2.set(record.storyId, record);
1489
+ while (stories2.size > capacity) {
1490
+ const oldest = stories2.keys().next().value;
1491
+ if (oldest === void 0) break;
1492
+ stories2.delete(oldest);
1493
+ }
1494
+ },
1495
+ async get(storyId) {
1496
+ return stories2.get(storyId);
1497
+ },
1498
+ async query(criteria = {}) {
1499
+ return applyQuery(stories2.values(), criteria);
1500
+ },
1501
+ async children(parentStoryId) {
1502
+ return applyQuery(stories2.values(), { parentStoryId, order: "oldest" });
1503
+ },
1504
+ async prune(before) {
1505
+ const boundary = before.getTime();
1506
+ let removed = 0;
1507
+ for (const [id, story] of stories2) {
1508
+ if (Date.parse(story.timestamp) < boundary) {
1509
+ stories2.delete(id);
1510
+ removed++;
1511
+ }
1512
+ }
1513
+ return removed;
1514
+ }
1515
+ };
1516
+ }
1517
+
1518
+ // src/store/storeAudience.ts
1519
+ function storeAudience(store, options = {}) {
1520
+ return {
1521
+ name: options.name ?? "store",
1522
+ hears: ["story"],
1523
+ accepts: (event) => (options.level === void 0 || meetsLevel(event.level, options.level)) && (options.accepts === void 0 || options.accepts(event)),
1524
+ hear: async (event) => {
1525
+ await store.append(event);
1526
+ }
1527
+ };
1528
+ }
1529
+
1530
+ // src/store/stories.ts
1531
+ var UNITS = {
1532
+ ms: 1,
1533
+ s: 1e3,
1534
+ m: 6e4,
1535
+ h: 36e5,
1536
+ d: 864e5,
1537
+ w: 6048e5
1538
+ };
1539
+ function parseDuration(input) {
1540
+ if (typeof input === "number") {
1541
+ if (!Number.isFinite(input) || input < 0) throw new RangeError(`Not a duration: ${input}`);
1542
+ return input;
1543
+ }
1544
+ const match = /^\s*(\d+(?:\.\d+)?)\s*(ms|s|m|h|d|w)\s*$/i.exec(input);
1545
+ if (!match) throw new RangeError(`Not a duration: "${input}" (expected e.g. "30s", "5m", "24h", "7d", "2w")`);
1546
+ return Number(match[1]) * UNITS[match[2].toLowerCase()];
1547
+ }
1548
+ var StoryQueryBuilder = class _StoryQueryBuilder {
1549
+ constructor(store, query, now) {
1550
+ this.store = store;
1551
+ this.query = query;
1552
+ this.now = now;
1553
+ }
1554
+ with(patch) {
1555
+ return new _StoryQueryBuilder(this.store, { ...this.query, ...patch }, this.now);
1556
+ }
1557
+ instant(input) {
1558
+ return input instanceof Date ? input : new Date(this.now().getTime() - parseDuration(input));
1559
+ }
1560
+ /** Title, note text, scalar context or error message mentions this */
1561
+ about(text) {
1562
+ return this.with({ about: text });
1563
+ }
1564
+ /** The origin — who, what or where — mentions this */
1565
+ from(origin) {
1566
+ return this.with({ from: origin });
1567
+ }
1568
+ /** Exactly this level. Accepts the aliases: `"info"`, `"warn"`, `"oops"`. */
1569
+ level(level) {
1570
+ return this.with({ level: toStoryLevel(level) });
1571
+ }
1572
+ /** This level or worse */
1573
+ atLeast(level) {
1574
+ return this.with({ minimumLevel: toStoryLevel(level) });
1575
+ }
1576
+ /** Carried an error, or closed at Error level */
1577
+ failing() {
1578
+ return this.with({ failed: true });
1579
+ }
1580
+ /** Neither an error nor closed at Error level */
1581
+ succeeding() {
1582
+ return this.with({ failed: false });
1583
+ }
1584
+ /** Took longer than this: `"5s"`, `"2m"`, or milliseconds */
1585
+ slowerThan(duration) {
1586
+ return this.with({ slowerThanMs: parseDuration(duration) });
1587
+ }
1588
+ /** Began within this long ago (`"24h"`), or at or after this date */
1589
+ since(when) {
1590
+ return this.with({ since: this.instant(when) });
1591
+ }
1592
+ /** Began before this long ago, or before this date */
1593
+ until(when) {
1594
+ return this.with({ until: this.instant(when) });
1595
+ }
1596
+ /** The chapters of this story */
1597
+ under(parentStoryId) {
1598
+ return this.with({ parentStoryId });
1599
+ }
1600
+ /** Most recent first — the default */
1601
+ newest() {
1602
+ return this.with({ order: "newest" });
1603
+ }
1604
+ /** Earliest first */
1605
+ oldest() {
1606
+ return this.with({ order: "oldest" });
1607
+ }
1608
+ /** At most this many */
1609
+ limit(count) {
1610
+ return this.with({ limit: count });
1611
+ }
1612
+ /** Skip this many first */
1613
+ skip(count) {
1614
+ return this.with({ offset: count });
1615
+ }
1616
+ /** The structured criteria this question compiles to — what an adapter receives */
1617
+ toQuery() {
1618
+ return { ...this.query };
1619
+ }
1620
+ /** Every matching story */
1621
+ all() {
1622
+ return this.store.query(this.toQuery());
1623
+ }
1624
+ /** The first match, or nothing */
1625
+ async first() {
1626
+ const [story] = await this.store.query({ ...this.query, limit: 1 });
1627
+ return story;
1628
+ }
1629
+ /** How many match, ignoring paging */
1630
+ async count() {
1631
+ const { limit: _limit, offset: _offset, ...unpaged } = this.query;
1632
+ return (await this.store.query(unpaged)).length;
1633
+ }
1634
+ /** An awaited builder resolves to `.all()` */
1635
+ then(onFulfilled, onRejected) {
1636
+ return this.all().then(onFulfilled, onRejected);
1637
+ }
1638
+ };
1639
+ function stories(store, options = {}) {
1640
+ return new StoryQueryBuilder(store, {}, options.now ?? (() => /* @__PURE__ */ new Date()));
1641
+ }
1642
+
1219
1643
  // src/report/writeStoryReport.ts
1220
- function writeStoryReport(stories, options = {}) {
1644
+ function writeStoryReport(stories2, options = {}) {
1221
1645
  const {
1222
1646
  timezone = Intl.DateTimeFormat().resolvedOptions().timeZone,
1223
1647
  locale = "en-US",
@@ -1226,10 +1650,10 @@ function writeStoryReport(stories, options = {}) {
1226
1650
  showData = true,
1227
1651
  colors = true
1228
1652
  } = options;
1229
- if (!stories.length) {
1653
+ if (!stories2.length) {
1230
1654
  return "Storyteller Report\n\n(no stories)\n";
1231
1655
  }
1232
- const sorted = [...stories].sort(
1656
+ const sorted = [...stories2].sort(
1233
1657
  (storyA, storyB) => Date.parse(storyA.timestamp) - Date.parse(storyB.timestamp)
1234
1658
  );
1235
1659
  const dateFormatter = new Intl.DateTimeFormat(locale, {
@@ -1322,23 +1746,34 @@ function writeStoryReport(stories, options = {}) {
1322
1746
  AudienceRegistry,
1323
1747
  DEFAULT_REDACT_KEYS,
1324
1748
  REDACTED,
1749
+ StoryQueryBuilder,
1325
1750
  Storyteller,
1751
+ applyQuery,
1752
+ canonicalRow,
1326
1753
  consoleAudience,
1327
1754
  dbAudience,
1755
+ flattenOrigin,
1328
1756
  formatDuration,
1329
1757
  formatOrigin,
1330
1758
  formatStory,
1331
1759
  getLevelColor,
1760
+ matchesQuery,
1332
1761
  meetsLevel,
1762
+ memoryStore,
1333
1763
  ndjsonAudience,
1334
1764
  normalizeError,
1335
1765
  normalizeValue,
1766
+ parseDuration,
1336
1767
  readEnvironmentValue,
1337
1768
  resolveColors,
1338
1769
  resolveMinimumLevel,
1339
1770
  resolveOutputFormat,
1771
+ storeAudience,
1772
+ stories,
1773
+ storySearchText,
1340
1774
  summarizeContext,
1341
1775
  summarizeStory,
1776
+ toStoredStory,
1342
1777
  toStoryLevel,
1343
1778
  useStoryteller,
1344
1779
  writeStoryReport