@copilotkit/channels-whatsapp 0.0.1

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.
Files changed (90) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +236 -0
  3. package/dist/adapter.d.ts +57 -0
  4. package/dist/adapter.d.ts.map +1 -0
  5. package/dist/adapter.js +242 -0
  6. package/dist/adapter.test.d.ts +2 -0
  7. package/dist/adapter.test.d.ts.map +1 -0
  8. package/dist/adapter.test.js +137 -0
  9. package/dist/built-in-context.d.ts +7 -0
  10. package/dist/built-in-context.d.ts.map +1 -0
  11. package/dist/built-in-context.js +19 -0
  12. package/dist/built-in-context.test.d.ts +2 -0
  13. package/dist/built-in-context.test.d.ts.map +1 -0
  14. package/dist/built-in-context.test.js +17 -0
  15. package/dist/built-in-tools.d.ts +8 -0
  16. package/dist/built-in-tools.d.ts.map +1 -0
  17. package/dist/built-in-tools.js +6 -0
  18. package/dist/client.d.ts +40 -0
  19. package/dist/client.d.ts.map +1 -0
  20. package/dist/client.js +108 -0
  21. package/dist/client.test.d.ts +2 -0
  22. package/dist/client.test.d.ts.map +1 -0
  23. package/dist/client.test.js +108 -0
  24. package/dist/conversation-store.d.ts +23 -0
  25. package/dist/conversation-store.d.ts.map +1 -0
  26. package/dist/conversation-store.js +41 -0
  27. package/dist/conversation-store.test.d.ts +2 -0
  28. package/dist/conversation-store.test.d.ts.map +1 -0
  29. package/dist/conversation-store.test.js +40 -0
  30. package/dist/download-files.d.ts +44 -0
  31. package/dist/download-files.d.ts.map +1 -0
  32. package/dist/download-files.js +66 -0
  33. package/dist/download-files.test.d.ts +2 -0
  34. package/dist/download-files.test.d.ts.map +1 -0
  35. package/dist/download-files.test.js +34 -0
  36. package/dist/event-renderer.d.ts +18 -0
  37. package/dist/event-renderer.d.ts.map +1 -0
  38. package/dist/event-renderer.js +110 -0
  39. package/dist/event-renderer.test.d.ts +2 -0
  40. package/dist/event-renderer.test.d.ts.map +1 -0
  41. package/dist/event-renderer.test.js +68 -0
  42. package/dist/history-store.d.ts +36 -0
  43. package/dist/history-store.d.ts.map +1 -0
  44. package/dist/history-store.js +17 -0
  45. package/dist/history-store.test.d.ts +2 -0
  46. package/dist/history-store.test.d.ts.map +1 -0
  47. package/dist/history-store.test.js +24 -0
  48. package/dist/index.d.ts +18 -0
  49. package/dist/index.d.ts.map +1 -0
  50. package/dist/index.js +12 -0
  51. package/dist/interaction.d.ts +19 -0
  52. package/dist/interaction.d.ts.map +1 -0
  53. package/dist/interaction.js +56 -0
  54. package/dist/interaction.test.d.ts +2 -0
  55. package/dist/interaction.test.d.ts.map +1 -0
  56. package/dist/interaction.test.js +67 -0
  57. package/dist/markdown-to-wa.d.ts +15 -0
  58. package/dist/markdown-to-wa.d.ts.map +1 -0
  59. package/dist/markdown-to-wa.js +56 -0
  60. package/dist/markdown-to-wa.test.d.ts +2 -0
  61. package/dist/markdown-to-wa.test.d.ts.map +1 -0
  62. package/dist/markdown-to-wa.test.js +29 -0
  63. package/dist/render/budget.d.ts +34 -0
  64. package/dist/render/budget.d.ts.map +1 -0
  65. package/dist/render/budget.js +40 -0
  66. package/dist/render/budget.test.d.ts +2 -0
  67. package/dist/render/budget.test.d.ts.map +1 -0
  68. package/dist/render/budget.test.js +24 -0
  69. package/dist/render/message.d.ts +74 -0
  70. package/dist/render/message.d.ts.map +1 -0
  71. package/dist/render/message.js +247 -0
  72. package/dist/render/message.test.d.ts +2 -0
  73. package/dist/render/message.test.d.ts.map +1 -0
  74. package/dist/render/message.test.js +184 -0
  75. package/dist/types.d.ts +107 -0
  76. package/dist/types.d.ts.map +1 -0
  77. package/dist/types.js +1 -0
  78. package/dist/webhook-listener.d.ts +16 -0
  79. package/dist/webhook-listener.d.ts.map +1 -0
  80. package/dist/webhook-listener.js +128 -0
  81. package/dist/webhook-listener.test.d.ts +2 -0
  82. package/dist/webhook-listener.test.d.ts.map +1 -0
  83. package/dist/webhook-listener.test.js +204 -0
  84. package/dist/webhook-server.d.ts +23 -0
  85. package/dist/webhook-server.d.ts.map +1 -0
  86. package/dist/webhook-server.js +99 -0
  87. package/dist/webhook-server.test.d.ts +2 -0
  88. package/dist/webhook-server.test.d.ts.map +1 -0
  89. package/dist/webhook-server.test.js +150 -0
  90. package/package.json +55 -0
@@ -0,0 +1,66 @@
1
+ const DEFAULTS = {
2
+ maxBytesPerFile: 8 * 1024 * 1024,
3
+ maxFiles: 5,
4
+ maxTextBytes: 200 * 1024,
5
+ };
6
+ const TEXT_MIME_PREFIXES = ["text/"];
7
+ const TEXT_MIME_EXACT = new Set([
8
+ "application/json",
9
+ "application/csv",
10
+ "application/xml",
11
+ "application/x-ndjson",
12
+ "application/yaml",
13
+ ]);
14
+ function isText(mime) {
15
+ return (TEXT_MIME_PREFIXES.some((p) => mime.startsWith(p)) ||
16
+ TEXT_MIME_EXACT.has(mime));
17
+ }
18
+ function mediaKind(mime) {
19
+ if (mime.startsWith("image/"))
20
+ return "image";
21
+ if (mime.startsWith("audio/"))
22
+ return "audio";
23
+ if (mime.startsWith("video/"))
24
+ return "video";
25
+ return "document";
26
+ }
27
+ /** Download inbound media via the client and build AG-UI content parts. */
28
+ export async function buildFileContentParts(media, client, config) {
29
+ const cfg = { ...DEFAULTS, ...config };
30
+ const parts = [];
31
+ const notes = [];
32
+ // WhatsApp delivers one media object per inbound message, so the ingress path
33
+ // passes a single-element array today. The maxFiles clamp/overflow note exists
34
+ // for callers that batch multiple media (e.g. future album support).
35
+ for (const m of media.slice(0, cfg.maxFiles)) {
36
+ let dl;
37
+ try {
38
+ dl = await client.downloadMedia(m.id);
39
+ }
40
+ catch (err) {
41
+ notes.push(`failed to download ${m.id}: ${err.message}`);
42
+ continue;
43
+ }
44
+ if (dl.bytes.byteLength > cfg.maxBytesPerFile) {
45
+ notes.push(`media ${m.id} too large (${dl.bytes.byteLength} bytes)`);
46
+ continue;
47
+ }
48
+ const mime = m.mime_type ?? dl.mimeType;
49
+ if (isText(mime)) {
50
+ const slice = dl.bytes.slice(0, cfg.maxTextBytes);
51
+ parts.push({ type: "text", text: new TextDecoder().decode(slice) });
52
+ continue;
53
+ }
54
+ parts.push({
55
+ type: mediaKind(mime),
56
+ source: { type: "data", value: base64(dl.bytes), mimeType: mime },
57
+ });
58
+ }
59
+ if (media.length > cfg.maxFiles) {
60
+ notes.push(`dropped ${media.length - cfg.maxFiles} extra media (limit ${cfg.maxFiles})`);
61
+ }
62
+ return { parts, notes };
63
+ }
64
+ function base64(bytes) {
65
+ return Buffer.from(bytes).toString("base64");
66
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=download-files.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"download-files.test.d.ts","sourceRoot":"","sources":["../src/download-files.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,34 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { buildFileContentParts } from "./download-files.js";
3
+ const fakeClient = (media) => ({
4
+ downloadMedia: async (id) => media[id],
5
+ });
6
+ describe("buildFileContentParts", () => {
7
+ it("converts an image media into an image data part", async () => {
8
+ const client = fakeClient({
9
+ M1: { bytes: new Uint8Array([1, 2, 3]), mimeType: "image/png" },
10
+ });
11
+ const { parts, notes } = await buildFileContentParts([{ id: "M1", mime_type: "image/png" }], client, {});
12
+ expect(notes).toEqual([]);
13
+ expect(parts).toHaveLength(1);
14
+ expect(parts[0]).toMatchObject({
15
+ type: "image",
16
+ source: { type: "data", mimeType: "image/png" },
17
+ });
18
+ });
19
+ it("decodes a text media into a text part", async () => {
20
+ const client = fakeClient({
21
+ M2: { bytes: new TextEncoder().encode("hello"), mimeType: "text/plain" },
22
+ });
23
+ const { parts } = await buildFileContentParts([{ id: "M2", mime_type: "text/plain" }], client, {});
24
+ expect(parts[0]).toEqual({ type: "text", text: "hello" });
25
+ });
26
+ it("skips an oversized file with a note", async () => {
27
+ const client = fakeClient({
28
+ M3: { bytes: new Uint8Array(10), mimeType: "image/png" },
29
+ });
30
+ const { parts, notes } = await buildFileContentParts([{ id: "M3", mime_type: "image/png" }], client, { maxBytesPerFile: 5 });
31
+ expect(parts).toEqual([]);
32
+ expect(notes[0]).toMatch(/too large/i);
33
+ });
34
+ });
@@ -0,0 +1,18 @@
1
+ import type { RunRenderer } from "@copilotkit/channels";
2
+ export interface RunRendererArgs {
3
+ /** Send a finished text message to the conversation (the adapter wires the Cloud API client). */
4
+ send: (text: string) => Promise<void>;
5
+ /** Persist the assistant's final text to the HistoryStore. */
6
+ onAssistantText?: (text: string) => void;
7
+ /** Custom-event names treated as interrupts. Defaults to {"on_interrupt"}. */
8
+ interruptEventNames?: ReadonlySet<string>;
9
+ }
10
+ /**
11
+ * Buffered run renderer for WhatsApp. Unlike Slack's streaming renderer, this
12
+ * accumulates text deltas and sends the full message only on TEXT_MESSAGE_END
13
+ * (WhatsApp messages are immutable — there is no chat.update). Tool-call and
14
+ * interrupt capture mirror the Slack renderer so the engine's run-loop reads
15
+ * them identically.
16
+ */
17
+ export declare function createRunRenderer(args: RunRendererArgs): RunRenderer;
18
+ //# sourceMappingURL=event-renderer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-renderer.d.ts","sourceRoot":"","sources":["../src/event-renderer.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,WAAW,EAGZ,MAAM,sBAAsB,CAAC;AAI9B,MAAM,WAAW,eAAe;IAC9B,iGAAiG;IACjG,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,8DAA8D;IAC9D,eAAe,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC,8EAA8E;IAC9E,mBAAmB,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CAC3C;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,eAAe,GAAG,WAAW,CA2GpE"}
@@ -0,0 +1,110 @@
1
+ const INTERRUPTED_SUFFIX = "\n_(interrupted)_";
2
+ /**
3
+ * Buffered run renderer for WhatsApp. Unlike Slack's streaming renderer, this
4
+ * accumulates text deltas and sends the full message only on TEXT_MESSAGE_END
5
+ * (WhatsApp messages are immutable — there is no chat.update). Tool-call and
6
+ * interrupt capture mirror the Slack renderer so the engine's run-loop reads
7
+ * them identically.
8
+ */
9
+ export function createRunRenderer(args) {
10
+ const interruptEventNames = args.interruptEventNames ?? new Set(["on_interrupt"]);
11
+ const buffers = new Map();
12
+ const finalised = new Set();
13
+ const capturedToolCalls = [];
14
+ let pendingInterrupt;
15
+ let aborted = false;
16
+ const captureToolCall = (toolCallId, toolCallName, toolCallArgs) => {
17
+ const existing = capturedToolCalls.find((c) => c.toolCallId === toolCallId);
18
+ if (existing) {
19
+ existing.toolCallName = toolCallName;
20
+ existing.toolCallArgs = toolCallArgs;
21
+ }
22
+ else {
23
+ capturedToolCalls.push({ toolCallId, toolCallName, toolCallArgs });
24
+ }
25
+ };
26
+ const flush = async (text) => {
27
+ if (!text)
28
+ return;
29
+ await args.send(text);
30
+ args.onAssistantText?.(text);
31
+ };
32
+ const subscriber = {
33
+ onTextMessageStartEvent({ event }) {
34
+ if (aborted)
35
+ return;
36
+ buffers.set(event.messageId, "");
37
+ },
38
+ onTextMessageContentEvent({ event }) {
39
+ if (aborted || finalised.has(event.messageId))
40
+ return;
41
+ buffers.set(event.messageId, (buffers.get(event.messageId) ?? "") + (event.delta ?? ""));
42
+ },
43
+ async onTextMessageEndEvent({ event }) {
44
+ if (aborted || finalised.has(event.messageId))
45
+ return;
46
+ const text = buffers.get(event.messageId) ?? "";
47
+ buffers.delete(event.messageId);
48
+ finalised.add(event.messageId);
49
+ await flush(text);
50
+ },
51
+ onToolCallArgsEvent({ event, toolCallName, partialToolCallArgs }) {
52
+ if (aborted)
53
+ return;
54
+ captureToolCall(event.toolCallId, toolCallName, (partialToolCallArgs ?? {}));
55
+ },
56
+ onToolCallEndEvent({ event, toolCallName, toolCallArgs }) {
57
+ if (aborted)
58
+ return;
59
+ captureToolCall(event.toolCallId, toolCallName, (toolCallArgs ?? {}));
60
+ },
61
+ onCustomEvent({ event }) {
62
+ if (aborted)
63
+ return;
64
+ const e = event;
65
+ if (!e.name || !interruptEventNames.has(e.name))
66
+ return;
67
+ let value = e.value;
68
+ if (typeof value === "string") {
69
+ try {
70
+ value = JSON.parse(value);
71
+ }
72
+ catch {
73
+ /* leave as string; downstream schema will reject explicitly */
74
+ }
75
+ }
76
+ pendingInterrupt = { eventName: e.name, value };
77
+ },
78
+ async onRunErrorEvent({ event }) {
79
+ if (aborted)
80
+ return;
81
+ try {
82
+ await args.send(`⚠️ Agent error: ${event.message ?? "unknown error"}`);
83
+ }
84
+ catch {
85
+ /* best-effort */
86
+ }
87
+ },
88
+ };
89
+ return {
90
+ subscriber,
91
+ getCapturedToolCalls: () => capturedToolCalls,
92
+ getPendingInterrupt: () => pendingInterrupt,
93
+ clearPendingInterrupt: () => {
94
+ pendingInterrupt = undefined;
95
+ },
96
+ async markInterrupted() {
97
+ if (aborted)
98
+ return;
99
+ aborted = true;
100
+ const tasks = [];
101
+ for (const [id, buf] of Array.from(buffers.entries())) {
102
+ if (buf.length > 0)
103
+ tasks.push(flush(buf + INTERRUPTED_SUFFIX));
104
+ finalised.add(id);
105
+ }
106
+ buffers.clear();
107
+ await Promise.all(tasks);
108
+ },
109
+ };
110
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=event-renderer.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-renderer.test.d.ts","sourceRoot":"","sources":["../src/event-renderer.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,68 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { createRunRenderer } from "./event-renderer.js";
3
+ function evt(o) {
4
+ return { event: o };
5
+ }
6
+ describe("createRunRenderer (buffered)", () => {
7
+ it("buffers text deltas and sends once on TEXT_MESSAGE_END", async () => {
8
+ const sent = [];
9
+ const r = createRunRenderer({
10
+ send: async (text) => {
11
+ sent.push(text);
12
+ },
13
+ });
14
+ const s = r.subscriber;
15
+ s.onTextMessageStartEvent?.(evt({ messageId: "m1" }));
16
+ s.onTextMessageContentEvent?.(evt({ messageId: "m1", delta: "Hello " }));
17
+ s.onTextMessageContentEvent?.(evt({ messageId: "m1", delta: "world" }));
18
+ expect(sent).toEqual([]); // nothing sent mid-stream — no editing
19
+ await s.onTextMessageEndEvent?.(evt({ messageId: "m1" }));
20
+ expect(sent).toEqual(["Hello world"]);
21
+ });
22
+ it("invokes onAssistantText with the final text", async () => {
23
+ const persisted = [];
24
+ const r = createRunRenderer({
25
+ send: async () => { },
26
+ onAssistantText: (t) => persisted.push(t),
27
+ });
28
+ const s = r.subscriber;
29
+ s.onTextMessageStartEvent?.(evt({ messageId: "m" }));
30
+ s.onTextMessageContentEvent?.(evt({ messageId: "m", delta: "Hi" }));
31
+ await s.onTextMessageEndEvent?.(evt({ messageId: "m" }));
32
+ expect(persisted).toEqual(["Hi"]);
33
+ });
34
+ it("captures tool calls for the run loop", async () => {
35
+ const r = createRunRenderer({ send: async () => { } });
36
+ const s = r.subscriber;
37
+ await s.onToolCallEndEvent?.({
38
+ event: { toolCallId: "t1" },
39
+ toolCallName: "do_x",
40
+ toolCallArgs: { a: 1 },
41
+ });
42
+ expect(r.getCapturedToolCalls()).toEqual([
43
+ { toolCallId: "t1", toolCallName: "do_x", toolCallArgs: { a: 1 } },
44
+ ]);
45
+ });
46
+ it("captures interrupts via matching custom event", () => {
47
+ const r = createRunRenderer({ send: async () => { } });
48
+ r.subscriber.onCustomEvent?.({
49
+ event: { name: "on_interrupt", value: { q: 1 } },
50
+ });
51
+ expect(r.getPendingInterrupt()).toEqual({
52
+ eventName: "on_interrupt",
53
+ value: { q: 1 },
54
+ });
55
+ });
56
+ it("does not send after markInterrupted", async () => {
57
+ const sent = [];
58
+ const r = createRunRenderer({ send: async (t) => void sent.push(t) });
59
+ const s = r.subscriber;
60
+ s.onTextMessageStartEvent?.(evt({ messageId: "m" }));
61
+ s.onTextMessageContentEvent?.(evt({ messageId: "m", delta: "partial" }));
62
+ await r.markInterrupted();
63
+ await s.onTextMessageEndEvent?.(evt({ messageId: "m" }));
64
+ // partial reply flushed once by markInterrupted with an interrupted marker;
65
+ // the late END is ignored.
66
+ expect(sent).toEqual(["partial\n_(interrupted)_"]);
67
+ });
68
+ });
@@ -0,0 +1,36 @@
1
+ import type { AgentContentPart } from "./download-files.js";
2
+ export interface StoredMessage {
3
+ role: "user" | "assistant";
4
+ /** Plain text for normal turns; multimodal parts when the user sent media. */
5
+ content: string | AgentContentPart[];
6
+ /** Sortable timestamp (the inbound message ts, or a monotonic counter). */
7
+ ts: string;
8
+ /**
9
+ * Platform message id (WhatsApp `wamid`) when known. Lets a later quote-reply
10
+ * resolve the message it quotes (the webhook sends only the quoted id, not its
11
+ * text). Optional — durable stores may omit it.
12
+ */
13
+ id?: string;
14
+ }
15
+ /**
16
+ * Pluggable conversation-history persistence. WhatsApp exposes no readable
17
+ * history, so the adapter holds it here and replays it into `agent.messages`
18
+ * each turn. The default is in-memory; swap in a durable backend (Redis,
19
+ * Postgres, …) by implementing this interface.
20
+ */
21
+ export interface HistoryStore {
22
+ append(conversationKey: string, message: StoredMessage): Promise<void>;
23
+ read(conversationKey: string): Promise<StoredMessage[]>;
24
+ }
25
+ export interface InMemoryHistoryStoreOptions {
26
+ /** Keep at most this many messages per conversation (drops oldest). Default 100. */
27
+ maxMessages?: number;
28
+ }
29
+ export declare class InMemoryHistoryStore implements HistoryStore {
30
+ private readonly map;
31
+ private readonly maxMessages;
32
+ constructor(opts?: InMemoryHistoryStoreOptions);
33
+ append(conversationKey: string, message: StoredMessage): Promise<void>;
34
+ read(conversationKey: string): Promise<StoredMessage[]>;
35
+ }
36
+ //# sourceMappingURL=history-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"history-store.d.ts","sourceRoot":"","sources":["../src/history-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5D,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAC3B,8EAA8E;IAC9E,OAAO,EAAE,MAAM,GAAG,gBAAgB,EAAE,CAAC;IACrC,2EAA2E;IAC3E,EAAE,EAAE,MAAM,CAAC;IACX;;;;OAIG;IACH,EAAE,CAAC,EAAE,MAAM,CAAC;CACb;AAED;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,eAAe,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvE,IAAI,CAAC,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CAAC;CACzD;AAED,MAAM,WAAW,2BAA2B;IAC1C,oFAAoF;IACpF,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,qBAAa,oBAAqB,YAAW,YAAY;IACvD,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAsC;IAC1D,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;gBAEzB,IAAI,GAAE,2BAAgC;IAI5C,MAAM,CAAC,eAAe,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAQtE,IAAI,CAAC,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC;CAG9D"}
@@ -0,0 +1,17 @@
1
+ export class InMemoryHistoryStore {
2
+ map = new Map();
3
+ maxMessages;
4
+ constructor(opts = {}) {
5
+ this.maxMessages = opts.maxMessages ?? 100;
6
+ }
7
+ async append(conversationKey, message) {
8
+ const arr = this.map.get(conversationKey) ?? [];
9
+ arr.push(message);
10
+ if (arr.length > this.maxMessages)
11
+ arr.splice(0, arr.length - this.maxMessages);
12
+ this.map.set(conversationKey, arr);
13
+ }
14
+ async read(conversationKey) {
15
+ return [...(this.map.get(conversationKey) ?? [])];
16
+ }
17
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=history-store.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"history-store.test.d.ts","sourceRoot":"","sources":["../src/history-store.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,24 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { InMemoryHistoryStore } from "./history-store.js";
3
+ describe("InMemoryHistoryStore", () => {
4
+ it("appends and reads back per conversation in order", async () => {
5
+ const store = new InMemoryHistoryStore();
6
+ await store.append("c1", { role: "user", content: "hi", ts: "1" });
7
+ await store.append("c1", { role: "assistant", content: "hello", ts: "2" });
8
+ await store.append("c2", { role: "user", content: "other", ts: "3" });
9
+ expect(await store.read("c1")).toEqual([
10
+ { role: "user", content: "hi", ts: "1" },
11
+ { role: "assistant", content: "hello", ts: "2" },
12
+ ]);
13
+ expect(await store.read("c2")).toHaveLength(1);
14
+ expect(await store.read("missing")).toEqual([]);
15
+ });
16
+ it("caps stored history to maxMessages (drops oldest)", async () => {
17
+ const store = new InMemoryHistoryStore({ maxMessages: 2 });
18
+ await store.append("c", { role: "user", content: "a", ts: "1" });
19
+ await store.append("c", { role: "assistant", content: "b", ts: "2" });
20
+ await store.append("c", { role: "user", content: "c", ts: "3" });
21
+ const out = await store.read("c");
22
+ expect(out.map((m) => m.content)).toEqual(["b", "c"]);
23
+ });
24
+ });
@@ -0,0 +1,18 @@
1
+ export { whatsapp, WhatsAppAdapter } from "./adapter.js";
2
+ export type { WhatsAppAdapterOptions, ReplyTarget, WhatsAppMessageRef, } from "./types.js";
3
+ export { WhatsAppConversationStore } from "./conversation-store.js";
4
+ export { InMemoryHistoryStore } from "./history-store.js";
5
+ export type { HistoryStore, StoredMessage } from "./history-store.js";
6
+ export { renderWhatsAppMessage } from "./render/message.js";
7
+ export type { WhatsAppOutbound } from "./render/message.js";
8
+ export { WA_LIMITS, truncateText, clampArray } from "./render/budget.js";
9
+ export { markdownToWhatsApp } from "./markdown-to-wa.js";
10
+ export { decodeInteraction, conversationKeyOf } from "./interaction.js";
11
+ export { createRunRenderer } from "./event-renderer.js";
12
+ export { WhatsAppClient } from "./client.js";
13
+ export type { DownloadedMedia } from "./client.js";
14
+ export { buildFileContentParts } from "./download-files.js";
15
+ export type { AgentContentPart, FileDeliveryConfig } from "./download-files.js";
16
+ export { defaultWhatsAppTools } from "./built-in-tools.js";
17
+ export { defaultWhatsAppContext, whatsAppFormattingContext, whatsAppDeliveryContext, } from "./built-in-context.js";
18
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACzD,YAAY,EACV,sBAAsB,EACtB,WAAW,EACX,kBAAkB,GACnB,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,yBAAyB,EAAE,MAAM,yBAAyB,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC1D,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEtE,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,YAAY,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAEzE,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAEzD,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACxE,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAExD,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,YAAY,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,YAAY,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAEhF,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EACL,sBAAsB,EACtB,yBAAyB,EACzB,uBAAuB,GACxB,MAAM,uBAAuB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ export { whatsapp, WhatsAppAdapter } from "./adapter.js";
2
+ export { WhatsAppConversationStore } from "./conversation-store.js";
3
+ export { InMemoryHistoryStore } from "./history-store.js";
4
+ export { renderWhatsAppMessage } from "./render/message.js";
5
+ export { WA_LIMITS, truncateText, clampArray } from "./render/budget.js";
6
+ export { markdownToWhatsApp } from "./markdown-to-wa.js";
7
+ export { decodeInteraction, conversationKeyOf } from "./interaction.js";
8
+ export { createRunRenderer } from "./event-renderer.js";
9
+ export { WhatsAppClient } from "./client.js";
10
+ export { buildFileContentParts } from "./download-files.js";
11
+ export { defaultWhatsAppTools } from "./built-in-tools.js";
12
+ export { defaultWhatsAppContext, whatsAppFormattingContext, whatsAppDeliveryContext, } from "./built-in-context.js";
@@ -0,0 +1,19 @@
1
+ import type { InteractionEvent } from "@copilotkit/channels";
2
+ import type { InboundMessage, ReplyTarget } from "./types.js";
3
+ /**
4
+ * Stable conversation key shared by ingress (onTurn) and interaction decoding
5
+ * so `awaitChoice` waiters resolve. Both paths MUST derive the key from this
6
+ * single helper — a mismatch silently strands the waiter.
7
+ */
8
+ export declare function conversationKeyOf(waId: string): string;
9
+ /** Inverse of conversationKeyOf: recover the wa_id from a conversation key. */
10
+ export declare function waIdFromKey(conversationKey: string): string;
11
+ /**
12
+ * Decode an inbound interactive message (`button_reply` / `list_reply`) into a
13
+ * bot `InteractionEvent`. The opaque minted id (`ck:...`) rides in the reply
14
+ * `id`. A control's value is encoded as `${actionId}::${JSON.stringify(value)}`
15
+ * (see render/message.ts), so we split it back here: the engine dispatches by
16
+ * `actionId` and the value rides in `value`.
17
+ */
18
+ export declare function decodeInteraction(msg: InboundMessage, replyTarget: ReplyTarget): InteractionEvent | undefined;
19
+ //# sourceMappingURL=interaction.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interaction.d.ts","sourceRoot":"","sources":["../src/interaction.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9D;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED,+EAA+E;AAC/E,wBAAgB,WAAW,CAAC,eAAe,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,cAAc,EACnB,WAAW,EAAE,WAAW,GACvB,gBAAgB,GAAG,SAAS,CAoC9B"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Stable conversation key shared by ingress (onTurn) and interaction decoding
3
+ * so `awaitChoice` waiters resolve. Both paths MUST derive the key from this
4
+ * single helper — a mismatch silently strands the waiter.
5
+ */
6
+ export function conversationKeyOf(waId) {
7
+ return `whatsapp:${waId}`;
8
+ }
9
+ /** Inverse of conversationKeyOf: recover the wa_id from a conversation key. */
10
+ export function waIdFromKey(conversationKey) {
11
+ return conversationKey.replace(/^whatsapp:/, "");
12
+ }
13
+ /**
14
+ * Decode an inbound interactive message (`button_reply` / `list_reply`) into a
15
+ * bot `InteractionEvent`. The opaque minted id (`ck:...`) rides in the reply
16
+ * `id`. A control's value is encoded as `${actionId}::${JSON.stringify(value)}`
17
+ * (see render/message.ts), so we split it back here: the engine dispatches by
18
+ * `actionId` and the value rides in `value`.
19
+ */
20
+ export function decodeInteraction(msg, replyTarget) {
21
+ if (msg.type !== "interactive" || !msg.interactive)
22
+ return undefined;
23
+ const i = msg.interactive;
24
+ const reply = i.type === "button_reply"
25
+ ? i.button_reply
26
+ : i.type === "list_reply"
27
+ ? i.list_reply
28
+ : undefined;
29
+ if (!reply?.id)
30
+ return undefined;
31
+ let id = reply.id;
32
+ let value = undefined;
33
+ const sep = reply.id.indexOf("::");
34
+ if (sep !== -1) {
35
+ id = reply.id.slice(0, sep);
36
+ const raw = reply.id.slice(sep + 2);
37
+ try {
38
+ value = JSON.parse(raw);
39
+ }
40
+ catch {
41
+ value = raw;
42
+ }
43
+ }
44
+ return {
45
+ id,
46
+ conversationKey: conversationKeyOf(msg.from),
47
+ replyTarget,
48
+ value,
49
+ user: { id: msg.from },
50
+ messageRef: {
51
+ id: msg.id,
52
+ to: replyTarget.to,
53
+ phoneNumberId: replyTarget.phoneNumberId,
54
+ },
55
+ };
56
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=interaction.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interaction.test.d.ts","sourceRoot":"","sources":["../src/interaction.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,67 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { conversationKeyOf, decodeInteraction } from "./interaction.js";
3
+ describe("conversationKeyOf", () => {
4
+ it("keys a conversation by wa_id", () => {
5
+ expect(conversationKeyOf("15551234567")).toBe("whatsapp:15551234567");
6
+ });
7
+ });
8
+ describe("decodeInteraction", () => {
9
+ const target = { to: "15551234567", phoneNumberId: "PNID" };
10
+ it("decodes a button_reply into an InteractionEvent", () => {
11
+ const msg = {
12
+ from: "15551234567",
13
+ id: "wamid.A",
14
+ type: "interactive",
15
+ interactive: {
16
+ type: "button_reply",
17
+ button_reply: { id: "ck:42", title: "Yes" },
18
+ },
19
+ };
20
+ const evt = decodeInteraction(msg, target);
21
+ expect(evt).toEqual({
22
+ id: "ck:42",
23
+ conversationKey: "whatsapp:15551234567",
24
+ replyTarget: target,
25
+ value: undefined,
26
+ user: { id: "15551234567" },
27
+ messageRef: { id: "wamid.A", to: "15551234567", phoneNumberId: "PNID" },
28
+ });
29
+ });
30
+ it("decodes a list_reply, splitting the JSON-encoded option value off the id", () => {
31
+ const msg = {
32
+ from: "15551234567",
33
+ id: "wamid.B",
34
+ type: "interactive",
35
+ interactive: {
36
+ type: "list_reply",
37
+ list_reply: { id: 'ck:sel::"eu"', title: "EU" },
38
+ },
39
+ };
40
+ const evt = decodeInteraction(msg, target);
41
+ expect(evt?.id).toBe("ck:sel");
42
+ expect(evt?.value).toBe("eu");
43
+ });
44
+ it("decodes a button_reply with a JSON-encoded value", () => {
45
+ const msg = {
46
+ from: "15551234567",
47
+ id: "wamid.C",
48
+ type: "interactive",
49
+ interactive: {
50
+ type: "button_reply",
51
+ button_reply: { id: 'ck:7::{"confirmed":true}', title: "Yes" },
52
+ },
53
+ };
54
+ const evt = decodeInteraction(msg, target);
55
+ expect(evt?.id).toBe("ck:7");
56
+ expect(evt?.value).toEqual({ confirmed: true });
57
+ });
58
+ it("returns undefined for a non-interactive message", () => {
59
+ const msg = {
60
+ from: "x",
61
+ id: "1",
62
+ type: "text",
63
+ text: { body: "hi" },
64
+ };
65
+ expect(decodeInteraction(msg, target)).toBeUndefined();
66
+ });
67
+ });
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Translate GFM Markdown into WhatsApp's formatting subset:
3
+ * **bold** / __bold__ → *bold*
4
+ * *italic* / _italic_ → _italic_
5
+ * ~~strike~~ → ~strike~
6
+ * [text](url) → text (url)
7
+ * # / ## / ### Heading → *Heading*
8
+ * Fenced (```) and inline (`) code are preserved verbatim — no transforms
9
+ * run inside them.
10
+ *
11
+ * Implementation is a single forward scan that copies code spans/fences
12
+ * untouched and applies the substitutions only to the prose between them.
13
+ */
14
+ export declare function markdownToWhatsApp(input: string): string;
15
+ //# sourceMappingURL=markdown-to-wa.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown-to-wa.d.ts","sourceRoot":"","sources":["../src/markdown-to-wa.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAKxD"}