@diegoaltoworks/chatter 0.29.0 → 0.31.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.
@@ -410,6 +410,324 @@ function createWhatsAppChannel(config) {
410
410
  }
411
411
  };
412
412
  }
413
+
414
+ // src/core/guardrails.ts
415
+ function detectLeakage(text) {
416
+ const flags = [
417
+ /system prompt/i,
418
+ /hidden instruction/i,
419
+ /here (are|is) (my|the) rules/i,
420
+ /BEGIN SYSTEM PROMPT/i,
421
+ /tool instructions/i
422
+ ];
423
+ return flags.some((rx) => rx.test(text));
424
+ }
425
+ function scrubOutput(text) {
426
+ return text.replace(/sk-[A-Za-z0-9]{15,}/g, "[REDACTED]").replace(/AIza[0-9A-Za-z_-]{30,}/g, "[REDACTED]");
427
+ }
428
+ function guardOutput(text) {
429
+ if (detectLeakage(text)) {
430
+ return "Sorry, I can't share internal instructions. How else can I help?";
431
+ }
432
+ return scrubOutput(text);
433
+ }
434
+
435
+ // src/core/llm.ts
436
+ var DEFAULT_MODEL = "gpt-4o";
437
+ async function completeOnce({
438
+ client,
439
+ system,
440
+ messages,
441
+ temperature = 0.2,
442
+ model = DEFAULT_MODEL
443
+ }) {
444
+ const res = await client.chat.completions.create({
445
+ model,
446
+ temperature,
447
+ messages: [{ role: "system", content: system }, ...messages]
448
+ });
449
+ const text = res.choices[0]?.message?.content ?? "";
450
+ return {
451
+ content: guardOutput(text),
452
+ usage: res.usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
453
+ };
454
+ }
455
+
456
+ // src/core/answer.ts
457
+ var noUsage = () => ({ prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 });
458
+ async function runAnswerFn(answerFn, { system, messages, mode, sender }) {
459
+ const result = await answerFn({ system, messages, mode, sender });
460
+ const raw = typeof result === "string" ? result : result?.content;
461
+ const usage = typeof result === "string" ? void 0 : result?.usage;
462
+ return {
463
+ content: guardOutput(typeof raw === "string" ? raw : ""),
464
+ usage: usage ?? noUsage()
465
+ };
466
+ }
467
+ async function answerOnce({
468
+ answerFn,
469
+ client,
470
+ system,
471
+ messages,
472
+ mode,
473
+ sender,
474
+ temperature,
475
+ model
476
+ }) {
477
+ if (answerFn) {
478
+ return runAnswerFn(answerFn, { system, messages, mode, sender });
479
+ }
480
+ return completeOnce({ client, system, messages, temperature, model });
481
+ }
482
+
483
+ // src/core/buckets.ts
484
+ function defaultBuckets(mode) {
485
+ return ["base", mode];
486
+ }
487
+ function isIdentified(sender) {
488
+ return typeof sender === "string" && sender.trim().length > 0;
489
+ }
490
+ function normalize(buckets) {
491
+ const out = [];
492
+ for (const raw of buckets) {
493
+ if (typeof raw !== "string") continue;
494
+ const bucket = raw.trim();
495
+ if (bucket && !out.includes(bucket)) out.push(bucket);
496
+ }
497
+ return out;
498
+ }
499
+ async function resolveBuckets({
500
+ mode,
501
+ sender,
502
+ bucketsFor
503
+ }) {
504
+ if (!bucketsFor) return void 0;
505
+ const identified = isIdentified(sender);
506
+ const requested = await bucketsFor(identified ? { mode, sender } : { mode });
507
+ if (!Array.isArray(requested)) return void 0;
508
+ const cleaned = normalize(requested);
509
+ if (identified) return cleaned;
510
+ const ceiling = new Set(defaultBuckets(mode));
511
+ return cleaned.filter((bucket) => ceiling.has(bucket));
512
+ }
513
+
514
+ // src/core/pipeline.ts
515
+ var MODE_SETTINGS = {
516
+ public: { topK: 6, label: "Context" },
517
+ private: { topK: 8, label: "Internal Context" }
518
+ };
519
+ async function prepareChat({
520
+ store,
521
+ prompts,
522
+ mode,
523
+ messages,
524
+ personaLayer,
525
+ channelHint,
526
+ buckets
527
+ }) {
528
+ const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
529
+ if (!lastUserMsg) {
530
+ throw new Error("no user message found in conversation");
531
+ }
532
+ const { topK, label } = MODE_SETTINGS[mode];
533
+ const ctx = await store.query(lastUserMsg.content, topK, buckets ?? defaultBuckets(mode));
534
+ const persona = personaLayer?.trim() || (mode === "private" ? prompts.privatePersona : prompts.publicPersona);
535
+ const hint = channelHint?.trim();
536
+ const system = [
537
+ prompts.baseSystemRules,
538
+ persona,
539
+ ...hint ? [hint] : [],
540
+ `${label}:
541
+ ${ctx.join("\n\n")}`
542
+ ].join("\n\n");
543
+ return { system, messages };
544
+ }
545
+
546
+ // src/channels/gates.ts
547
+ function matches(re, text) {
548
+ return new RegExp(re.source, re.flags.replace(/[gy]/g, "")).test(text);
549
+ }
550
+ function decideChannelAction(msg, config) {
551
+ if (msg.fromBot || !msg.text.trim()) return "ignore";
552
+ if (!msg.isDirectMessage) {
553
+ if (config.allowedChats.length > 0 && !config.allowedChats.includes(msg.chatId)) {
554
+ return "ignore";
555
+ }
556
+ if (config.muteRegex && matches(config.muteRegex, msg.text)) return "mute";
557
+ if (config.unmuteRegex && matches(config.unmuteRegex, msg.text)) return "unmute";
558
+ if (config.mutedChats.has(msg.chatId)) return "ignore";
559
+ return msg.mentionsBot || msg.isReplyToBot ? "reply" : "ignore";
560
+ }
561
+ return "reply";
562
+ }
563
+ function createSlidingWindowRateLimiter(maxPerWindow, windowMs, now = Date.now) {
564
+ const log = /* @__PURE__ */ new Map();
565
+ return function allow(key) {
566
+ const cutoff = now() - windowMs;
567
+ const entries = (log.get(key) ?? []).filter((t) => t > cutoff);
568
+ if (entries.length >= maxPerWindow) {
569
+ if (entries.length > 0) log.set(key, entries);
570
+ else log.delete(key);
571
+ return false;
572
+ }
573
+ entries.push(now());
574
+ log.set(key, entries);
575
+ return true;
576
+ };
577
+ }
578
+ function underReplyRateLimit(msg, limiters) {
579
+ return msg.isDirectMessage ? limiters.dm(msg.chatId) : limiters.group(msg.chatId);
580
+ }
581
+ function isFromAnySession(id, registry) {
582
+ if (!id) return false;
583
+ for (const ids of registry.values()) {
584
+ if (ids.includes(id)) return true;
585
+ }
586
+ return false;
587
+ }
588
+ function isEffectivelyFromSelf(msg, registry) {
589
+ return msg.fromBot || isFromAnySession(msg.senderId, registry);
590
+ }
591
+
592
+ // src/channels/whatsapp/inbound.ts
593
+ function isGroupJid(jid) {
594
+ return jid.endsWith("@g.us");
595
+ }
596
+ function jidsMatch(a, b) {
597
+ if (!a || !b) return false;
598
+ const bare = (jid) => jid.split("@")[0]?.split(":")[0] ?? jid;
599
+ return bare(a) === bare(b);
600
+ }
601
+ function jidToPhoneNumber(jid) {
602
+ const bare = jid.split("@")[0]?.split(":")[0] ?? "";
603
+ return bare.startsWith("+") ? bare : `+${bare}`;
604
+ }
605
+ function normalizeJid(jid) {
606
+ const [user = "", domain] = jid.split("@");
607
+ const bareUser = user.split(":")[0] ?? user;
608
+ return domain ? `${bareUser}@${domain}` : bareUser;
609
+ }
610
+ async function senderPhoneFor(sock, message, chatJid) {
611
+ const key = message.key;
612
+ let jid = [key.participantPn, key.senderPn, key.participant, key.remoteJidAlt, chatJid].find(
613
+ (candidate) => typeof candidate === "string" && candidate.length > 0
614
+ ) ?? chatJid;
615
+ if (jid.endsWith("@lid")) {
616
+ try {
617
+ const mapped = await sock.signalRepository?.lidMapping?.getPNForLID?.(jid);
618
+ if (mapped) jid = mapped;
619
+ } catch {
620
+ }
621
+ }
622
+ return jid.endsWith("@lid") ? `lid:${jid.split("@")[0]?.split(":")[0] ?? jid}` : jidToPhoneNumber(jid);
623
+ }
624
+ function messageContext(message) {
625
+ const content = message.message;
626
+ const contextInfo = content?.extendedTextMessage?.contextInfo ?? content?.imageMessage?.contextInfo ?? content?.videoMessage?.contextInfo ?? content?.documentMessage?.contextInfo;
627
+ return {
628
+ mentionedJids: contextInfo?.mentionedJid ?? [],
629
+ quotedParticipantJid: contextInfo?.participant ?? void 0
630
+ };
631
+ }
632
+ function extractText(message) {
633
+ const content = message.message;
634
+ if (!content) return "";
635
+ return content.conversation ?? content.extendedTextMessage?.text ?? content.imageMessage?.caption ?? content.videoMessage?.caption ?? content.documentMessage?.caption ?? "";
636
+ }
637
+ var DEFAULT_GROUP_RATE_LIMIT = { max: 30, windowMs: 60 * 60 * 1e3 };
638
+ var DEFAULT_DM_RATE_LIMIT = { max: 20, windowMs: 60 * 60 * 1e3 };
639
+ async function resolvePersonaLayer(config, ctx) {
640
+ if (!config.personaResolver) return void 0;
641
+ try {
642
+ return await config.personaResolver(ctx);
643
+ } catch {
644
+ return void 0;
645
+ }
646
+ }
647
+ function createWhatsAppInboundHandler(config) {
648
+ const mutedChats = /* @__PURE__ */ new Set();
649
+ const now = config.now ?? Date.now;
650
+ const group = config.groupRateLimit ?? DEFAULT_GROUP_RATE_LIMIT;
651
+ const dm = config.dmRateLimit ?? DEFAULT_DM_RATE_LIMIT;
652
+ const underGroupRateLimit = createSlidingWindowRateLimiter(group.max, group.windowMs, now);
653
+ const underDmRateLimit = createSlidingWindowRateLimiter(dm.max, dm.windowMs, now);
654
+ return async function handleWhatsAppMessage(event) {
655
+ const { sessionId, sock, message } = event;
656
+ try {
657
+ const chatId = message.key.remoteJid ?? "";
658
+ if (!chatId || chatId === "status@broadcast") return;
659
+ const text = extractText(message);
660
+ const context = messageContext(message);
661
+ const senderId = message.key.participant ?? chatId;
662
+ const ownIds = [sock.user?.id, sock.user?.lid].filter((id) => Boolean(id)).map(normalizeJid);
663
+ config.registry.set(sessionId, ownIds);
664
+ const fromBot = isEffectivelyFromSelf(
665
+ { fromBot: message.key.fromMe ?? false, senderId: normalizeJid(senderId) },
666
+ config.registry
667
+ );
668
+ const msg = {
669
+ chatId,
670
+ senderId,
671
+ text,
672
+ isDirectMessage: !isGroupJid(chatId),
673
+ mentionsBot: context.mentionedJids.some((jid) => ownIds.some((own) => jidsMatch(jid, own))),
674
+ isReplyToBot: ownIds.some((own) => jidsMatch(context.quotedParticipantJid, own)),
675
+ fromBot
676
+ };
677
+ const action = decideChannelAction(msg, {
678
+ allowedChats: config.allowedChats ?? [],
679
+ mutedChats,
680
+ muteRegex: config.muteRegex,
681
+ unmuteRegex: config.unmuteRegex
682
+ });
683
+ if (action === "mute") {
684
+ mutedChats.add(chatId);
685
+ if (config.muteReply) await sock.sendMessage(chatId, { text: config.muteReply });
686
+ return;
687
+ }
688
+ if (action === "unmute") {
689
+ mutedChats.delete(chatId);
690
+ if (config.unmuteReply) await sock.sendMessage(chatId, { text: config.unmuteReply });
691
+ return;
692
+ }
693
+ if (action !== "reply") return;
694
+ if (!underReplyRateLimit(msg, { dm: underDmRateLimit, group: underGroupRateLimit })) {
695
+ return;
696
+ }
697
+ const senderPhone = await senderPhoneFor(sock, message, chatId);
698
+ const personaLayer = await resolvePersonaLayer(config, { senderPhone, text });
699
+ const buckets = await resolveBuckets({
700
+ mode: "public",
701
+ sender: senderPhone,
702
+ bucketsFor: config.bucketsFor
703
+ });
704
+ const messages = [{ role: "user", content: text }];
705
+ const { system } = await prepareChat({
706
+ store: config.store,
707
+ prompts: config.prompts,
708
+ mode: "public",
709
+ messages,
710
+ personaLayer,
711
+ channelHint: config.channelHint,
712
+ buckets
713
+ });
714
+ const { content } = await answerOnce({
715
+ answerFn: config.answerFn,
716
+ client: config.client,
717
+ system,
718
+ messages,
719
+ mode: "public",
720
+ sender: senderPhone,
721
+ model: config.model
722
+ });
723
+ if (content) {
724
+ await sock.sendMessage(chatId, { text: content }, { quoted: message });
725
+ }
726
+ } catch (error) {
727
+ console.warn(`WhatsApp[${sessionId}]: inbound message handling failed:`, error);
728
+ }
729
+ };
730
+ }
413
731
  export {
414
732
  LEASE_HEARTBEAT_MS,
415
733
  LEASE_STALE_MS,
@@ -418,11 +736,18 @@ export {
418
736
  canAcquireLease,
419
737
  createTursoWaLeaseStore,
420
738
  createWhatsAppChannel,
739
+ createWhatsAppInboundHandler,
421
740
  decrypt,
422
741
  encrypt,
742
+ extractText,
743
+ isGroupJid,
744
+ jidToPhoneNumber,
745
+ jidsMatch,
423
746
  loadBaileys,
747
+ messageContext,
424
748
  reconnectDelayMs,
425
749
  senderNameFor,
750
+ senderPhoneFor,
426
751
  shutdownWaSessions,
427
752
  useTursoAuthState
428
753
  };
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Named greetings for known contacts — rides the same persona window as the
3
+ * chat that follows, so a call greeted in one persona/language continues in
4
+ * it. Ships no content: templates are entirely caller-supplied.
5
+ */
6
+ import type { PersonaResolver } from "./resolver";
7
+ export interface GreeterOptions {
8
+ resolver: PersonaResolver;
9
+ /** Greeting templates keyed by persona id, each containing a `{name}` placeholder. */
10
+ templates: Record<string, readonly string[]>;
11
+ /** Injectable randomness for tests. Default `Math.random`. */
12
+ random?: () => number;
13
+ }
14
+ export interface Greeter {
15
+ /** A named greeting for a known contact in their current persona, or null (unknown contact, or no template pool for that persona). */
16
+ greet(contactId: string): string | null;
17
+ }
18
+ export declare function createGreeter({ resolver, templates, random, }: GreeterOptions): Greeter;
19
+ //# sourceMappingURL=greeter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"greeter.d.ts","sourceRoot":"","sources":["../../src/personas/greeter.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,eAAe,CAAC;IAC1B,sFAAsF;IACtF,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC;IAC7C,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,OAAO;IACtB,sIAAsI;IACtI,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;CACzC;AAMD,wBAAgB,aAAa,CAAC,EAC5B,QAAQ,EACR,SAAS,EACT,MAAoB,GACrB,EAAE,cAAc,GAAG,OAAO,CAmB1B"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=greeter.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"greeter.test.d.ts","sourceRoot":"","sources":["../../src/personas/greeter.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Persona machinery — dynamic prompt layers and named greetings driven by a
3
+ * JSON registry. Published as the `@diegoaltoworks/chatter/personas` subpath
4
+ * so the core install stays untouched by it; this module adds no required
5
+ * dependencies.
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+ export type { Greeter, GreeterOptions } from "./greeter";
10
+ export { createGreeter } from "./greeter";
11
+ export type { PersonaContact, PersonaDefinition, PersonaRegistry, PersonaResolver, PersonaResolverOptions, } from "./resolver";
12
+ export { createPersonaResolver } from "./resolver";
13
+ export { timeContext } from "./timeContext";
14
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/personas/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,YAAY,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC1C,YAAY,EACV,cAAc,EACd,iBAAiB,EACjB,eAAe,EACf,eAAe,EACf,sBAAsB,GACvB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC"}
@@ -0,0 +1,206 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/personas/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ createGreeter: () => createGreeter,
24
+ createPersonaResolver: () => createPersonaResolver,
25
+ timeContext: () => timeContext
26
+ });
27
+ module.exports = __toCommonJS(index_exports);
28
+
29
+ // src/personas/greeter.ts
30
+ function capitalize(name) {
31
+ return name.charAt(0).toUpperCase() + name.slice(1);
32
+ }
33
+ function createGreeter({
34
+ resolver,
35
+ templates,
36
+ random = Math.random
37
+ }) {
38
+ return {
39
+ greet(contactId) {
40
+ try {
41
+ const contact = resolver.contactFor(contactId);
42
+ if (!contact?.name) return null;
43
+ const personaId = resolver.rollPersonaId(contactId) ?? resolver.defaultPersonaId();
44
+ const pool = personaId ? templates[personaId] : void 0;
45
+ if (!pool || pool.length === 0) return null;
46
+ const template = pool[Math.floor(random() * pool.length)];
47
+ if (!template) return null;
48
+ return template.replaceAll("{name}", capitalize(contact.name));
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
53
+ };
54
+ }
55
+
56
+ // src/personas/resolver.ts
57
+ var import_node_fs = require("node:fs");
58
+ var import_node_path = require("node:path");
59
+ var DEFAULT_WINDOW_MINUTES = 10;
60
+ var DEFAULT_PROBABILITY = 0.5;
61
+ function interpolate(template, vars) {
62
+ return Object.entries(vars).reduce(
63
+ (acc, [key, value]) => acc.replaceAll(`{{${key}}}`, value),
64
+ template
65
+ );
66
+ }
67
+ function createPersonaResolver(options = {}) {
68
+ if (!options.registry && !options.registryPath) {
69
+ throw new Error("createPersonaResolver requires a registry or registryPath");
70
+ }
71
+ const random = options.random ?? Math.random;
72
+ const now = options.now ?? Date.now;
73
+ const vars = options.vars ?? {};
74
+ const promptsDir = options.promptsDir ?? (options.registryPath ? (0, import_node_path.dirname)(options.registryPath) : process.cwd());
75
+ let registry = options.registry;
76
+ let registryLoadError;
77
+ const promptCache = /* @__PURE__ */ new Map();
78
+ const window = /* @__PURE__ */ new Map();
79
+ const warned = /* @__PURE__ */ new Set();
80
+ function warnOnce(kind, message, error) {
81
+ if (warned.has(kind)) return;
82
+ warned.add(kind);
83
+ console.warn(message, error);
84
+ }
85
+ function loadRegistry() {
86
+ if (registry) return registry;
87
+ if (registryLoadError !== void 0) throw registryLoadError;
88
+ try {
89
+ registry = JSON.parse((0, import_node_fs.readFileSync)(options.registryPath, "utf-8"));
90
+ return registry;
91
+ } catch (error) {
92
+ registryLoadError = error;
93
+ warnOnce(
94
+ "registry",
95
+ `Persona registry load failed (${options.registryPath}); falling back to the default persona for every contact.`,
96
+ error
97
+ );
98
+ throw error;
99
+ }
100
+ }
101
+ function contactFor(contactId) {
102
+ try {
103
+ return loadRegistry().contacts?.[contactId];
104
+ } catch {
105
+ return void 0;
106
+ }
107
+ }
108
+ function rollPersonaId(contactId) {
109
+ try {
110
+ const reg = loadRegistry();
111
+ const nowMs = now();
112
+ const existing = window.get(contactId);
113
+ if (existing && existing.expiresAt > nowMs) {
114
+ return existing.personaId;
115
+ }
116
+ for (const [key, value] of window) {
117
+ if (value.expiresAt <= nowMs) window.delete(key);
118
+ }
119
+ const contact = reg.contacts?.[contactId];
120
+ let personaId = null;
121
+ const probability = contact?.probability ?? reg.personaProbability ?? DEFAULT_PROBABILITY;
122
+ if (contact && random() < probability) {
123
+ personaId = contact.persona === reg.defaultPersona ? null : contact.persona;
124
+ }
125
+ const windowMs = (reg.personaWindowMinutes ?? DEFAULT_WINDOW_MINUTES) * 60 * 1e3;
126
+ window.set(contactId, { personaId, expiresAt: nowMs + windowMs });
127
+ return personaId;
128
+ } catch {
129
+ return null;
130
+ }
131
+ }
132
+ function personaLayerFor(personaId) {
133
+ if (!personaId) return null;
134
+ try {
135
+ const definition = loadRegistry().personas[personaId];
136
+ if (!definition) return null;
137
+ if (promptCache.has(personaId)) return promptCache.get(personaId) ?? null;
138
+ try {
139
+ const prompt = interpolate(
140
+ (0, import_node_fs.readFileSync)((0, import_node_path.resolve)(promptsDir, definition.prompt), "utf-8"),
141
+ vars
142
+ );
143
+ promptCache.set(personaId, prompt);
144
+ return prompt;
145
+ } catch (error) {
146
+ promptCache.set(personaId, null);
147
+ warnOnce(
148
+ `prompt:${personaId}`,
149
+ `Persona prompt load failed for "${personaId}" (${definition.prompt}); falling back to the default persona.`,
150
+ error
151
+ );
152
+ return null;
153
+ }
154
+ } catch {
155
+ return null;
156
+ }
157
+ }
158
+ function defaultPersonaId() {
159
+ try {
160
+ return loadRegistry().defaultPersona ?? null;
161
+ } catch {
162
+ return null;
163
+ }
164
+ }
165
+ function resolvePersonaLayer(contactId) {
166
+ return personaLayerFor(rollPersonaId(contactId)) ?? personaLayerFor(defaultPersonaId());
167
+ }
168
+ return {
169
+ rollPersonaId,
170
+ resolvePersonaLayer,
171
+ personaLayerFor,
172
+ contactFor,
173
+ defaultPersonaId
174
+ };
175
+ }
176
+
177
+ // src/personas/timeContext.ts
178
+ function timeContext(zones, now = Date.now()) {
179
+ if (zones.length === 0) return "";
180
+ const at = new Date(now);
181
+ const parts = zones.flatMap((zone) => {
182
+ try {
183
+ const formatted = new Intl.DateTimeFormat("en-GB", {
184
+ timeZone: zone,
185
+ weekday: "long",
186
+ day: "numeric",
187
+ month: "long",
188
+ year: "numeric",
189
+ hour: "2-digit",
190
+ minute: "2-digit",
191
+ hour12: false
192
+ }).format(at);
193
+ return [`${formatted} (${zone})`];
194
+ } catch {
195
+ return [];
196
+ }
197
+ });
198
+ if (parts.length === 0) return "";
199
+ return `Current date and time: ${parts.join(" / ")}.`;
200
+ }
201
+ // Annotate the CommonJS export names for ESM import in node:
202
+ 0 && (module.exports = {
203
+ createGreeter,
204
+ createPersonaResolver,
205
+ timeContext
206
+ });