@copilotkit/channels-discord 0.0.2
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/LICENSE +21 -0
- package/README.md +308 -0
- package/dist/__tests__/ephemeral.test.d.ts +2 -0
- package/dist/__tests__/ephemeral.test.d.ts.map +1 -0
- package/dist/__tests__/ephemeral.test.js +26 -0
- package/dist/__tests__/intents.test.d.ts +2 -0
- package/dist/__tests__/intents.test.d.ts.map +1 -0
- package/dist/__tests__/intents.test.js +8 -0
- package/dist/__tests__/modal-submit.test.d.ts +2 -0
- package/dist/__tests__/modal-submit.test.d.ts.map +1 -0
- package/dist/__tests__/modal-submit.test.js +26 -0
- package/dist/__tests__/reactions.test.d.ts +2 -0
- package/dist/__tests__/reactions.test.d.ts.map +1 -0
- package/dist/__tests__/reactions.test.js +87 -0
- package/dist/adapter.d.ts +118 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +567 -0
- package/dist/adapter.test.d.ts +2 -0
- package/dist/adapter.test.d.ts.map +1 -0
- package/dist/adapter.test.js +343 -0
- package/dist/auto-close-streaming.d.ts +61 -0
- package/dist/auto-close-streaming.d.ts.map +1 -0
- package/dist/auto-close-streaming.js +276 -0
- package/dist/auto-close-streaming.test.d.ts +2 -0
- package/dist/auto-close-streaming.test.d.ts.map +1 -0
- package/dist/auto-close-streaming.test.js +149 -0
- package/dist/built-in-context.d.ts +20 -0
- package/dist/built-in-context.d.ts.map +1 -0
- package/dist/built-in-context.js +30 -0
- package/dist/built-in-context.test.d.ts +2 -0
- package/dist/built-in-context.test.d.ts.map +1 -0
- package/dist/built-in-context.test.js +21 -0
- package/dist/built-in-tools.d.ts +4 -0
- package/dist/built-in-tools.d.ts.map +1 -0
- package/dist/built-in-tools.js +27 -0
- package/dist/built-in-tools.test.d.ts +2 -0
- package/dist/built-in-tools.test.d.ts.map +1 -0
- package/dist/built-in-tools.test.js +30 -0
- package/dist/chunked-message-stream.d.ts +94 -0
- package/dist/chunked-message-stream.d.ts.map +1 -0
- package/dist/chunked-message-stream.js +203 -0
- package/dist/chunked-message-stream.test.d.ts +2 -0
- package/dist/chunked-message-stream.test.d.ts.map +1 -0
- package/dist/chunked-message-stream.test.js +384 -0
- package/dist/commands.d.ts +29 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +193 -0
- package/dist/commands.test.d.ts +2 -0
- package/dist/commands.test.d.ts.map +1 -0
- package/dist/commands.test.js +281 -0
- package/dist/conversation-store.d.ts +45 -0
- package/dist/conversation-store.d.ts.map +1 -0
- package/dist/conversation-store.js +78 -0
- package/dist/conversation-store.test.d.ts +2 -0
- package/dist/conversation-store.test.d.ts.map +1 -0
- package/dist/conversation-store.test.js +106 -0
- package/dist/discord-listener.d.ts +88 -0
- package/dist/discord-listener.d.ts.map +1 -0
- package/dist/discord-listener.js +118 -0
- package/dist/discord-listener.test.d.ts +2 -0
- package/dist/discord-listener.test.d.ts.map +1 -0
- package/dist/discord-listener.test.js +361 -0
- package/dist/download-files.d.ts +94 -0
- package/dist/download-files.d.ts.map +1 -0
- package/dist/download-files.js +145 -0
- package/dist/download-files.test.d.ts +2 -0
- package/dist/download-files.test.d.ts.map +1 -0
- package/dist/download-files.test.js +181 -0
- package/dist/event-renderer.d.ts +36 -0
- package/dist/event-renderer.d.ts.map +1 -0
- package/dist/event-renderer.js +270 -0
- package/dist/event-renderer.test.d.ts +2 -0
- package/dist/event-renderer.test.d.ts.map +1 -0
- package/dist/event-renderer.test.js +160 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/interaction.d.ts +11 -0
- package/dist/interaction.d.ts.map +1 -0
- package/dist/interaction.js +138 -0
- package/dist/interaction.test.d.ts +2 -0
- package/dist/interaction.test.d.ts.map +1 -0
- package/dist/interaction.test.js +120 -0
- package/dist/markdown.d.ts +7 -0
- package/dist/markdown.d.ts.map +1 -0
- package/dist/markdown.js +66 -0
- package/dist/markdown.test.d.ts +2 -0
- package/dist/markdown.test.d.ts.map +1 -0
- package/dist/markdown.test.js +47 -0
- package/dist/message-stream.d.ts +45 -0
- package/dist/message-stream.d.ts.map +1 -0
- package/dist/message-stream.js +76 -0
- package/dist/message-stream.test.d.ts +2 -0
- package/dist/message-stream.test.d.ts.map +1 -0
- package/dist/message-stream.test.js +162 -0
- package/dist/pending-interactions.d.ts +26 -0
- package/dist/pending-interactions.d.ts.map +1 -0
- package/dist/pending-interactions.js +50 -0
- package/dist/pending-interactions.test.d.ts +2 -0
- package/dist/pending-interactions.test.d.ts.map +1 -0
- package/dist/pending-interactions.test.js +73 -0
- package/dist/render/budget.d.ts +28 -0
- package/dist/render/budget.d.ts.map +1 -0
- package/dist/render/budget.js +49 -0
- package/dist/render/budget.test.d.ts +2 -0
- package/dist/render/budget.test.d.ts.map +1 -0
- package/dist/render/budget.test.js +59 -0
- package/dist/render/components-v2.d.ts +16 -0
- package/dist/render/components-v2.d.ts.map +1 -0
- package/dist/render/components-v2.js +396 -0
- package/dist/render/components-v2.test.d.ts +2 -0
- package/dist/render/components-v2.test.d.ts.map +1 -0
- package/dist/render/components-v2.test.js +395 -0
- package/dist/render/modal.d.ts +12 -0
- package/dist/render/modal.d.ts.map +1 -0
- package/dist/render/modal.js +44 -0
- package/dist/types.d.ts +16 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/dist/types.test.d.ts +2 -0
- package/dist/types.test.d.ts.map +1 -0
- package/dist/types.test.js +12 -0
- package/package.json +56 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auto-close-streaming.test.d.ts","sourceRoot":"","sources":["../src/auto-close-streaming.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { autoCloseOpenMarkdown } from "./auto-close-streaming.js";
|
|
3
|
+
describe("autoCloseOpenMarkdown", () => {
|
|
4
|
+
// ── No-op cases ──────────────────────────────────────────────────
|
|
5
|
+
it("returns empty string unchanged", () => {
|
|
6
|
+
expect(autoCloseOpenMarkdown("")).toBe("");
|
|
7
|
+
});
|
|
8
|
+
it("returns plain text unchanged", () => {
|
|
9
|
+
expect(autoCloseOpenMarkdown("just plain text 123")).toBe("just plain text 123");
|
|
10
|
+
});
|
|
11
|
+
it("does not modify already-balanced markdown", () => {
|
|
12
|
+
expect(autoCloseOpenMarkdown("**bold** and *italic* and ~~strike~~")).toBe("**bold** and *italic* and ~~strike~~");
|
|
13
|
+
});
|
|
14
|
+
// ── Bold ──────────────────────────────────────────────────────────
|
|
15
|
+
it("closes an unbalanced **opening — `**hello` → `**hello**`", () => {
|
|
16
|
+
expect(autoCloseOpenMarkdown("**hello")).toBe("**hello**");
|
|
17
|
+
});
|
|
18
|
+
it("does NOT close a `**` with nothing after it", () => {
|
|
19
|
+
// Just opened — closing would render as `****`, which looks worse.
|
|
20
|
+
expect(autoCloseOpenMarkdown("hi **")).toBe("hi **");
|
|
21
|
+
});
|
|
22
|
+
it("closes `__bold` as `__bold__`", () => {
|
|
23
|
+
expect(autoCloseOpenMarkdown("__bold")).toBe("__bold__");
|
|
24
|
+
});
|
|
25
|
+
// ── Italic ───────────────────────────────────────────────────────
|
|
26
|
+
it("closes `*italic` → `*italic*`", () => {
|
|
27
|
+
expect(autoCloseOpenMarkdown("*italic")).toBe("*italic*");
|
|
28
|
+
});
|
|
29
|
+
it("closes `_italic` → `_italic_`", () => {
|
|
30
|
+
expect(autoCloseOpenMarkdown("_italic")).toBe("_italic_");
|
|
31
|
+
});
|
|
32
|
+
it("does NOT close a `*` with nothing after it", () => {
|
|
33
|
+
expect(autoCloseOpenMarkdown("text *")).toBe("text *");
|
|
34
|
+
});
|
|
35
|
+
it("closes an open italic `*` that precedes an empty trailing `**` — `*ab**`", () => {
|
|
36
|
+
// The trailing `**` is empty (just opened) and is dropped, but the
|
|
37
|
+
// leading `*ab` italic has real content and must still be closed.
|
|
38
|
+
// Regression: lastIndexOf("*") used to match the `*` inside `**`,
|
|
39
|
+
// wrongly treating the italic as empty and leaving it unbalanced.
|
|
40
|
+
expect(autoCloseOpenMarkdown("*ab**")).toBe("*ab***");
|
|
41
|
+
});
|
|
42
|
+
it("closes an open underscore-italic `_` that precedes an empty trailing `__` — `_ab__`", () => {
|
|
43
|
+
expect(autoCloseOpenMarkdown("_ab__")).toBe("_ab___");
|
|
44
|
+
});
|
|
45
|
+
// ── Strike ───────────────────────────────────────────────────────
|
|
46
|
+
it("closes `~~strike` → `~~strike~~`", () => {
|
|
47
|
+
expect(autoCloseOpenMarkdown("~~strike")).toBe("~~strike~~");
|
|
48
|
+
});
|
|
49
|
+
// ── Inline code ──────────────────────────────────────────────────
|
|
50
|
+
it("closes `` `code `` → `` `code` ``", () => {
|
|
51
|
+
expect(autoCloseOpenMarkdown("`code")).toBe("`code`");
|
|
52
|
+
});
|
|
53
|
+
it("doesn't touch content inside paired inline code", () => {
|
|
54
|
+
// `code with *stars*` — the stars are inside code, so no italic close.
|
|
55
|
+
expect(autoCloseOpenMarkdown("`code *stars*` and more")).toBe("`code *stars*` and more");
|
|
56
|
+
});
|
|
57
|
+
it("treats unbalanced markers INSIDE inline code as opaque", () => {
|
|
58
|
+
// `**foo` is inside an unclosed inline → we close the backtick, but
|
|
59
|
+
// don't try to close the `**` inside code.
|
|
60
|
+
expect(autoCloseOpenMarkdown("see `**foo")).toBe("see `**foo`");
|
|
61
|
+
});
|
|
62
|
+
// ── Fenced code ──────────────────────────────────────────────────
|
|
63
|
+
it("closes an open fence — adds `\\n```` after the content", () => {
|
|
64
|
+
const input = "```py\ndef foo():";
|
|
65
|
+
const out = autoCloseOpenMarkdown(input);
|
|
66
|
+
expect(out.startsWith(input)).toBe(true);
|
|
67
|
+
expect(out.trim().endsWith("```")).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
it("does NOT close a bare ``` with nothing after it (just opened)", () => {
|
|
70
|
+
expect(autoCloseOpenMarkdown("```")).toBe("```");
|
|
71
|
+
expect(autoCloseOpenMarkdown("```py")).toBe("```py"); // language tag alone
|
|
72
|
+
});
|
|
73
|
+
it("balanced fences left untouched", () => {
|
|
74
|
+
const input = "before\n```\ncode\n```\nafter";
|
|
75
|
+
expect(autoCloseOpenMarkdown(input)).toBe(input);
|
|
76
|
+
});
|
|
77
|
+
it("balanced fence + unbalanced markers in surrounding text close correctly", () => {
|
|
78
|
+
// The fence is paired; the trailing `**hel` outside the fence needs close.
|
|
79
|
+
const input = "**hel ```code``` more text";
|
|
80
|
+
const out = autoCloseOpenMarkdown(input);
|
|
81
|
+
expect(out).toContain("**hel");
|
|
82
|
+
expect(out).toContain("```code```");
|
|
83
|
+
expect(out).toContain("**"); // close
|
|
84
|
+
});
|
|
85
|
+
it("markers inside paired fences don't count toward balance", () => {
|
|
86
|
+
// Inside the fence we have `**oops`, but the markdown text is balanced.
|
|
87
|
+
const input = "```\n**oops never closes\n``` after";
|
|
88
|
+
expect(autoCloseOpenMarkdown(input)).toBe(input);
|
|
89
|
+
});
|
|
90
|
+
// ── Nesting ──────────────────────────────────────────────────────
|
|
91
|
+
it("closes innermost first — `**bold _italic` → `**bold _italic_**`", () => {
|
|
92
|
+
expect(autoCloseOpenMarkdown("**bold _italic")).toBe("**bold _italic_**");
|
|
93
|
+
});
|
|
94
|
+
it("doesn't double-close already-balanced inner markers", () => {
|
|
95
|
+
expect(autoCloseOpenMarkdown("**bold _italic_ tail")).toBe("**bold _italic_ tail**");
|
|
96
|
+
});
|
|
97
|
+
// ── Stream-evolution: simulate consecutive deltas; the agent's eventual
|
|
98
|
+
// close should NOT cause double-closes in the produced text. ──
|
|
99
|
+
it("stream-evolution: agent eventually closes; auto-close adds nothing at that point", () => {
|
|
100
|
+
const states = [
|
|
101
|
+
"**h",
|
|
102
|
+
"**he",
|
|
103
|
+
"**hel",
|
|
104
|
+
"**hell",
|
|
105
|
+
"**hello",
|
|
106
|
+
"**hello*",
|
|
107
|
+
"**hello**",
|
|
108
|
+
];
|
|
109
|
+
const out = states.map(autoCloseOpenMarkdown);
|
|
110
|
+
// Intermediate states are closed
|
|
111
|
+
expect(out[0]).toBe("**h**");
|
|
112
|
+
expect(out[4]).toBe("**hello**");
|
|
113
|
+
// Final state is unchanged (already balanced)
|
|
114
|
+
expect(out[out.length - 1]).toBe("**hello**");
|
|
115
|
+
});
|
|
116
|
+
it("stream-evolution for fenced code: closes mid-stream; goes silent on real close", () => {
|
|
117
|
+
const states = [
|
|
118
|
+
"```py\n",
|
|
119
|
+
"```py\ndef ",
|
|
120
|
+
"```py\ndef foo():",
|
|
121
|
+
"```py\ndef foo():\n pass\n```",
|
|
122
|
+
];
|
|
123
|
+
const out = states.map(autoCloseOpenMarkdown);
|
|
124
|
+
// First state: just opened — no content yet, don't auto-close
|
|
125
|
+
expect(out[0]).toBe("```py\n");
|
|
126
|
+
// Middle states: closed
|
|
127
|
+
expect(out[1].trim().endsWith("```")).toBe(true);
|
|
128
|
+
expect(out[2].trim().endsWith("```")).toBe(true);
|
|
129
|
+
// Final: agent emitted the close, balanced, no change
|
|
130
|
+
expect(out[3]).toBe(states[3]);
|
|
131
|
+
});
|
|
132
|
+
// ── Combined ────────────────────────────────────────────────────
|
|
133
|
+
it("closes bold before an unclosed fence opener, and closes the fence too", () => {
|
|
134
|
+
// `**bold ` is open bold then a space; `\`\`\`code\n` opens a fence with
|
|
135
|
+
// content. We close bold (with closer inserted before the trailing
|
|
136
|
+
// space, so the space stays adjacent to the fence) and close the fence.
|
|
137
|
+
const input = "**bold ```py\ndef foo():";
|
|
138
|
+
const out = autoCloseOpenMarkdown(input);
|
|
139
|
+
expect(out).toContain("**bold**"); // bold closed before fence
|
|
140
|
+
expect(out).toContain("def foo():"); // fence content preserved
|
|
141
|
+
expect(out.trim().endsWith("```")).toBe(true); // fence closed
|
|
142
|
+
});
|
|
143
|
+
// ── Sanity: long input ─────────────────────────────────────────
|
|
144
|
+
it("handles a long buffer with mixed structures", () => {
|
|
145
|
+
const lots = "**Bold opener** and `code` and *italic* and `unclosed inline";
|
|
146
|
+
const out = autoCloseOpenMarkdown(lots);
|
|
147
|
+
expect(out).toBe("**Bold opener** and `code` and *italic* and `unclosed inline`");
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discord-platform-universal context entries — knowledge the LLM needs
|
|
3
|
+
* about Discord itself (tagging procedure, formatting, conversation
|
|
4
|
+
* model). Apps spread `defaultDiscordContext` into the `context:` config
|
|
5
|
+
* they pass to `createBot`.
|
|
6
|
+
*
|
|
7
|
+
* Each entry is exported individually too so apps can cherry-pick.
|
|
8
|
+
*/
|
|
9
|
+
import type { ContextEntry } from "@copilotkit/channels";
|
|
10
|
+
export declare const discordTaggingContext: ContextEntry;
|
|
11
|
+
export declare const discordFormattingContext: ContextEntry;
|
|
12
|
+
export declare const discordConversationModelContext: ContextEntry;
|
|
13
|
+
/**
|
|
14
|
+
* The default context entries the SDK ships. Spread into your
|
|
15
|
+
* `createBot({context: …})`:
|
|
16
|
+
*
|
|
17
|
+
* context: [...defaultDiscordContext, ...myAppContext],
|
|
18
|
+
*/
|
|
19
|
+
export declare const defaultDiscordContext: ContextEntry[];
|
|
20
|
+
//# sourceMappingURL=built-in-context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"built-in-context.d.ts","sourceRoot":"","sources":["../src/built-in-context.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEzD,eAAO,MAAM,qBAAqB,EAAE,YAMnC,CAAC;AAEF,eAAO,MAAM,wBAAwB,EAAE,YAOtC,CAAC;AAEF,eAAO,MAAM,+BAA+B,EAAE,YAM7C,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,EAAE,YAAY,EAI/C,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const discordTaggingContext = {
|
|
2
|
+
description: "How to tag/mention people in Discord",
|
|
3
|
+
value: "To mention a user, use <@USER_ID> (numeric id). If you only know a name or " +
|
|
4
|
+
"handle, call lookup_discord_user to resolve it to an id first, then mention " +
|
|
5
|
+
"with <@id>. Mention a channel with <#CHANNEL_ID>.",
|
|
6
|
+
};
|
|
7
|
+
export const discordFormattingContext = {
|
|
8
|
+
description: "Discord message formatting",
|
|
9
|
+
value: "Use standard Markdown: **bold**, *italic*, `code`, ```fenced``` blocks, > quotes, " +
|
|
10
|
+
"and # / ## / ### headings. Discord renders NO tables — never output a pipe table; " +
|
|
11
|
+
"put tabular data in a fenced code block or call the appropriate render tool. " +
|
|
12
|
+
"Mentions use <@USER_ID> and channels <#CHANNEL_ID>.",
|
|
13
|
+
};
|
|
14
|
+
export const discordConversationModelContext = {
|
|
15
|
+
description: "The Discord conversation model",
|
|
16
|
+
value: "Each channel, thread, and DM is one conversation. You reply in the same channel " +
|
|
17
|
+
"the message arrived in. Use the read-history tool to see prior messages when you " +
|
|
18
|
+
"need context from earlier in the conversation.",
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* The default context entries the SDK ships. Spread into your
|
|
22
|
+
* `createBot({context: …})`:
|
|
23
|
+
*
|
|
24
|
+
* context: [...defaultDiscordContext, ...myAppContext],
|
|
25
|
+
*/
|
|
26
|
+
export const defaultDiscordContext = [
|
|
27
|
+
discordTaggingContext,
|
|
28
|
+
discordFormattingContext,
|
|
29
|
+
discordConversationModelContext,
|
|
30
|
+
];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"built-in-context.test.d.ts","sourceRoot":"","sources":["../src/built-in-context.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { discordFormattingContext, discordConversationModelContext, discordTaggingContext, defaultDiscordContext, } from "./built-in-context.js";
|
|
3
|
+
describe("defaultDiscordContext", () => {
|
|
4
|
+
it("bundles the three context entries", () => {
|
|
5
|
+
expect(defaultDiscordContext).toEqual([
|
|
6
|
+
discordTaggingContext,
|
|
7
|
+
discordFormattingContext,
|
|
8
|
+
discordConversationModelContext,
|
|
9
|
+
]);
|
|
10
|
+
});
|
|
11
|
+
it("each entry has a description and value", () => {
|
|
12
|
+
for (const e of defaultDiscordContext) {
|
|
13
|
+
expect(typeof e.description).toBe("string");
|
|
14
|
+
expect(e.value.length).toBeGreaterThan(0);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
it("formatting guidance warns there are no tables and gives mention syntax", () => {
|
|
18
|
+
expect(discordFormattingContext.value).toMatch(/table/i);
|
|
19
|
+
expect(discordFormattingContext.value).toContain("<@");
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"built-in-tools.d.ts","sourceRoot":"","sources":["../src/built-in-tools.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAEpD,eAAO,MAAM,qBAAqB,EAAE,OAsBlC,CAAC;AAEH,eAAO,MAAM,mBAAmB,EAAE,OAAO,EAA4B,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineBotTool } from "@copilotkit/channels";
|
|
3
|
+
export const lookupDiscordUserTool = defineBotTool({
|
|
4
|
+
name: "lookup_discord_user",
|
|
5
|
+
description: "Resolve a person's name, display name, or handle to a Discord user id and a " +
|
|
6
|
+
"ready-to-use <@id> mention.",
|
|
7
|
+
parameters: z.object({
|
|
8
|
+
query: z
|
|
9
|
+
.string()
|
|
10
|
+
.min(1)
|
|
11
|
+
.describe("A name, display name, or handle to resolve."),
|
|
12
|
+
}),
|
|
13
|
+
async handler({ query }, { thread }) {
|
|
14
|
+
let user;
|
|
15
|
+
try {
|
|
16
|
+
user = await thread.lookupUser(query);
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
console.error(`lookup_discord_user failed for "${query}":`, error);
|
|
20
|
+
return `Couldn't resolve a Discord user for "${query}" (lookup unavailable).`;
|
|
21
|
+
}
|
|
22
|
+
if (!user)
|
|
23
|
+
return `No Discord user found matching "${query}".`;
|
|
24
|
+
return { id: user.id, name: user.name, mention: `<@${user.id}>` };
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
export const defaultDiscordTools = [lookupDiscordUserTool];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"built-in-tools.test.d.ts","sourceRoot":"","sources":["../src/built-in-tools.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { lookupDiscordUserTool, defaultDiscordTools, } from "./built-in-tools.js";
|
|
3
|
+
describe("lookupDiscordUserTool", () => {
|
|
4
|
+
it("resolves a query via thread.lookupUser and returns a mention", async () => {
|
|
5
|
+
const thread = {
|
|
6
|
+
lookupUser: vi.fn(async () => ({ id: "u9", name: "Ann", handle: "ann" })),
|
|
7
|
+
};
|
|
8
|
+
const res = await lookupDiscordUserTool.handler({ query: "ann" }, {
|
|
9
|
+
thread,
|
|
10
|
+
});
|
|
11
|
+
expect(thread.lookupUser).toHaveBeenCalledWith("ann");
|
|
12
|
+
expect(JSON.stringify(res)).toContain("<@u9>");
|
|
13
|
+
});
|
|
14
|
+
it("returns a graceful string when thread.lookupUser rejects", async () => {
|
|
15
|
+
const thread = {
|
|
16
|
+
lookupUser: vi.fn(async () => {
|
|
17
|
+
throw new Error("intent missing");
|
|
18
|
+
}),
|
|
19
|
+
};
|
|
20
|
+
const res = await lookupDiscordUserTool.handler({ query: "ann" }, {
|
|
21
|
+
thread,
|
|
22
|
+
});
|
|
23
|
+
expect(thread.lookupUser).toHaveBeenCalledWith("ann");
|
|
24
|
+
expect(typeof res).toBe("string");
|
|
25
|
+
expect(res).toContain("lookup unavailable");
|
|
26
|
+
});
|
|
27
|
+
it("is included in defaultDiscordTools", () => {
|
|
28
|
+
expect(defaultDiscordTools).toContain(lookupDiscordUserTool);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Placeholder texts posted for streaming messages: the first chunk shows
|
|
3
|
+
* "_thinking…_" and later (continuation) chunks show "_…(continued)_" until
|
|
4
|
+
* real content lands. Exported as a shared const so the producer here and the
|
|
5
|
+
* history filter in adapter.ts (which excludes the bot's own placeholders from
|
|
6
|
+
* `read_thread`) never drift apart. The `…` is a real U+2026 ellipsis.
|
|
7
|
+
*/
|
|
8
|
+
export declare const STREAM_PLACEHOLDERS: readonly ["_thinking…_", "_…(continued)_"];
|
|
9
|
+
/**
|
|
10
|
+
* Discord's HARD per-message content limit is 2000 chars. For longer agent
|
|
11
|
+
* responses we spread the text over several Discord messages — but the
|
|
12
|
+
* *chunk boundaries* must be stable once committed (we can't reflow a
|
|
13
|
+
* sentence into a previous chunk after that chunk has already been posted
|
|
14
|
+
* to Discord).
|
|
15
|
+
*
|
|
16
|
+
* ChunkedMessageStream owns N underlying `MessageStream`s (one per Discord
|
|
17
|
+
* message). It accepts the full accumulated text on `append()`, decides
|
|
18
|
+
* where the chunk boundaries are, mints new Discord messages as needed,
|
|
19
|
+
* and dispatches the right slice to each chunk's stream.
|
|
20
|
+
*
|
|
21
|
+
* Boundaries are chosen greedily: once an active chunk exceeds the soft
|
|
22
|
+
* limit, freeze the boundary at the last newline (or last space) within
|
|
23
|
+
* the limit; remaining text becomes the next chunk. Boundaries don't
|
|
24
|
+
* move once frozen, so an already-posted chunk's text never shrinks.
|
|
25
|
+
*/
|
|
26
|
+
export interface ChunkedMessageStreamConfig {
|
|
27
|
+
/**
|
|
28
|
+
* Soft per-message char limit used to choose chunk boundaries. Defaults to
|
|
29
|
+
* 1900 — deliberately below Discord's 2000 hard limit so the per-chunk
|
|
30
|
+
* `transform` has headroom to grow the text without overflowing. The
|
|
31
|
+
* transform appends a fence/marker closer (autoCloseOpenMarkdown) and, for
|
|
32
|
+
* continuation chunks, prepends a re-opener (e.g. "```longlangname\n"); a
|
|
33
|
+
* chunk sliced to exactly 2000 raw chars would exceed 2000 once transformed
|
|
34
|
+
* and Discord would reject the edit with BASE_TYPE_MAX_LENGTH.
|
|
35
|
+
*/
|
|
36
|
+
limit?: number;
|
|
37
|
+
/** Throttle floor for each underlying stream's message edit. */
|
|
38
|
+
minIntervalMs?: number;
|
|
39
|
+
/** Posts a new Discord message with placeholder text; resolves with its id. */
|
|
40
|
+
postPlaceholder: (text: string) => Promise<string>;
|
|
41
|
+
/** Updates the Discord message at `id` with `text`. */
|
|
42
|
+
updateAt: (id: string, text: string) => Promise<void>;
|
|
43
|
+
/**
|
|
44
|
+
* Optional transformer for the text *just before* it hits the edit call —
|
|
45
|
+
* e.g. markdown translation. Applied per-chunk.
|
|
46
|
+
* Defaults to `discordMarkdown`.
|
|
47
|
+
*/
|
|
48
|
+
transform?: (text: string) => string;
|
|
49
|
+
}
|
|
50
|
+
export declare class ChunkedMessageStream {
|
|
51
|
+
private buffer;
|
|
52
|
+
/** Sorted positions where a chunk ends (= where the next chunk begins). */
|
|
53
|
+
private boundaries;
|
|
54
|
+
private streams;
|
|
55
|
+
/** Serialises new-chunk creation so async postPlaceholder calls don't race. */
|
|
56
|
+
private setupPromise;
|
|
57
|
+
/**
|
|
58
|
+
* Records the first setup-chain failure (e.g. a rejecting `postPlaceholder`).
|
|
59
|
+
* The chain itself is kept rejection-free via a `.catch` so no interim
|
|
60
|
+
* unhandled promise rejection is emitted; the stored error is rethrown
|
|
61
|
+
* deterministically at the next `append`/`finish`.
|
|
62
|
+
*/
|
|
63
|
+
private setupError;
|
|
64
|
+
private finished;
|
|
65
|
+
private readonly limit;
|
|
66
|
+
private readonly minIntervalMs;
|
|
67
|
+
private readonly postPlaceholder;
|
|
68
|
+
private readonly updateAt;
|
|
69
|
+
private readonly transform;
|
|
70
|
+
constructor(config: ChunkedMessageStreamConfig);
|
|
71
|
+
append(fullText: string): void;
|
|
72
|
+
finish(): Promise<void>;
|
|
73
|
+
/** Wraps the recorded setup failure with context for surfacing to callers. */
|
|
74
|
+
private asSetupError;
|
|
75
|
+
/** Returns the number of Discord messages this stream has posted so far. */
|
|
76
|
+
get chunkCount(): number;
|
|
77
|
+
/**
|
|
78
|
+
* Walk forward from the last frozen boundary, freezing new ones whenever
|
|
79
|
+
* the active chunk's length exceeds the soft limit. Once frozen, a
|
|
80
|
+
* boundary doesn't move.
|
|
81
|
+
*
|
|
82
|
+
* Special case (block-keeps-whole): if the chosen boundary lands INSIDE
|
|
83
|
+
* an open fenced code block, we try to move the boundary BACK to the
|
|
84
|
+
* position right before the fence opener, so the *whole* block lives in
|
|
85
|
+
* the next Discord message rather than being split. The previous chunk
|
|
86
|
+
* gets shortened (message edit will update it down). Fallback when the
|
|
87
|
+
* block is too large to fit in one chunk: keep the inside-fence
|
|
88
|
+
* boundary; the dispatcher will prepend the fence opener to the next
|
|
89
|
+
* chunk (re-opener path).
|
|
90
|
+
*/
|
|
91
|
+
private refreezeBoundaries;
|
|
92
|
+
private ensureStreamsAndDispatch;
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=chunked-message-stream.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chunked-message-stream.d.ts","sourceRoot":"","sources":["../src/chunked-message-stream.ts"],"names":[],"mappings":"AAUA;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,4CAA6C,CAAC;AAyC9E;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,0BAA0B;IACzC;;;;;;;;OAQG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,+EAA+E;IAC/E,eAAe,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACnD,uDAAuD;IACvD,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtD;;;;OAIG;IACH,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CACtC;AAED,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,MAAM,CAAM;IACpB,2EAA2E;IAC3E,OAAO,CAAC,UAAU,CAAgB;IAClC,OAAO,CAAC,OAAO,CAAuB;IACtC,+EAA+E;IAC/E,OAAO,CAAC,YAAY,CAAoC;IACxD;;;;;OAKG;IACH,OAAO,CAAC,UAAU,CAAsB;IACxC,OAAO,CAAC,QAAQ,CAAS;IAEzB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAqB;IACnD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAoC;IACpE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA8C;IACvE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA2B;gBAEzC,MAAM,EAAE,0BAA0B;IAQ9C,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAmBxB,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAe7B,8EAA8E;IAC9E,OAAO,CAAC,YAAY;IAKpB,4EAA4E;IAC5E,IAAI,UAAU,IAAI,MAAM,CAEvB;IAED;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,kBAAkB;YA0BZ,wBAAwB;CAwCvC"}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { MessageStream } from "./message-stream.js";
|
|
2
|
+
import { detectOpenContext, renderContextOpener, } from "./auto-close-streaming.js";
|
|
3
|
+
import { discordMarkdown } from "./markdown.js";
|
|
4
|
+
/** Discord's hard per-message content limit. Edits above this are rejected. */
|
|
5
|
+
const DISCORD_HARD_LIMIT = 2000;
|
|
6
|
+
/**
|
|
7
|
+
* Placeholder texts posted for streaming messages: the first chunk shows
|
|
8
|
+
* "_thinking…_" and later (continuation) chunks show "_…(continued)_" until
|
|
9
|
+
* real content lands. Exported as a shared const so the producer here and the
|
|
10
|
+
* history filter in adapter.ts (which excludes the bot's own placeholders from
|
|
11
|
+
* `read_thread`) never drift apart. The `…` is a real U+2026 ellipsis.
|
|
12
|
+
*/
|
|
13
|
+
export const STREAM_PLACEHOLDERS = ["_thinking…_", "_…(continued)_"];
|
|
14
|
+
/**
|
|
15
|
+
* Soft chunk limit. Sits ~100 chars below the hard limit to leave headroom
|
|
16
|
+
* for the per-chunk transform, which can both append a closer ("\n```") and
|
|
17
|
+
* prepend a continuation re-opener ("```longlangname\n").
|
|
18
|
+
*/
|
|
19
|
+
const DEFAULT_LIMIT = 1900;
|
|
20
|
+
/**
|
|
21
|
+
* Last-resort safety net: guarantee a transformed chunk never exceeds
|
|
22
|
+
* Discord's hard limit. The 1900-char soft limit is the primary defence;
|
|
23
|
+
* this clamp only fires in pathological cases (e.g. an enormous re-opener
|
|
24
|
+
* language tag). If truncation severs an open code fence, re-append a
|
|
25
|
+
* closing fence so the message still renders as a balanced block — at the
|
|
26
|
+
* cost of dropping the overflow characters, which is preferable to a
|
|
27
|
+
* rejected edit that freezes the chunk on its placeholder.
|
|
28
|
+
*/
|
|
29
|
+
function clampToHardLimit(text) {
|
|
30
|
+
if (text.length <= DISCORD_HARD_LIMIT)
|
|
31
|
+
return text;
|
|
32
|
+
let clamped = text.slice(0, DISCORD_HARD_LIMIT);
|
|
33
|
+
// Odd number of triple-backticks → an open fence was severed. Reserve room
|
|
34
|
+
// for a "\n```" closer (4 chars) and re-append it to keep the fence balanced.
|
|
35
|
+
if ((clamped.match(/```/g) || []).length % 2 === 1) {
|
|
36
|
+
clamped = clamped.slice(0, DISCORD_HARD_LIMIT - 4) + "\n```";
|
|
37
|
+
}
|
|
38
|
+
return clamped;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Position (0-based) of the unpaired opening ``` in `buffer.slice(0, end)`,
|
|
42
|
+
* or null if all triple-backticks before `end` are paired.
|
|
43
|
+
*/
|
|
44
|
+
function findUnpairedFenceStart(buffer, end) {
|
|
45
|
+
const before = buffer.slice(0, end);
|
|
46
|
+
const parts = before.split("```");
|
|
47
|
+
// length is odd → balanced; even → one unpaired opener exists
|
|
48
|
+
if (parts.length % 2 !== 0)
|
|
49
|
+
return null;
|
|
50
|
+
return before.lastIndexOf("```");
|
|
51
|
+
}
|
|
52
|
+
export class ChunkedMessageStream {
|
|
53
|
+
buffer = "";
|
|
54
|
+
/** Sorted positions where a chunk ends (= where the next chunk begins). */
|
|
55
|
+
boundaries = [];
|
|
56
|
+
streams = [];
|
|
57
|
+
/** Serialises new-chunk creation so async postPlaceholder calls don't race. */
|
|
58
|
+
setupPromise = Promise.resolve();
|
|
59
|
+
/**
|
|
60
|
+
* Records the first setup-chain failure (e.g. a rejecting `postPlaceholder`).
|
|
61
|
+
* The chain itself is kept rejection-free via a `.catch` so no interim
|
|
62
|
+
* unhandled promise rejection is emitted; the stored error is rethrown
|
|
63
|
+
* deterministically at the next `append`/`finish`.
|
|
64
|
+
*/
|
|
65
|
+
setupError = undefined;
|
|
66
|
+
finished = false;
|
|
67
|
+
limit;
|
|
68
|
+
minIntervalMs;
|
|
69
|
+
postPlaceholder;
|
|
70
|
+
updateAt;
|
|
71
|
+
transform;
|
|
72
|
+
constructor(config) {
|
|
73
|
+
this.limit = config.limit ?? DEFAULT_LIMIT;
|
|
74
|
+
this.minIntervalMs = config.minIntervalMs;
|
|
75
|
+
this.postPlaceholder = config.postPlaceholder;
|
|
76
|
+
this.updateAt = config.updateAt;
|
|
77
|
+
this.transform = config.transform ?? discordMarkdown;
|
|
78
|
+
}
|
|
79
|
+
append(fullText) {
|
|
80
|
+
if (this.finished)
|
|
81
|
+
return;
|
|
82
|
+
// A prior setup-chain failure is fatal for the stream — surface it
|
|
83
|
+
// synchronously here rather than chaining more work onto a doomed promise.
|
|
84
|
+
if (this.setupError !== undefined)
|
|
85
|
+
throw this.asSetupError();
|
|
86
|
+
if (fullText === this.buffer)
|
|
87
|
+
return;
|
|
88
|
+
this.buffer = fullText;
|
|
89
|
+
this.refreezeBoundaries();
|
|
90
|
+
// Make sure we have one Discord message per chunk, then dispatch. The
|
|
91
|
+
// `.catch` keeps `setupPromise` itself rejection-free (no unhandled
|
|
92
|
+
// rejection between this append and the next surfacing point); the failure
|
|
93
|
+
// is recorded once and rethrown at the next append/finish.
|
|
94
|
+
this.setupPromise = this.setupPromise
|
|
95
|
+
.then(() => this.ensureStreamsAndDispatch())
|
|
96
|
+
.catch((err) => {
|
|
97
|
+
if (this.setupError === undefined)
|
|
98
|
+
this.setupError = err;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
async finish() {
|
|
102
|
+
this.finished = true;
|
|
103
|
+
// Drain any pending setup, then a final dispatch, then finish each stream.
|
|
104
|
+
this.setupPromise = this.setupPromise
|
|
105
|
+
.then(() => this.ensureStreamsAndDispatch())
|
|
106
|
+
.catch((err) => {
|
|
107
|
+
if (this.setupError === undefined)
|
|
108
|
+
this.setupError = err;
|
|
109
|
+
});
|
|
110
|
+
await this.setupPromise;
|
|
111
|
+
// Surface a setup failure (e.g. a rejecting postPlaceholder) here so the
|
|
112
|
+
// caller still sees it — just without an interim unhandled rejection.
|
|
113
|
+
if (this.setupError !== undefined)
|
|
114
|
+
throw this.asSetupError();
|
|
115
|
+
for (const s of this.streams)
|
|
116
|
+
await s.finish();
|
|
117
|
+
}
|
|
118
|
+
/** Wraps the recorded setup failure with context for surfacing to callers. */
|
|
119
|
+
asSetupError() {
|
|
120
|
+
const cause = this.setupError;
|
|
121
|
+
return new Error("ChunkedMessageStream setup failed", { cause });
|
|
122
|
+
}
|
|
123
|
+
/** Returns the number of Discord messages this stream has posted so far. */
|
|
124
|
+
get chunkCount() {
|
|
125
|
+
return this.streams.length;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Walk forward from the last frozen boundary, freezing new ones whenever
|
|
129
|
+
* the active chunk's length exceeds the soft limit. Once frozen, a
|
|
130
|
+
* boundary doesn't move.
|
|
131
|
+
*
|
|
132
|
+
* Special case (block-keeps-whole): if the chosen boundary lands INSIDE
|
|
133
|
+
* an open fenced code block, we try to move the boundary BACK to the
|
|
134
|
+
* position right before the fence opener, so the *whole* block lives in
|
|
135
|
+
* the next Discord message rather than being split. The previous chunk
|
|
136
|
+
* gets shortened (message edit will update it down). Fallback when the
|
|
137
|
+
* block is too large to fit in one chunk: keep the inside-fence
|
|
138
|
+
* boundary; the dispatcher will prepend the fence opener to the next
|
|
139
|
+
* chunk (re-opener path).
|
|
140
|
+
*/
|
|
141
|
+
refreezeBoundaries() {
|
|
142
|
+
let lastFrozen = this.boundaries.at(-1) ?? 0;
|
|
143
|
+
while (this.buffer.length - lastFrozen > this.limit) {
|
|
144
|
+
const window = this.buffer.slice(lastFrozen, lastFrozen + this.limit);
|
|
145
|
+
let breakAt = window.lastIndexOf("\n");
|
|
146
|
+
if (breakAt < this.limit / 4)
|
|
147
|
+
breakAt = window.lastIndexOf(" ");
|
|
148
|
+
if (breakAt < 0)
|
|
149
|
+
breakAt = this.limit - 1;
|
|
150
|
+
let candidate = lastFrozen + breakAt + 1;
|
|
151
|
+
// Block-keeps-whole: move boundary back if it lands inside an open
|
|
152
|
+
// fence. Only adjust if the fence opener is in the latter ~70% of
|
|
153
|
+
// the active chunk — otherwise shrinking would lose too much
|
|
154
|
+
// content (chunk N would be tiny) and the re-opener path is better.
|
|
155
|
+
const fenceStart = findUnpairedFenceStart(this.buffer, candidate);
|
|
156
|
+
if (fenceStart !== null) {
|
|
157
|
+
const minAcceptable = lastFrozen + Math.floor(this.limit * 0.3);
|
|
158
|
+
if (fenceStart > minAcceptable) {
|
|
159
|
+
candidate = fenceStart;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
this.boundaries.push(candidate);
|
|
163
|
+
lastFrozen = candidate;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
async ensureStreamsAndDispatch() {
|
|
167
|
+
// If we've never received any content, don't post a placeholder. This
|
|
168
|
+
// matters for the empty-response case: a TEXT_MESSAGE_START + END
|
|
169
|
+
// pair with no content events should produce no Discord message.
|
|
170
|
+
if (this.buffer.length === 0)
|
|
171
|
+
return;
|
|
172
|
+
const chunkCount = this.boundaries.length + 1;
|
|
173
|
+
while (this.streams.length < chunkCount) {
|
|
174
|
+
const i = this.streams.length;
|
|
175
|
+
const placeholder = i === 0 ? STREAM_PLACEHOLDERS[0] : STREAM_PLACEHOLDERS[1];
|
|
176
|
+
const id = await this.postPlaceholder(placeholder);
|
|
177
|
+
this.streams.push(new MessageStream({
|
|
178
|
+
update: (text) => this.updateAt(id, clampToHardLimit(this.transform(text)) || " "),
|
|
179
|
+
minIntervalMs: this.minIntervalMs,
|
|
180
|
+
}));
|
|
181
|
+
}
|
|
182
|
+
// Dispatch slices.
|
|
183
|
+
//
|
|
184
|
+
// For continuation chunks (i > 0), if the boundary fell inside an open
|
|
185
|
+
// markdown construct (e.g. inside a ```python block), prepend the
|
|
186
|
+
// opener for that construct to the slice. Otherwise the second Discord
|
|
187
|
+
// message would begin with raw code that Discord renders as plain text.
|
|
188
|
+
// The closer at the end of the chunk is added by `transform`
|
|
189
|
+
// (autoCloseOpenMarkdown), which runs per chunk.
|
|
190
|
+
for (let i = 0; i < chunkCount; i++) {
|
|
191
|
+
const start = i === 0 ? 0 : this.boundaries[i - 1];
|
|
192
|
+
const end = i < this.boundaries.length ? this.boundaries[i] : this.buffer.length;
|
|
193
|
+
let slice = this.buffer.slice(start, end);
|
|
194
|
+
if (i > 0) {
|
|
195
|
+
const ctx = detectOpenContext(this.buffer.slice(0, start));
|
|
196
|
+
const opener = renderContextOpener(ctx);
|
|
197
|
+
if (opener)
|
|
198
|
+
slice = opener + slice;
|
|
199
|
+
}
|
|
200
|
+
this.streams[i].append(slice);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chunked-message-stream.test.d.ts","sourceRoot":"","sources":["../src/chunked-message-stream.test.ts"],"names":[],"mappings":""}
|