@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/AGENTS.md +41 -0
- package/README.md +30 -0
- package/dist/index.cjs +451 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +53 -517
- package/dist/index.d.ts +53 -517
- package/dist/index.js +437 -13
- package/dist/index.js.map +1 -1
- package/dist/store/file.cjs +513 -0
- package/dist/store/file.cjs.map +1 -0
- package/dist/store/file.d.cts +29 -0
- package/dist/store/file.d.ts +29 -0
- package/dist/store/file.js +478 -0
- package/dist/store/file.js.map +1 -0
- package/dist/stories-BgN4BSoZ.d.cts +744 -0
- package/dist/stories-BgN4BSoZ.d.ts +744 -0
- package/llms.txt +10 -0
- package/package.json +16 -9
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
|
|
22
|
-
__export(
|
|
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(
|
|
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/
|
|
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
|
|
541
|
-
return
|
|
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(
|
|
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 (!
|
|
1653
|
+
if (!stories2.length) {
|
|
1230
1654
|
return "Storyteller Report\n\n(no stories)\n";
|
|
1231
1655
|
}
|
|
1232
|
-
const sorted = [...
|
|
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
|