@copilotkit/channels 0.1.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.
Files changed (156) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +204 -0
  3. package/dist/action-registry.d.ts +59 -0
  4. package/dist/action-registry.d.ts.map +1 -0
  5. package/dist/action-registry.js +217 -0
  6. package/dist/action-registry.test.d.ts +2 -0
  7. package/dist/action-registry.test.d.ts.map +1 -0
  8. package/dist/action-registry.test.js +115 -0
  9. package/dist/action-store.d.ts +21 -0
  10. package/dist/action-store.d.ts.map +1 -0
  11. package/dist/action-store.js +23 -0
  12. package/dist/action-store.test.d.ts +2 -0
  13. package/dist/action-store.test.d.ts.map +1 -0
  14. package/dist/action-store.test.js +27 -0
  15. package/dist/codec.d.ts +25 -0
  16. package/dist/codec.d.ts.map +1 -0
  17. package/dist/codec.js +1 -0
  18. package/dist/commands.d.ts +73 -0
  19. package/dist/commands.d.ts.map +1 -0
  20. package/dist/commands.js +38 -0
  21. package/dist/commands.test.d.ts +2 -0
  22. package/dist/commands.test.d.ts.map +1 -0
  23. package/dist/commands.test.js +23 -0
  24. package/dist/create-bot.d.ts +171 -0
  25. package/dist/create-bot.d.ts.map +1 -0
  26. package/dist/create-bot.foundations.test.d.ts +2 -0
  27. package/dist/create-bot.foundations.test.d.ts.map +1 -0
  28. package/dist/create-bot.foundations.test.js +147 -0
  29. package/dist/create-bot.js +576 -0
  30. package/dist/create-bot.test.d.ts +2 -0
  31. package/dist/create-bot.test.d.ts.map +1 -0
  32. package/dist/create-bot.test.js +936 -0
  33. package/dist/index.d.ts +26 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +32 -0
  36. package/dist/mint-id.d.ts +3 -0
  37. package/dist/mint-id.d.ts.map +1 -0
  38. package/dist/mint-id.js +20 -0
  39. package/dist/mint-id.test.d.ts +2 -0
  40. package/dist/mint-id.test.d.ts.map +1 -0
  41. package/dist/mint-id.test.js +16 -0
  42. package/dist/modal-submit.test.d.ts +2 -0
  43. package/dist/modal-submit.test.d.ts.map +1 -0
  44. package/dist/modal-submit.test.js +68 -0
  45. package/dist/platform-adapter.d.ts +312 -0
  46. package/dist/platform-adapter.d.ts.map +1 -0
  47. package/dist/platform-adapter.js +1 -0
  48. package/dist/platform-adapter.test.d.ts +2 -0
  49. package/dist/platform-adapter.test.d.ts.map +1 -0
  50. package/dist/platform-adapter.test.js +40 -0
  51. package/dist/reactions.test.d.ts +2 -0
  52. package/dist/reactions.test.d.ts.map +1 -0
  53. package/dist/reactions.test.js +197 -0
  54. package/dist/run-loop.d.ts +41 -0
  55. package/dist/run-loop.d.ts.map +1 -0
  56. package/dist/run-loop.js +102 -0
  57. package/dist/run-loop.test.d.ts +2 -0
  58. package/dist/run-loop.test.d.ts.map +1 -0
  59. package/dist/run-loop.test.js +125 -0
  60. package/dist/standard-schema.d.ts +52 -0
  61. package/dist/standard-schema.d.ts.map +1 -0
  62. package/dist/standard-schema.js +66 -0
  63. package/dist/state/duration.d.ts +3 -0
  64. package/dist/state/duration.d.ts.map +1 -0
  65. package/dist/state/duration.js +16 -0
  66. package/dist/state/duration.test.d.ts +2 -0
  67. package/dist/state/duration.test.d.ts.map +1 -0
  68. package/dist/state/duration.test.js +13 -0
  69. package/dist/state/kv-action-store.d.ts +7 -0
  70. package/dist/state/kv-action-store.d.ts.map +1 -0
  71. package/dist/state/kv-action-store.js +9 -0
  72. package/dist/state/kv-action-store.test.d.ts +2 -0
  73. package/dist/state/kv-action-store.test.d.ts.map +1 -0
  74. package/dist/state/kv-action-store.test.js +21 -0
  75. package/dist/state/memory-store.d.ts +41 -0
  76. package/dist/state/memory-store.d.ts.map +1 -0
  77. package/dist/state/memory-store.js +110 -0
  78. package/dist/state/memory-store.test.d.ts +2 -0
  79. package/dist/state/memory-store.test.d.ts.map +1 -0
  80. package/dist/state/memory-store.test.js +22 -0
  81. package/dist/state/state-conversation-store.d.ts +14 -0
  82. package/dist/state/state-conversation-store.d.ts.map +1 -0
  83. package/dist/state/state-conversation-store.js +22 -0
  84. package/dist/state/state-conversation-store.test.d.ts +2 -0
  85. package/dist/state/state-conversation-store.test.d.ts.map +1 -0
  86. package/dist/state/state-conversation-store.test.js +58 -0
  87. package/dist/state/state-store.d.ts +63 -0
  88. package/dist/state/state-store.d.ts.map +1 -0
  89. package/dist/state/state-store.js +1 -0
  90. package/dist/telemetry/bot-telemetry.d.ts +31 -0
  91. package/dist/telemetry/bot-telemetry.d.ts.map +1 -0
  92. package/dist/telemetry/bot-telemetry.js +70 -0
  93. package/dist/telemetry/bot-telemetry.test.d.ts +2 -0
  94. package/dist/telemetry/bot-telemetry.test.d.ts.map +1 -0
  95. package/dist/telemetry/bot-telemetry.test.js +56 -0
  96. package/dist/telemetry/create-bot-telemetry.test.d.ts +2 -0
  97. package/dist/telemetry/create-bot-telemetry.test.d.ts.map +1 -0
  98. package/dist/telemetry/create-bot-telemetry.test.js +71 -0
  99. package/dist/telemetry/e2e-telemetry.test.d.ts +2 -0
  100. package/dist/telemetry/e2e-telemetry.test.d.ts.map +1 -0
  101. package/dist/telemetry/e2e-telemetry.test.js +56 -0
  102. package/dist/telemetry/events-catalog.test.d.ts +2 -0
  103. package/dist/telemetry/events-catalog.test.d.ts.map +1 -0
  104. package/dist/telemetry/events-catalog.test.js +8 -0
  105. package/dist/telemetry/install-id.d.ts +8 -0
  106. package/dist/telemetry/install-id.d.ts.map +1 -0
  107. package/dist/telemetry/install-id.js +43 -0
  108. package/dist/telemetry/install-id.test.d.ts +2 -0
  109. package/dist/telemetry/install-id.test.d.ts.map +1 -0
  110. package/dist/telemetry/install-id.test.js +43 -0
  111. package/dist/telemetry/platform-allowlist-coverage.test.d.ts +2 -0
  112. package/dist/telemetry/platform-allowlist-coverage.test.d.ts.map +1 -0
  113. package/dist/telemetry/platform-allowlist-coverage.test.js +45 -0
  114. package/dist/telemetry/sanitize-error.d.ts +3 -0
  115. package/dist/telemetry/sanitize-error.d.ts.map +1 -0
  116. package/dist/telemetry/sanitize-error.js +31 -0
  117. package/dist/telemetry/sanitize-error.test.d.ts +2 -0
  118. package/dist/telemetry/sanitize-error.test.d.ts.map +1 -0
  119. package/dist/telemetry/sanitize-error.test.js +33 -0
  120. package/dist/testing/fake-adapter.d.ts +137 -0
  121. package/dist/testing/fake-adapter.d.ts.map +1 -0
  122. package/dist/testing/fake-adapter.js +273 -0
  123. package/dist/testing/fake-agent.d.ts +28 -0
  124. package/dist/testing/fake-agent.d.ts.map +1 -0
  125. package/dist/testing/fake-agent.js +36 -0
  126. package/dist/testing/state-store-conformance.d.ts +4 -0
  127. package/dist/testing/state-store-conformance.d.ts.map +1 -0
  128. package/dist/testing/state-store-conformance.js +117 -0
  129. package/dist/thread-capabilities.test.d.ts +2 -0
  130. package/dist/thread-capabilities.test.d.ts.map +1 -0
  131. package/dist/thread-capabilities.test.js +77 -0
  132. package/dist/thread-ephemeral.test.d.ts +2 -0
  133. package/dist/thread-ephemeral.test.d.ts.map +1 -0
  134. package/dist/thread-ephemeral.test.js +45 -0
  135. package/dist/thread-reactions.test.d.ts +2 -0
  136. package/dist/thread-reactions.test.d.ts.map +1 -0
  137. package/dist/thread-reactions.test.js +38 -0
  138. package/dist/thread.d.ts +152 -0
  139. package/dist/thread.d.ts.map +1 -0
  140. package/dist/thread.js +329 -0
  141. package/dist/thread.test.d.ts +2 -0
  142. package/dist/thread.test.d.ts.map +1 -0
  143. package/dist/thread.test.js +83 -0
  144. package/dist/tools.d.ts +67 -0
  145. package/dist/tools.d.ts.map +1 -0
  146. package/dist/tools.js +38 -0
  147. package/dist/tools.test.d.ts +2 -0
  148. package/dist/tools.test.d.ts.map +1 -0
  149. package/dist/tools.test.js +31 -0
  150. package/dist/transcripts.d.ts +52 -0
  151. package/dist/transcripts.d.ts.map +1 -0
  152. package/dist/transcripts.js +67 -0
  153. package/dist/transcripts.test.d.ts +2 -0
  154. package/dist/transcripts.test.d.ts.map +1 -0
  155. package/dist/transcripts.test.js +108 -0
  156. package/package.json +72 -0
@@ -0,0 +1,71 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ const capture = vi.fn();
3
+ vi.mock("./bot-telemetry.js", () => ({
4
+ // A `function` (not an arrow) so `new BotTelemetry(...)` in create-bot.ts is
5
+ // constructible under vitest's mock — an arrow implementation throws
6
+ // "is not a constructor".
7
+ BotTelemetry: vi.fn().mockImplementation(function () {
8
+ return { capture };
9
+ }),
10
+ BOT_TELEMETRY_EVENTS: [],
11
+ }));
12
+ import { createBot } from "../create-bot.js";
13
+ import { FakeAdapter } from "../testing/fake-adapter.js";
14
+ import { FakeAgent } from "../testing/fake-agent.js";
15
+ import { Section } from "@copilotkit/channels-ui";
16
+ const tick = () => new Promise((r) => setTimeout(r, 0));
17
+ describe("createBot telemetry wiring", () => {
18
+ beforeEach(() => capture.mockClear());
19
+ it("emits oss.bot.configured with a config snapshot", async () => {
20
+ // The config snapshot is captured at start() — the backend (and therefore
21
+ // telemetry) is resolved there, not at construction, so an adapter attached
22
+ // via addAdapter can still provide the persistence backend.
23
+ const bot = createBot({
24
+ adapters: [new FakeAdapter()],
25
+ components: [
26
+ function Card() {
27
+ return Section({ children: "x" });
28
+ },
29
+ ],
30
+ });
31
+ await bot.start();
32
+ const call = capture.mock.calls.find((c) => c[0] === "oss.bot.configured");
33
+ expect(call).toBeDefined();
34
+ expect(call[1].platforms).toEqual(["custom"]); // FakeAdapter.platform "fake" → normalized
35
+ expect(call[1].store).toBe("memory");
36
+ expect(call[1].hasComponents).toBe(true);
37
+ });
38
+ it("emits oss.bot.started on start, start_failed (category only) on a throwing adapter", async () => {
39
+ const ok = new FakeAdapter();
40
+ const bot = createBot({ adapters: [ok] });
41
+ await bot.start();
42
+ expect(capture.mock.calls.find((c) => c[0] === "oss.bot.started")?.[1]
43
+ .startedCount).toBe(1);
44
+ capture.mockClear();
45
+ const bad = new FakeAdapter();
46
+ bad.start = () => Promise.reject(Object.assign(new Error("xoxb-SECRET token bad"), { code: "EAUTH" }));
47
+ const bot2 = createBot({ adapters: [bad] });
48
+ await bot2.start();
49
+ const f = capture.mock.calls.find((c) => c[0] === "oss.bot.start_failed");
50
+ expect(f).toBeDefined();
51
+ expect(f[1].errorClass).toBe("auth");
52
+ expect(JSON.stringify(f[1])).not.toContain("SECRET");
53
+ });
54
+ it("emits oss.bot.agent_run on a successful run", async () => {
55
+ const fake = new FakeAdapter();
56
+ const bot = createBot({ adapters: [fake], agent: () => new FakeAgent() });
57
+ bot.onMention(async ({ thread }) => {
58
+ await thread.runAgent();
59
+ });
60
+ await bot.start();
61
+ capture.mockClear();
62
+ fake.emitTurn({ userText: "hi", conversationKey: "c1" });
63
+ await tick();
64
+ await tick();
65
+ const run = capture.mock.calls.find((c) => c[0] === "oss.bot.agent_run");
66
+ expect(run).toBeDefined();
67
+ expect(run[1].platform).toBe("custom"); // "fake" → normalized
68
+ expect(typeof run[1].durationMs).toBe("number");
69
+ expect(run[1].interrupted).toBe(false);
70
+ });
71
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=e2e-telemetry.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"e2e-telemetry.test.d.ts","sourceRoot":"","sources":["../../src/telemetry/e2e-telemetry.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,56 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ // vi.hoisted so the spy exists before the hoisted vi.mock factory runs
3
+ // (otherwise "Cannot access 'sendSpy' before initialization").
4
+ const { sendSpy } = vi.hoisted(() => ({
5
+ sendSpy: vi.fn().mockResolvedValue(undefined),
6
+ }));
7
+ vi.mock("@copilotkit/shared", async (importOriginal) => {
8
+ const actual = await importOriginal();
9
+ return { ...actual, lambdaClient: { send: sendSpy } };
10
+ });
11
+ import { createBot } from "../create-bot.js";
12
+ import { FakeAdapter } from "../testing/fake-adapter.js";
13
+ import { FakeAgent } from "../testing/fake-agent.js";
14
+ const waitFor = async (pred, ms = 1000) => {
15
+ const start = Date.now();
16
+ while (!pred() && Date.now() - start < ms)
17
+ await new Promise((r) => setTimeout(r, 10));
18
+ };
19
+ describe("oss.bot.* end-to-end (real BotTelemetry, only network boundary stubbed)", () => {
20
+ const saved = { ...process.env };
21
+ beforeEach(() => {
22
+ sendSpy.mockClear();
23
+ // Enable telemetry: clear the test-runner suppressors + any opt-out.
24
+ delete process.env.VITEST;
25
+ delete process.env.JEST_WORKER_ID;
26
+ delete process.env.COPILOTKIT_TELEMETRY_DISABLED;
27
+ delete process.env.DO_NOT_TRACK;
28
+ process.env.NODE_ENV = "production";
29
+ // Deliberately set NO COPILOTKIT_TELEMETRY_URL / license / API key — zero-config.
30
+ });
31
+ afterEach(() => {
32
+ process.env = { ...saved };
33
+ });
34
+ it("flows configured -> started -> agent_run with anonymous_id + bot_session_id and no env config", async () => {
35
+ const fake = new FakeAdapter();
36
+ const bot = createBot({ adapters: [fake], agent: () => new FakeAgent() });
37
+ bot.onMention(async ({ thread }) => {
38
+ await thread.runAgent();
39
+ });
40
+ await bot.start();
41
+ fake.emitTurn({ userText: "hi", conversationKey: "c1" });
42
+ await waitFor(() => sendSpy.mock.calls.some((c) => c[0].event === "oss.bot.agent_run"));
43
+ const events = sendSpy.mock.calls.map((c) => c[0].event);
44
+ expect(events).toContain("oss.bot.configured");
45
+ expect(events).toContain("oss.bot.started");
46
+ expect(events).toContain("oss.bot.agent_run");
47
+ for (const [arg] of sendSpy.mock.calls) {
48
+ expect(typeof arg.globalProperties.anonymous_id).toBe("string");
49
+ expect(typeof arg.globalProperties.bot_session_id).toBe("string");
50
+ expect(arg.licenseToken).toBeUndefined(); // anonymous: no license ever attached
51
+ }
52
+ const run = sendSpy.mock.calls.find((c) => c[0].event === "oss.bot.agent_run")[0];
53
+ expect(run.properties.platform).toBe("custom"); // "fake" → normalized
54
+ expect(typeof run.properties.durationMs).toBe("number");
55
+ });
56
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=events-catalog.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events-catalog.test.d.ts","sourceRoot":"","sources":["../../src/telemetry/events-catalog.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,8 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import catalog from "../../telemetry-events.json" with { type: "json" };
3
+ import { BOT_TELEMETRY_EVENTS } from "./bot-telemetry.js";
4
+ describe("telemetry-events.json", () => {
5
+ it("documents exactly the emitted events", () => {
6
+ expect(Object.keys(catalog.events).sort()).toEqual([...BOT_TELEMETRY_EVENTS].sort());
7
+ });
8
+ });
@@ -0,0 +1,8 @@
1
+ import type { StateStore } from "../state/state-store.js";
2
+ export interface ResolveInstallIdDeps {
3
+ backend: StateStore;
4
+ cacheDir?: string;
5
+ uuid?: () => string;
6
+ }
7
+ export declare function resolveInstallId(deps: ResolveInstallIdDeps): Promise<string>;
8
+ //# sourceMappingURL=install-id.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"install-id.d.ts","sourceRoot":"","sources":["../../src/telemetry/install-id.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAQ1D,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,UAAU,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,MAAM,CAAC;CACrB;AAED,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,oBAAoB,GACzB,OAAO,CAAC,MAAM,CAAC,CA8BjB"}
@@ -0,0 +1,43 @@
1
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { MemoryStore } from "../state/memory-store.js";
4
+ const STORE_KEY = "cpk:telemetry:install_id";
5
+ const FILE_NAME = "telemetry-id";
6
+ const defaultUuid = () => globalThis.crypto.randomUUID();
7
+ const defaultCacheDir = () => join(process.cwd(), "node_modules", ".cache", "copilotkit");
8
+ export async function resolveInstallId(deps) {
9
+ const uuid = deps.uuid ?? defaultUuid;
10
+ if (!(deps.backend instanceof MemoryStore)) {
11
+ try {
12
+ const existing = await deps.backend.kv.get(STORE_KEY);
13
+ if (existing)
14
+ return existing;
15
+ const id = uuid();
16
+ await deps.backend.kv.set(STORE_KEY, id);
17
+ return id;
18
+ }
19
+ catch {
20
+ /* fall through */
21
+ }
22
+ }
23
+ try {
24
+ const dir = deps.cacheDir ?? defaultCacheDir();
25
+ const file = join(dir, FILE_NAME);
26
+ try {
27
+ const existing = readFileSync(file, "utf8").trim();
28
+ if (existing)
29
+ return existing;
30
+ }
31
+ catch {
32
+ /* not created yet */
33
+ }
34
+ mkdirSync(dir, { recursive: true });
35
+ const id = uuid();
36
+ writeFileSync(file, id, "utf8");
37
+ return id;
38
+ }
39
+ catch {
40
+ /* fall through */
41
+ }
42
+ return uuid();
43
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=install-id.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"install-id.test.d.ts","sourceRoot":"","sources":["../../src/telemetry/install-id.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,43 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { mkdtempSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { resolveInstallId } from "./install-id.js";
6
+ import { MemoryStore } from "../state/memory-store.js";
7
+ class FakeDurableStore {
8
+ map = new Map();
9
+ kv = {
10
+ get: async (k) => this.map.get(k),
11
+ set: async (k, v) => void this.map.set(k, v),
12
+ delete: async (k) => void this.map.delete(k),
13
+ };
14
+ list = {};
15
+ lock = {};
16
+ dedup = {};
17
+ queue = {};
18
+ }
19
+ describe("resolveInstallId", () => {
20
+ it("persists + reuses in a durable store", async () => {
21
+ const backend = new FakeDurableStore();
22
+ const a = await resolveInstallId({ backend });
23
+ const b = await resolveInstallId({ backend });
24
+ expect(a).toBe(b);
25
+ expect(backend.map.get("cpk:telemetry:install_id")).toBe(a);
26
+ });
27
+ it("persists + reuses in a file for MemoryStore", async () => {
28
+ const cacheDir = mkdtempSync(join(tmpdir(), "cpk-"));
29
+ const backend = new MemoryStore();
30
+ const a = await resolveInstallId({ backend, cacheDir });
31
+ const b = await resolveInstallId({ backend, cacheDir });
32
+ expect(a).toBe(b);
33
+ });
34
+ it("falls back to a uuid when the file dir is unwritable", async () => {
35
+ const backend = new MemoryStore();
36
+ const id = await resolveInstallId({
37
+ backend,
38
+ cacheDir: "/dev/null/nope",
39
+ uuid: () => "FALLBACK",
40
+ });
41
+ expect(id).toBe("FALLBACK");
42
+ });
43
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=platform-allowlist-coverage.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform-allowlist-coverage.test.d.ts","sourceRoot":"","sources":["../../src/telemetry/platform-allowlist-coverage.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,45 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { readFileSync, readdirSync, existsSync } from "node:fs";
3
+ import { fileURLToPath } from "node:url";
4
+ import { join } from "node:path";
5
+ import { normalizePlatform } from "./sanitize-error.js";
6
+ // Monorepo invariant: every official channel adapter's `platform` label must be in
7
+ // normalizePlatform()'s allow-list, otherwise a newly-added adapter's events
8
+ // silently bucket to "custom" and we lose its per-platform telemetry signal.
9
+ //
10
+ // We can't import the adapter packages (they depend on @copilotkit/channels, not the
11
+ // reverse), so we discover them by scanning sibling channels-* packages on disk. This
12
+ // runs only in the monorepo (tests aren't published), which is exactly where the
13
+ // drift would be introduced.
14
+ // packages/channels/src/telemetry/ -> packages/
15
+ const packagesDir = fileURLToPath(new URL("../../../", import.meta.url));
16
+ function discoverAdapterPlatforms() {
17
+ const found = [];
18
+ for (const entry of readdirSync(packagesDir)) {
19
+ if (!entry.startsWith("channels-"))
20
+ continue;
21
+ // Adapter packages declare `readonly platform = "x"` in src/adapter.ts;
22
+ // non-adapter channels-* packages (channels-ui) have no such file/field.
23
+ const adapterFile = join(packagesDir, entry, "src", "adapter.ts");
24
+ if (!existsSync(adapterFile))
25
+ continue;
26
+ const src = readFileSync(adapterFile, "utf8");
27
+ const m = src.match(/readonly\s+platform\s*=\s*["']([^"']+)["']/);
28
+ if (m)
29
+ found.push({ pkg: entry, platform: m[1] });
30
+ }
31
+ return found;
32
+ }
33
+ describe("normalizePlatform allow-list coverage (monorepo invariant)", () => {
34
+ it("covers every official channel adapter's declared platform", () => {
35
+ const discovered = discoverAdapterPlatforms();
36
+ // Guard against a broken scan path silently passing: we ship at least
37
+ // slack/discord/telegram/whatsapp.
38
+ expect(discovered.length, `expected to discover the official channel adapters under ${packagesDir}`).toBeGreaterThanOrEqual(4);
39
+ const missing = discovered.filter(({ platform }) => normalizePlatform(platform) !== platform);
40
+ expect(missing, `These adapter platforms are NOT in normalizePlatform's allow-list, so their ` +
41
+ `telemetry would bucket to "custom". Add them to KNOWN_PLATFORMS in ` +
42
+ `packages/channels/src/telemetry/sanitize-error.ts: ` +
43
+ missing.map((m) => `"${m.platform}" (${m.pkg})`).join(", ")).toEqual([]);
44
+ });
45
+ });
@@ -0,0 +1,3 @@
1
+ export declare function errorClass(err: unknown): string;
2
+ export declare function normalizePlatform(platform: string): string;
3
+ //# sourceMappingURL=sanitize-error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitize-error.d.ts","sourceRoot":"","sources":["../../src/telemetry/sanitize-error.ts"],"names":[],"mappings":"AAEA,wBAAgB,UAAU,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAY/C;AAaD,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAE1D"}
@@ -0,0 +1,31 @@
1
+ // Map an arbitrary thrown value to a bounded, non-identifying category.
2
+ // NEVER returns the error message or stack — only a fixed-cardinality label.
3
+ export function errorClass(err) {
4
+ const e = err;
5
+ const name = typeof e?.name === "string" ? e.name : "";
6
+ const code = typeof e?.code === "string" ? e.code : "";
7
+ const hay = `${name} ${code}`.toLowerCase();
8
+ if (/abort|timeout|etimedout|deadline/.test(hay))
9
+ return "timeout";
10
+ if (/network|fetch|econn|enotfound|socket|dns|epipe/.test(hay))
11
+ return "network";
12
+ if (/auth|unauthorized|forbidden|token|credential|401|403/.test(hay))
13
+ return "auth";
14
+ if (/zod|valid|schema|parse/.test(hay))
15
+ return "validation";
16
+ return "unknown";
17
+ }
18
+ // `Adapter.platform` is a free-form string an adapter author sets, so a custom
19
+ // third-party adapter could put a tenant/project name there. Bound it to the
20
+ // known platforms and bucket everything else as "custom" — prevents leaking
21
+ // caller-chosen labels and caps telemetry cardinality.
22
+ const KNOWN_PLATFORMS = new Set([
23
+ "slack",
24
+ "discord",
25
+ "telegram",
26
+ "whatsapp",
27
+ "teams",
28
+ ]);
29
+ export function normalizePlatform(platform) {
30
+ return KNOWN_PLATFORMS.has(platform) ? platform : "custom";
31
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=sanitize-error.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitize-error.test.d.ts","sourceRoot":"","sources":["../../src/telemetry/sanitize-error.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,33 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { errorClass, normalizePlatform } from "./sanitize-error.js";
3
+ describe("normalizePlatform", () => {
4
+ it("passes through known platforms and buckets the rest as custom", () => {
5
+ expect(normalizePlatform("slack")).toBe("slack");
6
+ expect(normalizePlatform("discord")).toBe("discord");
7
+ expect(normalizePlatform("telegram")).toBe("telegram");
8
+ expect(normalizePlatform("whatsapp")).toBe("whatsapp");
9
+ expect(normalizePlatform("teams")).toBe("teams");
10
+ // Free-form / custom adapter labels must not leak through.
11
+ expect(normalizePlatform("acme-internal-tenant")).toBe("custom");
12
+ expect(normalizePlatform("fake")).toBe("custom");
13
+ expect(normalizePlatform("")).toBe("custom");
14
+ });
15
+ });
16
+ describe("errorClass", () => {
17
+ it("never leaks the error message", () => {
18
+ const err = new Error("postgres://user:s3cret@db/prod failed");
19
+ const out = errorClass(err);
20
+ expect(out).not.toContain("s3cret");
21
+ expect(["auth", "network", "timeout", "validation", "unknown"]).toContain(out);
22
+ });
23
+ it("categorizes by name/code", () => {
24
+ expect(errorClass(new (class extends Error {
25
+ name = "AbortError";
26
+ })())).toBe("timeout");
27
+ expect(errorClass(Object.assign(new Error("x"), { code: "ENOTFOUND" }))).toBe("network");
28
+ expect(errorClass(new (class extends Error {
29
+ name = "ZodError";
30
+ })())).toBe("validation");
31
+ expect(errorClass("plain string")).toBe("unknown");
32
+ });
33
+ });
@@ -0,0 +1,137 @@
1
+ import type { BotNode, MessageRef, PlatformUser, ThreadMessage } from "@copilotkit/channels-ui";
2
+ import type { PlatformAdapter, SurfaceCapabilities, IngressSink, IncomingTurn, IncomingThreadStart, InteractionEvent, IncomingCommand, RunRenderer, ReplyTarget, NativePayload, UserQuery, ConversationStore, IncomingReaction, IncomingModalSubmit, IncomingModalClose, ModalSubmitResult } from "../platform-adapter.js";
3
+ import type { CommandSpec } from "../commands.js";
4
+ import type { StateStore } from "../state/state-store.js";
5
+ /** A RunRenderer whose subscriber captures tool-call-end and custom (interrupt) events — used by run-loop tests. */
6
+ export declare function makeFakeRunRenderer(): RunRenderer;
7
+ export declare class FakeAdapter implements PlatformAdapter {
8
+ platform: string;
9
+ readonly capabilities: SurfaceCapabilities;
10
+ readonly ackDeadlineMs = 3000;
11
+ /** When true, `start()` rejects (set via constructor `failStart`). */
12
+ readonly failStart: boolean;
13
+ /** When true, `registerCommands()` rejects (set via constructor `failRegisterCommands`). */
14
+ readonly failRegisterCommands: boolean;
15
+ /** When true, `stop()` rejects (set via constructor `failStop`). */
16
+ readonly failStop: boolean;
17
+ /** Set once `start()` has run to completion; lets tests assert a healthy adapter still started. */
18
+ started: boolean;
19
+ /**
20
+ * @param fakeOpts.paneMethods When `false`, the optional
21
+ * `setSuggestedPrompts`/`setThreadTitle` methods are omitted (and the
22
+ * matching capability flags cleared) so tests can exercise the
23
+ * capability-gated `{ ok: false }` path. Defaults to present.
24
+ * @param fakeOpts.reactions When `false`, `addReaction`/`removeReaction` are
25
+ * omitted and `supportsReactions` is cleared. Defaults to present.
26
+ * @param fakeOpts.nativeEphemeral When `true`, `postEphemeral` reports a
27
+ * native success (`usedFallback: false`) and `supportsEphemeral` is set;
28
+ * otherwise DM-fallback / `null` semantics apply and `supportsEphemeral`
29
+ * is `false`.
30
+ * @param fakeOpts.modals When `false`, `renderModal`/`openModal` are omitted
31
+ * and `supportsModals` is `false`. Defaults to present.
32
+ */
33
+ constructor(fakeOpts?: {
34
+ paneMethods?: boolean;
35
+ reactions?: boolean;
36
+ nativeEphemeral?: boolean;
37
+ modals?: boolean;
38
+ /** Platform name override (defaults to "fake"); useful when a test needs distinct adapters. */
39
+ platform?: string;
40
+ /** When true, `start()` rejects — simulates an adapter that fails to come up. */
41
+ failStart?: boolean;
42
+ /** When true, `registerCommands()` rejects — simulates a command-registration failure. */
43
+ failRegisterCommands?: boolean;
44
+ /** When true, `stop()` rejects — simulates a shutdown failure. */
45
+ failStop?: boolean;
46
+ });
47
+ readonly conversationStore: ConversationStore;
48
+ posted: BotNode[][];
49
+ updated: {
50
+ ref: MessageRef;
51
+ ir: BotNode[];
52
+ }[];
53
+ interactionsSeen: InteractionEvent[];
54
+ lastRunRenderer?: RunRenderer;
55
+ /** History returned by getMessages(); override in tests. */
56
+ messages: ThreadMessage[];
57
+ /** User returned by lookupUser(); override in tests. */
58
+ user?: PlatformUser;
59
+ /** Optional persistence backend the adapter provides (test-only); exercises createBot's store resolution. */
60
+ stateStore?: StateStore;
61
+ private sink?;
62
+ private counter;
63
+ /** Expose the registered sink so tests can invoke onTurn() directly for overlap/lock tests. */
64
+ getSink(): IngressSink;
65
+ start(sink: IngressSink): Promise<void>;
66
+ stop(): Promise<void>;
67
+ render(ir: BotNode[]): NativePayload;
68
+ post(_target: ReplyTarget, ir: BotNode[]): Promise<MessageRef>;
69
+ update(ref: MessageRef, ir: BotNode[]): Promise<void>;
70
+ stream(_target: ReplyTarget, chunks: AsyncIterable<string>): Promise<MessageRef>;
71
+ delete(_ref: MessageRef): Promise<void>;
72
+ createRunRenderer(_target: ReplyTarget): RunRenderer;
73
+ decodeInteraction(raw: unknown): InteractionEvent | undefined;
74
+ lookupUser(_q: UserQuery): Promise<PlatformUser | undefined>;
75
+ getMessages(_target: ReplyTarget): Promise<ThreadMessage[]>;
76
+ /** Suggested-prompt calls recorded by the capability-gated method (when present). */
77
+ suggestedPromptsCalls: {
78
+ target: ReplyTarget;
79
+ prompts: ReadonlyArray<{
80
+ title: string;
81
+ message: string;
82
+ }>;
83
+ opts?: {
84
+ title?: string;
85
+ };
86
+ }[];
87
+ setSuggestedPrompts?: PlatformAdapter["setSuggestedPrompts"];
88
+ /** Thread-title calls recorded by the capability-gated method (when present). */
89
+ threadTitleCalls: {
90
+ target: ReplyTarget;
91
+ title: string;
92
+ }[];
93
+ setThreadTitle?: PlatformAdapter["setThreadTitle"];
94
+ reactionsAdded: {
95
+ ref: MessageRef;
96
+ emoji: string;
97
+ }[];
98
+ reactionsRemoved: {
99
+ ref: MessageRef;
100
+ emoji: string;
101
+ }[];
102
+ addReaction?: PlatformAdapter["addReaction"];
103
+ removeReaction?: PlatformAdapter["removeReaction"];
104
+ ephemeralPosts: {
105
+ user: unknown;
106
+ ir: BotNode[];
107
+ opts: {
108
+ fallbackToDM: boolean;
109
+ };
110
+ }[];
111
+ postEphemeral?: PlatformAdapter["postEphemeral"];
112
+ openedModals: {
113
+ triggerId: string;
114
+ ir: BotNode[];
115
+ }[];
116
+ renderModal?: PlatformAdapter["renderModal"];
117
+ openModal?: PlatformAdapter["openModal"];
118
+ emitTurn(partial: Partial<IncomingTurn>): void;
119
+ emitThreadStarted(partial?: Partial<IncomingThreadStart>): Promise<void> | void;
120
+ emitInteraction(partial: Partial<InteractionEvent>): void;
121
+ emitCommand(partial: Partial<IncomingCommand> & {
122
+ command: string;
123
+ }): Promise<void> | void;
124
+ emitReaction(partial: Partial<IncomingReaction> & {
125
+ rawEmoji: string;
126
+ }): Promise<void> | void;
127
+ emitModalSubmit(partial: Partial<IncomingModalSubmit> & {
128
+ callbackId: string;
129
+ }): Promise<ModalSubmitResult | void> | undefined;
130
+ emitModalClose(partial: Partial<IncomingModalClose> & {
131
+ callbackId: string;
132
+ }): Promise<void> | void;
133
+ /** Commands handed to the adapter via `registerCommands`; asserts the capability hook fires. */
134
+ registeredCommands?: readonly CommandSpec[];
135
+ registerCommands(commands: readonly CommandSpec[]): Promise<void>;
136
+ }
137
+ //# sourceMappingURL=fake-adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fake-adapter.d.ts","sourceRoot":"","sources":["../../src/testing/fake-adapter.ts"],"names":[],"mappings":"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"}