@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.js
CHANGED
|
@@ -197,8 +197,138 @@ function readClockTime(timestamp) {
|
|
|
197
197
|
return timePart.length === 8 ? timePart : timestamp;
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
-
// src/
|
|
200
|
+
// src/redaction.ts
|
|
201
201
|
var REDACTED = "[redacted]";
|
|
202
|
+
var RULE_GROUPS = [
|
|
203
|
+
{
|
|
204
|
+
// Vendor key formats
|
|
205
|
+
anchor: /sk[-_]|rk_|gh[opusr]_|github_pat_|glpat-|npm_|xox[abprs]-|AIza|SG\.|AKIA|ASIA|eyJ|PRIVATE KEY/,
|
|
206
|
+
rules: [
|
|
207
|
+
// Stripe secret and restricted keys
|
|
208
|
+
{ pattern: /\b(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{8,}\b/g, replacement: REDACTED },
|
|
209
|
+
// OpenAI, Anthropic and similar `sk-` keys
|
|
210
|
+
{ pattern: /\bsk-(?:[A-Za-z0-9]+-)*[A-Za-z0-9_-]{16,}/g, replacement: REDACTED },
|
|
211
|
+
// GitHub tokens, classic and fine-grained
|
|
212
|
+
{ pattern: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{16,}\b/g, replacement: REDACTED },
|
|
213
|
+
{ pattern: /\bgithub_pat_[A-Za-z0-9_]{20,}\b/g, replacement: REDACTED },
|
|
214
|
+
// GitLab, npm, Slack, Google, SendGrid, AWS access key ids
|
|
215
|
+
{ pattern: /\bglpat-[A-Za-z0-9_-]{16,}\b/g, replacement: REDACTED },
|
|
216
|
+
{ pattern: /\bnpm_[A-Za-z0-9]{30,}\b/g, replacement: REDACTED },
|
|
217
|
+
{ pattern: /\bxox[abprs]-[A-Za-z0-9-]{10,}\b/g, replacement: REDACTED },
|
|
218
|
+
{ pattern: /\bAIza[0-9A-Za-z_-]{35}\b/g, replacement: REDACTED },
|
|
219
|
+
{ pattern: /\bSG\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\b/g, replacement: REDACTED },
|
|
220
|
+
{ pattern: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g, replacement: REDACTED },
|
|
221
|
+
// JSON Web Tokens
|
|
222
|
+
{ pattern: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g, replacement: REDACTED },
|
|
223
|
+
// PEM private keys, whole block
|
|
224
|
+
{ pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, replacement: REDACTED }
|
|
225
|
+
]
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
// Authorization header values: keep the scheme, drop the credential
|
|
229
|
+
anchor: /\b(?:Bearer|Basic|Token|Digest) /,
|
|
230
|
+
rules: [{ pattern: /\b(Bearer|Basic|Token|Digest)\s+[A-Za-z0-9._~+/=-]{12,}/g, replacement: `$1 ${REDACTED}` }]
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
// URLs: a password in the userinfo, a secret in the query string
|
|
234
|
+
anchor: /:\/\/|[?&]/,
|
|
235
|
+
rules: [
|
|
236
|
+
{ pattern: /(:\/\/[^\s/:@]+:)[^\s@/]+(@)/g, replacement: `$1${REDACTED}$2` },
|
|
237
|
+
{
|
|
238
|
+
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,
|
|
239
|
+
replacement: `$1${REDACTED}`
|
|
240
|
+
}
|
|
241
|
+
]
|
|
242
|
+
}
|
|
243
|
+
];
|
|
244
|
+
var STRONG_KEY_HINT = /password|passwd|passphrase|secret|apikey|privatekey|credential|authorization|accesskey|sessionid|cookie|refreshtoken|accesstoken|idtoken|clientkey|signingkey|encryptionkey|masterkey|servicekey/;
|
|
245
|
+
var WEAK_KEY_HINT = /token|key|auth|bearer|cert|pwd/;
|
|
246
|
+
var keyHints = /* @__PURE__ */ new Map();
|
|
247
|
+
var KEY_HINT_CACHE_LIMIT = 4096;
|
|
248
|
+
function hintFor(key) {
|
|
249
|
+
const cached = keyHints.get(key);
|
|
250
|
+
if (cached !== void 0) return cached;
|
|
251
|
+
const reduced = normalizeKeyForMatching(key);
|
|
252
|
+
const hint = STRONG_KEY_HINT.test(reduced) ? "strong" : WEAK_KEY_HINT.test(reduced) ? "weak" : "none";
|
|
253
|
+
if (keyHints.size >= KEY_HINT_CACHE_LIMIT) keyHints.clear();
|
|
254
|
+
keyHints.set(key, hint);
|
|
255
|
+
return hint;
|
|
256
|
+
}
|
|
257
|
+
var MIN_STRONG_LENGTH = 4;
|
|
258
|
+
var MIN_ENTROPY_LENGTH = 16;
|
|
259
|
+
var MIN_ENTROPY_BITS = 3;
|
|
260
|
+
var MIN_STRICT_LENGTH = 32;
|
|
261
|
+
function normalizeKeyForMatching(key) {
|
|
262
|
+
return key.replace(/[^a-zA-Z0-9]/g, "").toLowerCase();
|
|
263
|
+
}
|
|
264
|
+
function entropyOf(text) {
|
|
265
|
+
const counts = /* @__PURE__ */ new Map();
|
|
266
|
+
for (const char of text) counts.set(char, (counts.get(char) ?? 0) + 1);
|
|
267
|
+
let bits = 0;
|
|
268
|
+
for (const count of counts.values()) {
|
|
269
|
+
const p = count / text.length;
|
|
270
|
+
bits -= p * Math.log2(p);
|
|
271
|
+
}
|
|
272
|
+
return bits;
|
|
273
|
+
}
|
|
274
|
+
function looksRandom(value, minimumLength) {
|
|
275
|
+
if (value.length < minimumLength || /\s/.test(value)) return false;
|
|
276
|
+
if (/^[0-9a-f-]+$/i.test(value) || /^[0-9.-]+$/.test(value)) return false;
|
|
277
|
+
return entropyOf(value) >= MIN_ENTROPY_BITS;
|
|
278
|
+
}
|
|
279
|
+
function keyLooksSecret(key, value) {
|
|
280
|
+
if (typeof value !== "string") return false;
|
|
281
|
+
const hint = hintFor(key);
|
|
282
|
+
if (hint === "strong") return value.length >= MIN_STRONG_LENGTH;
|
|
283
|
+
if (hint === "weak") return looksRandom(value, MIN_ENTROPY_LENGTH);
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
var ANY_ANCHOR = new RegExp(RULE_GROUPS.map((group) => group.anchor.source).join("|"));
|
|
287
|
+
function redactString(value, strictness = "balanced") {
|
|
288
|
+
if (strictness === "off" || value.length < 8) return value;
|
|
289
|
+
let result = value;
|
|
290
|
+
if (!ANY_ANCHOR.test(value)) return strictness === "strict" ? redactRandomRuns(value) : value;
|
|
291
|
+
for (const group of RULE_GROUPS) {
|
|
292
|
+
if (!group.anchor.test(result)) continue;
|
|
293
|
+
for (const { pattern, replacement } of group.rules) {
|
|
294
|
+
pattern.lastIndex = 0;
|
|
295
|
+
result = result.replace(pattern, replacement);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return strictness === "strict" ? redactRandomRuns(result) : result;
|
|
299
|
+
}
|
|
300
|
+
function redactRandomRuns(text) {
|
|
301
|
+
return text.replace(
|
|
302
|
+
/(?<![A-Za-z0-9+/=_-])[A-Za-z0-9+/_-]{32,}={0,2}(?![A-Za-z0-9+/=_-])/g,
|
|
303
|
+
(run) => looksRandom(run, MIN_STRICT_LENGTH) && characterClasses(run) >= 3 ? REDACTED : run
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
function characterClasses(text) {
|
|
307
|
+
return [/[a-z]/, /[A-Z]/, /[0-9]/, /[^A-Za-z0-9]/].filter((test) => test.test(text)).length;
|
|
308
|
+
}
|
|
309
|
+
function redactJson(value, options = {}) {
|
|
310
|
+
const strictness = options.strictness ?? "balanced";
|
|
311
|
+
const keys = options.redactKeys;
|
|
312
|
+
const walk = (node) => {
|
|
313
|
+
if (typeof node === "string") return redactString(node, strictness);
|
|
314
|
+
if (Array.isArray(node)) return node.map(walk);
|
|
315
|
+
if (node && typeof node === "object") {
|
|
316
|
+
const out = {};
|
|
317
|
+
for (const [key, child] of Object.entries(node)) {
|
|
318
|
+
if (keys?.has(normalizeKeyForMatching(key)) || strictness !== "off" && keyLooksSecret(key, child)) {
|
|
319
|
+
out[key] = REDACTED;
|
|
320
|
+
} else {
|
|
321
|
+
out[key] = walk(child);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return out;
|
|
325
|
+
}
|
|
326
|
+
return node;
|
|
327
|
+
};
|
|
328
|
+
return walk(value);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// src/normalize.ts
|
|
202
332
|
var DEFAULT_REDACT_KEYS = [
|
|
203
333
|
"password",
|
|
204
334
|
"passphrase",
|
|
@@ -229,7 +359,8 @@ function normalizeValue(input, options = {}) {
|
|
|
229
359
|
redactKeys: new Set(
|
|
230
360
|
(options.redactKeys ?? DEFAULT_REDACT_KEYS).map(normalizeKeyForMatching)
|
|
231
361
|
),
|
|
232
|
-
redact: options.redact ?? true
|
|
362
|
+
redact: options.redact ?? true,
|
|
363
|
+
redactValues: options.redactValues ?? "balanced"
|
|
233
364
|
};
|
|
234
365
|
try {
|
|
235
366
|
return normalizeUnknown(input, resolved, 0, "$", /* @__PURE__ */ new Map());
|
|
@@ -246,7 +377,8 @@ function normalizeError(rawError, options = {}) {
|
|
|
246
377
|
redactKeys: new Set(
|
|
247
378
|
(options.redactKeys ?? DEFAULT_REDACT_KEYS).map(normalizeKeyForMatching)
|
|
248
379
|
),
|
|
249
|
-
redact: options.redact ?? true
|
|
380
|
+
redact: options.redact ?? true,
|
|
381
|
+
redactValues: options.redactValues ?? "balanced"
|
|
250
382
|
};
|
|
251
383
|
return normalizeErrorInternal(rawError, resolved, 0);
|
|
252
384
|
}
|
|
@@ -254,7 +386,7 @@ function normalizeErrorInternal(rawError, options, causeDepth) {
|
|
|
254
386
|
if (!(rawError instanceof Error)) {
|
|
255
387
|
if (isPlainRecord(rawError)) {
|
|
256
388
|
const record = rawError;
|
|
257
|
-
const message = typeof record["message"] === "string" ? record["message"] : void 0;
|
|
389
|
+
const message = typeof record["message"] === "string" ? redactText(record["message"], options) : void 0;
|
|
258
390
|
const name = typeof record["name"] === "string" ? record["name"] : void 0;
|
|
259
391
|
if (message !== void 0 || name !== void 0) {
|
|
260
392
|
return {
|
|
@@ -263,14 +395,14 @@ function normalizeErrorInternal(rawError, options, causeDepth) {
|
|
|
263
395
|
};
|
|
264
396
|
}
|
|
265
397
|
}
|
|
266
|
-
return { message: safeStringify(rawError, options.maxStringLength) };
|
|
398
|
+
return { message: redactText(safeStringify(rawError, options.maxStringLength), options) };
|
|
267
399
|
}
|
|
268
400
|
const normalized = {
|
|
269
401
|
name: rawError.name,
|
|
270
|
-
message: rawError.message
|
|
402
|
+
message: redactText(rawError.message, options)
|
|
271
403
|
};
|
|
272
404
|
if (rawError.stack !== void 0) {
|
|
273
|
-
normalized.stack = truncateString(rawError.stack, options.maxStringLength);
|
|
405
|
+
normalized.stack = truncateString(redactText(rawError.stack, options), options.maxStringLength);
|
|
274
406
|
}
|
|
275
407
|
const cause = rawError.cause;
|
|
276
408
|
if (cause !== void 0) {
|
|
@@ -292,7 +424,7 @@ function normalizeUnknown(value, options, depth, path, ancestors) {
|
|
|
292
424
|
if (value === null) return null;
|
|
293
425
|
const valueType = typeof value;
|
|
294
426
|
if (valueType === "string") {
|
|
295
|
-
return truncateString(value, options.maxStringLength);
|
|
427
|
+
return truncateString(redactText(value, options), options.maxStringLength);
|
|
296
428
|
}
|
|
297
429
|
if (valueType === "number") {
|
|
298
430
|
return Number.isFinite(value) ? value : String(value);
|
|
@@ -486,10 +618,13 @@ function redactOrNormalize(key, value, options, depth, path, ancestors) {
|
|
|
486
618
|
if (options.redact && options.redactKeys.has(normalizeKeyForMatching(key))) {
|
|
487
619
|
return REDACTED;
|
|
488
620
|
}
|
|
621
|
+
if (options.redact && options.redactValues !== "off" && keyLooksSecret(key, value)) {
|
|
622
|
+
return REDACTED;
|
|
623
|
+
}
|
|
489
624
|
return normalizeUnknown(value, options, depth, path, ancestors);
|
|
490
625
|
}
|
|
491
|
-
function
|
|
492
|
-
return
|
|
626
|
+
function redactText(value, options) {
|
|
627
|
+
return options.redact ? redactString(value, options.redactValues) : value;
|
|
493
628
|
}
|
|
494
629
|
function readConstructorName(value) {
|
|
495
630
|
try {
|
|
@@ -1160,16 +1295,293 @@ function dbAudience(insert) {
|
|
|
1160
1295
|
return {
|
|
1161
1296
|
name: "db",
|
|
1162
1297
|
hears: ["story"],
|
|
1163
|
-
accepts: (
|
|
1164
|
-
hear: async (
|
|
1165
|
-
|
|
1166
|
-
|
|
1298
|
+
accepts: (event) => event.level === "Warning" || event.level === "Error",
|
|
1299
|
+
hear: async (event) => {
|
|
1300
|
+
await insert(event);
|
|
1301
|
+
}
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
// src/store/storyStore.ts
|
|
1306
|
+
var DEFAULT_KEY_SET = new Set(DEFAULT_REDACT_KEYS.map(normalizeKeyForMatching));
|
|
1307
|
+
function toStoredStory(event, options = {}) {
|
|
1308
|
+
const { timestamp, level, title, storyId, parentStoryId, origin, notes, durationMs, droppedEmissions, error } = event;
|
|
1309
|
+
const strictness = options.redactValues ?? "balanced";
|
|
1310
|
+
const scrub = (value) => strictness === "off" ? clone(value) : redactJson(clone(value), { redactKeys: DEFAULT_KEY_SET, strictness });
|
|
1311
|
+
const record = {
|
|
1312
|
+
timestamp,
|
|
1313
|
+
level,
|
|
1314
|
+
title: scrub(title),
|
|
1315
|
+
storyId: storyId ?? generateStoryId(),
|
|
1316
|
+
notes: scrub(notes)
|
|
1317
|
+
};
|
|
1318
|
+
if (parentStoryId !== void 0) record.parentStoryId = parentStoryId;
|
|
1319
|
+
if (origin !== void 0) record.origin = scrub(origin);
|
|
1320
|
+
if (durationMs !== void 0) record.durationMs = durationMs;
|
|
1321
|
+
if (droppedEmissions !== void 0) record.droppedEmissions = droppedEmissions;
|
|
1322
|
+
if (error !== void 0) record.error = scrub(error);
|
|
1323
|
+
return record;
|
|
1324
|
+
}
|
|
1325
|
+
function clone(value) {
|
|
1326
|
+
return JSON.parse(JSON.stringify(value));
|
|
1327
|
+
}
|
|
1328
|
+
function generateStoryId() {
|
|
1329
|
+
const random = globalThis.crypto?.randomUUID;
|
|
1330
|
+
if (random) return random.call(globalThis.crypto);
|
|
1331
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
1332
|
+
}
|
|
1333
|
+
function storySearchText(story) {
|
|
1334
|
+
const parts = [story.title];
|
|
1335
|
+
for (const note of story.notes) {
|
|
1336
|
+
parts.push(note.note);
|
|
1337
|
+
for (const value of [note.who, note.what, note.where]) {
|
|
1338
|
+
if (typeof value === "string") parts.push(value);
|
|
1167
1339
|
}
|
|
1340
|
+
if (note.error?.message) parts.push(note.error.message);
|
|
1341
|
+
}
|
|
1342
|
+
if (story.error?.message) parts.push(story.error.message);
|
|
1343
|
+
return parts.join("\n");
|
|
1344
|
+
}
|
|
1345
|
+
function flattenOrigin(origin) {
|
|
1346
|
+
const flat = {};
|
|
1347
|
+
if (!origin) return flat;
|
|
1348
|
+
for (const key of ["who", "what", "where"]) {
|
|
1349
|
+
const value = origin[key];
|
|
1350
|
+
if (value === void 0 || value === null) continue;
|
|
1351
|
+
flat[key] = typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
1352
|
+
}
|
|
1353
|
+
return flat;
|
|
1354
|
+
}
|
|
1355
|
+
function canonicalRow(event) {
|
|
1356
|
+
const story = toStoredStory(event);
|
|
1357
|
+
const origin = flattenOrigin(story.origin);
|
|
1358
|
+
return {
|
|
1359
|
+
story_id: story.storyId,
|
|
1360
|
+
parent_story_id: story.parentStoryId ?? null,
|
|
1361
|
+
timestamp: story.timestamp,
|
|
1362
|
+
level: story.level,
|
|
1363
|
+
title: story.title,
|
|
1364
|
+
origin_who: origin.who ?? null,
|
|
1365
|
+
origin_what: origin.what ?? null,
|
|
1366
|
+
origin_where: origin.where ?? null,
|
|
1367
|
+
duration_ms: story.durationMs ?? null,
|
|
1368
|
+
error_message: story.error?.message ?? null,
|
|
1369
|
+
notes: JSON.stringify(story.notes),
|
|
1370
|
+
search_text: storySearchText(story),
|
|
1371
|
+
record: JSON.stringify(story)
|
|
1168
1372
|
};
|
|
1169
1373
|
}
|
|
1374
|
+
function matchesQuery(story, query = {}) {
|
|
1375
|
+
if (query.since && Date.parse(story.timestamp) < query.since.getTime()) return false;
|
|
1376
|
+
if (query.until && Date.parse(story.timestamp) >= query.until.getTime()) return false;
|
|
1377
|
+
if (query.level !== void 0) {
|
|
1378
|
+
const levels = Array.isArray(query.level) ? query.level : [query.level];
|
|
1379
|
+
if (!levels.includes(story.level)) return false;
|
|
1380
|
+
}
|
|
1381
|
+
if (query.minimumLevel && !meetsLevel(story.level, query.minimumLevel)) return false;
|
|
1382
|
+
if (query.parentStoryId !== void 0 && story.parentStoryId !== query.parentStoryId) return false;
|
|
1383
|
+
if (query.slowerThanMs !== void 0 && !((story.durationMs ?? -1) > query.slowerThanMs)) return false;
|
|
1384
|
+
if (query.failed !== void 0) {
|
|
1385
|
+
const failed = story.level === "Error" || story.error !== void 0;
|
|
1386
|
+
if (failed !== query.failed) return false;
|
|
1387
|
+
}
|
|
1388
|
+
if (query.about) {
|
|
1389
|
+
if (!storySearchText(story).toLowerCase().includes(query.about.toLowerCase())) return false;
|
|
1390
|
+
}
|
|
1391
|
+
if (query.from) {
|
|
1392
|
+
const origin = Object.values(flattenOrigin(story.origin)).join("\n").toLowerCase();
|
|
1393
|
+
if (!origin.includes(query.from.toLowerCase())) return false;
|
|
1394
|
+
}
|
|
1395
|
+
return true;
|
|
1396
|
+
}
|
|
1397
|
+
function applyQuery(stories2, query = {}) {
|
|
1398
|
+
const matched = [];
|
|
1399
|
+
let position = 0;
|
|
1400
|
+
for (const story of stories2) {
|
|
1401
|
+
if (matchesQuery(story, query)) matched.push({ story, position });
|
|
1402
|
+
position++;
|
|
1403
|
+
}
|
|
1404
|
+
const direction = query.order === "oldest" ? 1 : -1;
|
|
1405
|
+
matched.sort((a, b) => {
|
|
1406
|
+
const byTime = Date.parse(a.story.timestamp) - Date.parse(b.story.timestamp);
|
|
1407
|
+
return (byTime !== 0 ? byTime : a.position - b.position) * direction;
|
|
1408
|
+
});
|
|
1409
|
+
const offset = Math.max(0, query.offset ?? 0);
|
|
1410
|
+
const end = query.limit !== void 0 ? offset + Math.max(0, query.limit) : void 0;
|
|
1411
|
+
return matched.slice(offset, end).map((entry) => entry.story);
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
// src/store/memoryStore.ts
|
|
1415
|
+
function memoryStore(options = {}) {
|
|
1416
|
+
const capacity = Math.max(1, options.capacity ?? 1e4);
|
|
1417
|
+
const stories2 = /* @__PURE__ */ new Map();
|
|
1418
|
+
return {
|
|
1419
|
+
get size() {
|
|
1420
|
+
return stories2.size;
|
|
1421
|
+
},
|
|
1422
|
+
clear() {
|
|
1423
|
+
stories2.clear();
|
|
1424
|
+
},
|
|
1425
|
+
async append(event) {
|
|
1426
|
+
const record = toStoredStory(event);
|
|
1427
|
+
stories2.delete(record.storyId);
|
|
1428
|
+
stories2.set(record.storyId, record);
|
|
1429
|
+
while (stories2.size > capacity) {
|
|
1430
|
+
const oldest = stories2.keys().next().value;
|
|
1431
|
+
if (oldest === void 0) break;
|
|
1432
|
+
stories2.delete(oldest);
|
|
1433
|
+
}
|
|
1434
|
+
},
|
|
1435
|
+
async get(storyId) {
|
|
1436
|
+
return stories2.get(storyId);
|
|
1437
|
+
},
|
|
1438
|
+
async query(criteria = {}) {
|
|
1439
|
+
return applyQuery(stories2.values(), criteria);
|
|
1440
|
+
},
|
|
1441
|
+
async children(parentStoryId) {
|
|
1442
|
+
return applyQuery(stories2.values(), { parentStoryId, order: "oldest" });
|
|
1443
|
+
},
|
|
1444
|
+
async prune(before) {
|
|
1445
|
+
const boundary = before.getTime();
|
|
1446
|
+
let removed = 0;
|
|
1447
|
+
for (const [id, story] of stories2) {
|
|
1448
|
+
if (Date.parse(story.timestamp) < boundary) {
|
|
1449
|
+
stories2.delete(id);
|
|
1450
|
+
removed++;
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
return removed;
|
|
1454
|
+
}
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
// src/store/storeAudience.ts
|
|
1459
|
+
function storeAudience(store, options = {}) {
|
|
1460
|
+
return {
|
|
1461
|
+
name: options.name ?? "store",
|
|
1462
|
+
hears: ["story"],
|
|
1463
|
+
accepts: (event) => (options.level === void 0 || meetsLevel(event.level, options.level)) && (options.accepts === void 0 || options.accepts(event)),
|
|
1464
|
+
hear: async (event) => {
|
|
1465
|
+
await store.append(event);
|
|
1466
|
+
}
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
// src/store/stories.ts
|
|
1471
|
+
var UNITS = {
|
|
1472
|
+
ms: 1,
|
|
1473
|
+
s: 1e3,
|
|
1474
|
+
m: 6e4,
|
|
1475
|
+
h: 36e5,
|
|
1476
|
+
d: 864e5,
|
|
1477
|
+
w: 6048e5
|
|
1478
|
+
};
|
|
1479
|
+
function parseDuration(input) {
|
|
1480
|
+
if (typeof input === "number") {
|
|
1481
|
+
if (!Number.isFinite(input) || input < 0) throw new RangeError(`Not a duration: ${input}`);
|
|
1482
|
+
return input;
|
|
1483
|
+
}
|
|
1484
|
+
const match = /^\s*(\d+(?:\.\d+)?)\s*(ms|s|m|h|d|w)\s*$/i.exec(input);
|
|
1485
|
+
if (!match) throw new RangeError(`Not a duration: "${input}" (expected e.g. "30s", "5m", "24h", "7d", "2w")`);
|
|
1486
|
+
return Number(match[1]) * UNITS[match[2].toLowerCase()];
|
|
1487
|
+
}
|
|
1488
|
+
var StoryQueryBuilder = class _StoryQueryBuilder {
|
|
1489
|
+
constructor(store, query, now) {
|
|
1490
|
+
this.store = store;
|
|
1491
|
+
this.query = query;
|
|
1492
|
+
this.now = now;
|
|
1493
|
+
}
|
|
1494
|
+
with(patch) {
|
|
1495
|
+
return new _StoryQueryBuilder(this.store, { ...this.query, ...patch }, this.now);
|
|
1496
|
+
}
|
|
1497
|
+
instant(input) {
|
|
1498
|
+
return input instanceof Date ? input : new Date(this.now().getTime() - parseDuration(input));
|
|
1499
|
+
}
|
|
1500
|
+
/** Title, note text, scalar context or error message mentions this */
|
|
1501
|
+
about(text) {
|
|
1502
|
+
return this.with({ about: text });
|
|
1503
|
+
}
|
|
1504
|
+
/** The origin — who, what or where — mentions this */
|
|
1505
|
+
from(origin) {
|
|
1506
|
+
return this.with({ from: origin });
|
|
1507
|
+
}
|
|
1508
|
+
/** Exactly this level. Accepts the aliases: `"info"`, `"warn"`, `"oops"`. */
|
|
1509
|
+
level(level) {
|
|
1510
|
+
return this.with({ level: toStoryLevel(level) });
|
|
1511
|
+
}
|
|
1512
|
+
/** This level or worse */
|
|
1513
|
+
atLeast(level) {
|
|
1514
|
+
return this.with({ minimumLevel: toStoryLevel(level) });
|
|
1515
|
+
}
|
|
1516
|
+
/** Carried an error, or closed at Error level */
|
|
1517
|
+
failing() {
|
|
1518
|
+
return this.with({ failed: true });
|
|
1519
|
+
}
|
|
1520
|
+
/** Neither an error nor closed at Error level */
|
|
1521
|
+
succeeding() {
|
|
1522
|
+
return this.with({ failed: false });
|
|
1523
|
+
}
|
|
1524
|
+
/** Took longer than this: `"5s"`, `"2m"`, or milliseconds */
|
|
1525
|
+
slowerThan(duration) {
|
|
1526
|
+
return this.with({ slowerThanMs: parseDuration(duration) });
|
|
1527
|
+
}
|
|
1528
|
+
/** Began within this long ago (`"24h"`), or at or after this date */
|
|
1529
|
+
since(when) {
|
|
1530
|
+
return this.with({ since: this.instant(when) });
|
|
1531
|
+
}
|
|
1532
|
+
/** Began before this long ago, or before this date */
|
|
1533
|
+
until(when) {
|
|
1534
|
+
return this.with({ until: this.instant(when) });
|
|
1535
|
+
}
|
|
1536
|
+
/** The chapters of this story */
|
|
1537
|
+
under(parentStoryId) {
|
|
1538
|
+
return this.with({ parentStoryId });
|
|
1539
|
+
}
|
|
1540
|
+
/** Most recent first — the default */
|
|
1541
|
+
newest() {
|
|
1542
|
+
return this.with({ order: "newest" });
|
|
1543
|
+
}
|
|
1544
|
+
/** Earliest first */
|
|
1545
|
+
oldest() {
|
|
1546
|
+
return this.with({ order: "oldest" });
|
|
1547
|
+
}
|
|
1548
|
+
/** At most this many */
|
|
1549
|
+
limit(count) {
|
|
1550
|
+
return this.with({ limit: count });
|
|
1551
|
+
}
|
|
1552
|
+
/** Skip this many first */
|
|
1553
|
+
skip(count) {
|
|
1554
|
+
return this.with({ offset: count });
|
|
1555
|
+
}
|
|
1556
|
+
/** The structured criteria this question compiles to — what an adapter receives */
|
|
1557
|
+
toQuery() {
|
|
1558
|
+
return { ...this.query };
|
|
1559
|
+
}
|
|
1560
|
+
/** Every matching story */
|
|
1561
|
+
all() {
|
|
1562
|
+
return this.store.query(this.toQuery());
|
|
1563
|
+
}
|
|
1564
|
+
/** The first match, or nothing */
|
|
1565
|
+
async first() {
|
|
1566
|
+
const [story] = await this.store.query({ ...this.query, limit: 1 });
|
|
1567
|
+
return story;
|
|
1568
|
+
}
|
|
1569
|
+
/** How many match, ignoring paging */
|
|
1570
|
+
async count() {
|
|
1571
|
+
const { limit: _limit, offset: _offset, ...unpaged } = this.query;
|
|
1572
|
+
return (await this.store.query(unpaged)).length;
|
|
1573
|
+
}
|
|
1574
|
+
/** An awaited builder resolves to `.all()` */
|
|
1575
|
+
then(onFulfilled, onRejected) {
|
|
1576
|
+
return this.all().then(onFulfilled, onRejected);
|
|
1577
|
+
}
|
|
1578
|
+
};
|
|
1579
|
+
function stories(store, options = {}) {
|
|
1580
|
+
return new StoryQueryBuilder(store, {}, options.now ?? (() => /* @__PURE__ */ new Date()));
|
|
1581
|
+
}
|
|
1170
1582
|
|
|
1171
1583
|
// src/report/writeStoryReport.ts
|
|
1172
|
-
function writeStoryReport(
|
|
1584
|
+
function writeStoryReport(stories2, options = {}) {
|
|
1173
1585
|
const {
|
|
1174
1586
|
timezone = Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
1175
1587
|
locale = "en-US",
|
|
@@ -1178,10 +1590,10 @@ function writeStoryReport(stories, options = {}) {
|
|
|
1178
1590
|
showData = true,
|
|
1179
1591
|
colors = true
|
|
1180
1592
|
} = options;
|
|
1181
|
-
if (!
|
|
1593
|
+
if (!stories2.length) {
|
|
1182
1594
|
return "Storyteller Report\n\n(no stories)\n";
|
|
1183
1595
|
}
|
|
1184
|
-
const sorted = [...
|
|
1596
|
+
const sorted = [...stories2].sort(
|
|
1185
1597
|
(storyA, storyB) => Date.parse(storyA.timestamp) - Date.parse(storyB.timestamp)
|
|
1186
1598
|
);
|
|
1187
1599
|
const dateFormatter = new Intl.DateTimeFormat(locale, {
|
|
@@ -1273,23 +1685,34 @@ export {
|
|
|
1273
1685
|
AudienceRegistry,
|
|
1274
1686
|
DEFAULT_REDACT_KEYS,
|
|
1275
1687
|
REDACTED,
|
|
1688
|
+
StoryQueryBuilder,
|
|
1276
1689
|
Storyteller,
|
|
1690
|
+
applyQuery,
|
|
1691
|
+
canonicalRow,
|
|
1277
1692
|
consoleAudience,
|
|
1278
1693
|
dbAudience,
|
|
1694
|
+
flattenOrigin,
|
|
1279
1695
|
formatDuration,
|
|
1280
1696
|
formatOrigin,
|
|
1281
1697
|
formatStory,
|
|
1282
1698
|
getLevelColor,
|
|
1699
|
+
matchesQuery,
|
|
1283
1700
|
meetsLevel,
|
|
1701
|
+
memoryStore,
|
|
1284
1702
|
ndjsonAudience,
|
|
1285
1703
|
normalizeError,
|
|
1286
1704
|
normalizeValue,
|
|
1705
|
+
parseDuration,
|
|
1287
1706
|
readEnvironmentValue,
|
|
1288
1707
|
resolveColors,
|
|
1289
1708
|
resolveMinimumLevel,
|
|
1290
1709
|
resolveOutputFormat,
|
|
1710
|
+
storeAudience,
|
|
1711
|
+
stories,
|
|
1712
|
+
storySearchText,
|
|
1291
1713
|
summarizeContext,
|
|
1292
1714
|
summarizeStory,
|
|
1715
|
+
toStoredStory,
|
|
1293
1716
|
toStoryLevel,
|
|
1294
1717
|
useStoryteller,
|
|
1295
1718
|
writeStoryReport
|