@lovelaces-io/storyteller 0.3.0 → 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 +42 -1
- package/README.md +30 -0
- package/dist/index.cjs +454 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +55 -515
- package/dist/index.d.ts +55 -515
- package/dist/index.js +440 -17
- 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 +11 -1
- 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 {
|
|
@@ -1209,16 +1355,293 @@ function dbAudience(insert) {
|
|
|
1209
1355
|
return {
|
|
1210
1356
|
name: "db",
|
|
1211
1357
|
hears: ["story"],
|
|
1212
|
-
accepts: (
|
|
1213
|
-
hear: async (
|
|
1214
|
-
|
|
1215
|
-
|
|
1358
|
+
accepts: (event) => event.level === "Warning" || event.level === "Error",
|
|
1359
|
+
hear: async (event) => {
|
|
1360
|
+
await insert(event);
|
|
1361
|
+
}
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
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);
|
|
1216
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)
|
|
1217
1432
|
};
|
|
1218
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
|
+
}
|
|
1219
1642
|
|
|
1220
1643
|
// src/report/writeStoryReport.ts
|
|
1221
|
-
function writeStoryReport(
|
|
1644
|
+
function writeStoryReport(stories2, options = {}) {
|
|
1222
1645
|
const {
|
|
1223
1646
|
timezone = Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
1224
1647
|
locale = "en-US",
|
|
@@ -1227,10 +1650,10 @@ function writeStoryReport(stories, options = {}) {
|
|
|
1227
1650
|
showData = true,
|
|
1228
1651
|
colors = true
|
|
1229
1652
|
} = options;
|
|
1230
|
-
if (!
|
|
1653
|
+
if (!stories2.length) {
|
|
1231
1654
|
return "Storyteller Report\n\n(no stories)\n";
|
|
1232
1655
|
}
|
|
1233
|
-
const sorted = [...
|
|
1656
|
+
const sorted = [...stories2].sort(
|
|
1234
1657
|
(storyA, storyB) => Date.parse(storyA.timestamp) - Date.parse(storyB.timestamp)
|
|
1235
1658
|
);
|
|
1236
1659
|
const dateFormatter = new Intl.DateTimeFormat(locale, {
|
|
@@ -1323,23 +1746,34 @@ function writeStoryReport(stories, options = {}) {
|
|
|
1323
1746
|
AudienceRegistry,
|
|
1324
1747
|
DEFAULT_REDACT_KEYS,
|
|
1325
1748
|
REDACTED,
|
|
1749
|
+
StoryQueryBuilder,
|
|
1326
1750
|
Storyteller,
|
|
1751
|
+
applyQuery,
|
|
1752
|
+
canonicalRow,
|
|
1327
1753
|
consoleAudience,
|
|
1328
1754
|
dbAudience,
|
|
1755
|
+
flattenOrigin,
|
|
1329
1756
|
formatDuration,
|
|
1330
1757
|
formatOrigin,
|
|
1331
1758
|
formatStory,
|
|
1332
1759
|
getLevelColor,
|
|
1760
|
+
matchesQuery,
|
|
1333
1761
|
meetsLevel,
|
|
1762
|
+
memoryStore,
|
|
1334
1763
|
ndjsonAudience,
|
|
1335
1764
|
normalizeError,
|
|
1336
1765
|
normalizeValue,
|
|
1766
|
+
parseDuration,
|
|
1337
1767
|
readEnvironmentValue,
|
|
1338
1768
|
resolveColors,
|
|
1339
1769
|
resolveMinimumLevel,
|
|
1340
1770
|
resolveOutputFormat,
|
|
1771
|
+
storeAudience,
|
|
1772
|
+
stories,
|
|
1773
|
+
storySearchText,
|
|
1341
1774
|
summarizeContext,
|
|
1342
1775
|
summarizeStory,
|
|
1776
|
+
toStoredStory,
|
|
1343
1777
|
toStoryLevel,
|
|
1344
1778
|
useStoryteller,
|
|
1345
1779
|
writeStoryReport
|