@theowlops/channelhub 1.0.1 → 1.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.
@@ -21,4 +21,6 @@ export declare class DiscordChannelAdapter extends BaseChannel {
21
21
  sendText(chatId: string, text: string, options?: SendOptions): Promise<SentMessageResult>;
22
22
  sendMedia(chatId: string, media: MediaPayload, options?: SendOptions): Promise<SentMessageResult>;
23
23
  addReaction(chatId: string, messageId: string, emoji: string): Promise<void>;
24
+ sendTyping(chatId: string): Promise<void>;
25
+ editText(chatId: string, messageId: string, text: string): Promise<SentMessageResult>;
24
26
  }
@@ -124,6 +124,19 @@ class DiscordChannelAdapter extends BaseChannel {
124
124
  const encoded = encodeURIComponent(emoji);
125
125
  await this.callApi("PUT", `/channels/${chatId}/messages/${messageId}/reactions/${encoded}/@me`);
126
126
  }
127
+ async sendTyping(chatId) {
128
+ await this.callApi("POST", `/channels/${chatId}/typing`, {});
129
+ }
130
+ async editText(chatId, messageId, text) {
131
+ const res = await this.callApi("PATCH", `/channels/${chatId}/messages/${messageId}`, {
132
+ content: text
133
+ });
134
+ return {
135
+ messageId: String(res.id || messageId),
136
+ chatId,
137
+ timestamp: Date.now()
138
+ };
139
+ }
127
140
  }
128
141
  export {
129
142
  DiscordChannelAdapter
@@ -17,4 +17,6 @@ export declare class SlackChannelAdapter extends BaseChannel {
17
17
  sendText(chatId: string, text: string, options?: SendOptions): Promise<SentMessageResult>;
18
18
  sendMedia(chatId: string, media: MediaPayload, options?: SendOptions): Promise<SentMessageResult>;
19
19
  addReaction(chatId: string, messageId: string, emoji: string): Promise<void>;
20
+ sendTyping(chatId: string): Promise<void>;
21
+ editText(chatId: string, messageId: string, text: string): Promise<SentMessageResult>;
20
22
  }
@@ -116,6 +116,19 @@ class SlackChannelAdapter extends BaseChannel {
116
116
  name: cleanName
117
117
  });
118
118
  }
119
+ async sendTyping(chatId) {}
120
+ async editText(chatId, messageId, text) {
121
+ const res = await this.callApi("chat.update", {
122
+ channel: chatId,
123
+ ts: messageId,
124
+ text
125
+ });
126
+ return {
127
+ messageId: String(res.ts || messageId),
128
+ chatId,
129
+ timestamp: Date.now()
130
+ };
131
+ }
119
132
  }
120
133
  export {
121
134
  SlackChannelAdapter
@@ -18,4 +18,6 @@ export declare class TelegramChannelAdapter extends BaseChannel {
18
18
  sendText(chatId: string, text: string, options?: SendOptions): Promise<SentMessageResult>;
19
19
  sendMedia(chatId: string, media: MediaPayload, options?: SendOptions): Promise<SentMessageResult>;
20
20
  addReaction(chatId: string, messageId: string, emoji: string): Promise<void>;
21
+ sendTyping(chatId: string): Promise<void>;
22
+ editText(chatId: string, messageId: string, text: string): Promise<SentMessageResult>;
21
23
  }
@@ -173,6 +173,24 @@ class TelegramChannelAdapter extends BaseChannel {
173
173
  reaction: [{ type: "emoji", emoji }]
174
174
  });
175
175
  }
176
+ async sendTyping(chatId) {
177
+ await this.callApi("sendChatAction", {
178
+ chat_id: chatId,
179
+ action: "typing"
180
+ });
181
+ }
182
+ async editText(chatId, messageId, text) {
183
+ const res = await this.callApi("editMessageText", {
184
+ chat_id: chatId,
185
+ message_id: Number(messageId),
186
+ text
187
+ });
188
+ return {
189
+ messageId: String(res.message_id || messageId),
190
+ chatId,
191
+ timestamp: Date.now()
192
+ };
193
+ }
176
194
  }
177
195
  export {
178
196
  TelegramChannelAdapter
@@ -30,4 +30,5 @@ export declare class ZaloChannelAdapter extends BaseChannel {
30
30
  sendText(chatId: string, text: string, options?: SendOptions): Promise<SentMessageResult>;
31
31
  sendMedia(chatId: string, media: MediaPayload, options?: SendOptions): Promise<SentMessageResult>;
32
32
  addReaction(chatId: string, messageId: string, emoji: string): Promise<void>;
33
+ sendTyping(chatId: string): Promise<void>;
33
34
  }
@@ -37738,6 +37738,14 @@ class ZaloChannelAdapter extends BaseChannel {
37738
37738
  await this.api.addReaction(chatId, messageId, cliMsgId, reactionCode, threadType);
37739
37739
  });
37740
37740
  }
37741
+ async sendTyping(chatId) {
37742
+ if (!this.api?.sendTypingEvent)
37743
+ return;
37744
+ const threadType = this.resolveThreadType(chatId);
37745
+ try {
37746
+ await this.api.sendTypingEvent(chatId, true, threadType);
37747
+ } catch {}
37748
+ }
37741
37749
  }
37742
37750
  // src/personal/client.ts
37743
37751
  init_dist();
@@ -1,9 +1,12 @@
1
1
  import type { IChannelAdapter, MediaPayload, SendOptions, SentMessageResult, UnifiedMessage } from "./types";
2
+ import { type StreamOptions } from "./stream";
2
3
  export interface MessageContext {
3
4
  message: UnifiedMessage;
4
5
  channel: IChannelAdapter;
5
6
  reply: (text: string, options?: SendOptions) => Promise<SentMessageResult>;
6
7
  replyMedia: (media: MediaPayload, options?: SendOptions) => Promise<SentMessageResult>;
7
8
  react: (emoji: string) => Promise<void>;
9
+ sendTyping: () => Promise<void>;
10
+ stream: (tokenStream: AsyncIterable<string>, options?: StreamOptions) => Promise<SentMessageResult[]>;
8
11
  }
9
12
  export declare function createMessageContext(message: UnifiedMessage, channel: IChannelAdapter): MessageContext;
@@ -3,3 +3,4 @@ export * from "./adapter";
3
3
  export * from "./bus";
4
4
  export * from "./context";
5
5
  export * from "./hub";
6
+ export * from "./stream";
@@ -28,6 +28,126 @@ class ChannelEventBus extends EventEmitter2 {
28
28
  return this.emit("status", status);
29
29
  }
30
30
  }
31
+ // src/core/stream.ts
32
+ class SmartStreamer {
33
+ adapter;
34
+ options;
35
+ constructor(adapter, options = {}) {
36
+ this.adapter = adapter;
37
+ this.options = {
38
+ editDebounceMs: 1000,
39
+ typingIntervalMs: 4000,
40
+ chunkMode: "sentence",
41
+ minSentenceLength: 60,
42
+ initialPlaceholder: "...",
43
+ ...options
44
+ };
45
+ }
46
+ async stream(chatId, tokenStream, sendOptions) {
47
+ let typingActive = true;
48
+ const triggerTyping = async () => {
49
+ if (this.adapter.sendTyping) {
50
+ try {
51
+ await this.adapter.sendTyping(chatId);
52
+ } catch {}
53
+ }
54
+ };
55
+ await triggerTyping();
56
+ const typingTimer = setInterval(() => {
57
+ if (typingActive)
58
+ triggerTyping();
59
+ }, this.options.typingIntervalMs);
60
+ try {
61
+ if (typeof this.adapter.editText === "function") {
62
+ return await this.streamWithEdit(chatId, tokenStream, sendOptions);
63
+ } else {
64
+ return await this.streamWithoutEdit(chatId, tokenStream, sendOptions);
65
+ }
66
+ } finally {
67
+ typingActive = false;
68
+ clearInterval(typingTimer);
69
+ }
70
+ }
71
+ async streamWithEdit(chatId, tokenStream, sendOptions) {
72
+ let accumulated = "";
73
+ let sentMsg = null;
74
+ let lastEditTime = 0;
75
+ let pendingEditTimeout = null;
76
+ const performEdit = async (text) => {
77
+ if (sentMsg && this.adapter.editText) {
78
+ await this.adapter.editText(chatId, sentMsg.messageId, text);
79
+ lastEditTime = Date.now();
80
+ }
81
+ };
82
+ for await (const chunk of tokenStream) {
83
+ accumulated += chunk;
84
+ if (!sentMsg) {
85
+ sentMsg = await this.adapter.sendText(chatId, accumulated.trim() || this.options.initialPlaceholder, sendOptions);
86
+ lastEditTime = Date.now();
87
+ continue;
88
+ }
89
+ const now = Date.now();
90
+ const elapsed = now - lastEditTime;
91
+ if (elapsed >= this.options.editDebounceMs) {
92
+ if (pendingEditTimeout) {
93
+ clearTimeout(pendingEditTimeout);
94
+ pendingEditTimeout = null;
95
+ }
96
+ await performEdit(accumulated);
97
+ } else if (!pendingEditTimeout) {
98
+ pendingEditTimeout = setTimeout(async () => {
99
+ pendingEditTimeout = null;
100
+ await performEdit(accumulated);
101
+ }, this.options.editDebounceMs - elapsed);
102
+ }
103
+ }
104
+ if (pendingEditTimeout) {
105
+ clearTimeout(pendingEditTimeout);
106
+ pendingEditTimeout = null;
107
+ }
108
+ if (sentMsg && accumulated) {
109
+ await performEdit(accumulated);
110
+ return [sentMsg];
111
+ } else if (!sentMsg && accumulated) {
112
+ const res = await this.adapter.sendText(chatId, accumulated, sendOptions);
113
+ return [res];
114
+ }
115
+ return sentMsg ? [sentMsg] : [];
116
+ }
117
+ async streamWithoutEdit(chatId, tokenStream, sendOptions) {
118
+ const results = [];
119
+ if (this.options.chunkMode === "accumulate") {
120
+ let accumulated = "";
121
+ for await (const chunk of tokenStream) {
122
+ accumulated += chunk;
123
+ }
124
+ if (accumulated.trim()) {
125
+ const res = await this.adapter.sendText(chatId, accumulated, sendOptions);
126
+ results.push(res);
127
+ }
128
+ return results;
129
+ }
130
+ let buffer = "";
131
+ const sentenceEndRegex = /[.?!;\n]\s*$/;
132
+ for await (const chunk of tokenStream) {
133
+ buffer += chunk;
134
+ if (buffer.length >= this.options.minSentenceLength && sentenceEndRegex.test(buffer.trimEnd())) {
135
+ const textToSend = buffer.trim();
136
+ if (textToSend) {
137
+ const res = await this.adapter.sendText(chatId, textToSend, sendOptions);
138
+ results.push(res);
139
+ buffer = "";
140
+ }
141
+ }
142
+ }
143
+ if (buffer.trim()) {
144
+ const res = await this.adapter.sendText(chatId, buffer.trim(), sendOptions);
145
+ results.push(res);
146
+ }
147
+ return results;
148
+ }
149
+ }
150
+
31
151
  // src/core/context.ts
32
152
  function createMessageContext(message, channel) {
33
153
  return {
@@ -45,6 +165,17 @@ function createMessageContext(message, channel) {
45
165
  if (channel.addReaction) {
46
166
  await channel.addReaction(message.chat.id, message.id, emoji);
47
167
  }
168
+ },
169
+ sendTyping: async () => {
170
+ if (channel.sendTyping) {
171
+ await channel.sendTyping(message.chat.id);
172
+ }
173
+ },
174
+ stream: async (tokenStream, options) => {
175
+ const streamer = new SmartStreamer(channel, options);
176
+ return await streamer.stream(message.chat.id, tokenStream, {
177
+ replyToId: message.id
178
+ });
48
179
  }
49
180
  };
50
181
  }
@@ -95,5 +226,6 @@ export {
95
226
  BaseChannel,
96
227
  ChannelEventBus,
97
228
  ChannelHub,
229
+ SmartStreamer,
98
230
  createMessageContext
99
231
  };
@@ -0,0 +1,16 @@
1
+ import type { IChannelAdapter, SendOptions, SentMessageResult } from "./types";
2
+ export interface StreamOptions {
3
+ editDebounceMs?: number;
4
+ typingIntervalMs?: number;
5
+ chunkMode?: "sentence" | "accumulate";
6
+ minSentenceLength?: number;
7
+ initialPlaceholder?: string;
8
+ }
9
+ export declare class SmartStreamer {
10
+ private adapter;
11
+ private options;
12
+ constructor(adapter: IChannelAdapter, options?: StreamOptions);
13
+ stream(chatId: string, tokenStream: AsyncIterable<string>, sendOptions?: SendOptions): Promise<SentMessageResult[]>;
14
+ private streamWithEdit;
15
+ private streamWithoutEdit;
16
+ }
@@ -57,6 +57,8 @@ export interface IChannelAdapter {
57
57
  sendText(chatId: string, text: string, options?: SendOptions): Promise<SentMessageResult>;
58
58
  sendMedia(chatId: string, media: MediaPayload, options?: SendOptions): Promise<SentMessageResult>;
59
59
  addReaction?(chatId: string, messageId: string, emoji: string): Promise<void>;
60
+ sendTyping?(chatId: string): Promise<void>;
61
+ editText?(chatId: string, messageId: string, text: string): Promise<SentMessageResult>;
60
62
  on(event: "message", handler: (msg: UnifiedMessage) => Promise<void> | void): this;
61
63
  on(event: "error", handler: (err: Error) => void): this;
62
64
  on(event: "status", handler: (status: ChannelStatus) => void): this;
package/dist/index.cjs CHANGED
@@ -20548,9 +20548,9 @@ var require_websocket_server = __commonJS(function(exports2, module2) {
20548
20548
  });
20549
20549
 
20550
20550
  // node_modules/ws/wrapper.mjs
20551
- var import_stream, import_extension, import_permessage_deflate, import_receiver, import_sender, import_subprotocol, import_websocket, import_websocket_server, wrapper_default;
20551
+ var import_stream2, import_extension, import_permessage_deflate, import_receiver, import_sender, import_subprotocol, import_websocket, import_websocket_server, wrapper_default;
20552
20552
  var init_wrapper = __esm(() => {
20553
- import_stream = __toESM(require_stream(), 1);
20553
+ import_stream2 = __toESM(require_stream(), 1);
20554
20554
  import_extension = __toESM(require_extension(), 1);
20555
20555
  import_permessage_deflate = __toESM(require_permessage_deflate(), 1);
20556
20556
  import_receiver = __toESM(require_receiver(), 1);
@@ -37520,6 +37520,7 @@ __export(exports_src, {
37520
37520
  CommandRouter: () => CommandRouter,
37521
37521
  DiscordChannelAdapter: () => DiscordChannelAdapter,
37522
37522
  SlackChannelAdapter: () => SlackChannelAdapter,
37523
+ SmartStreamer: () => SmartStreamer,
37523
37524
  TelegramChannelAdapter: () => TelegramChannelAdapter,
37524
37525
  WebhookBridge: () => WebhookBridge,
37525
37526
  Zalo: () => Zalo,
@@ -37563,6 +37564,126 @@ class ChannelEventBus extends import_node_events2.EventEmitter {
37563
37564
  return this.emit("status", status);
37564
37565
  }
37565
37566
  }
37567
+ // src/core/stream.ts
37568
+ class SmartStreamer {
37569
+ adapter;
37570
+ options;
37571
+ constructor(adapter, options = {}) {
37572
+ this.adapter = adapter;
37573
+ this.options = {
37574
+ editDebounceMs: 1000,
37575
+ typingIntervalMs: 4000,
37576
+ chunkMode: "sentence",
37577
+ minSentenceLength: 60,
37578
+ initialPlaceholder: "...",
37579
+ ...options
37580
+ };
37581
+ }
37582
+ async stream(chatId, tokenStream, sendOptions) {
37583
+ let typingActive = true;
37584
+ const triggerTyping = async () => {
37585
+ if (this.adapter.sendTyping) {
37586
+ try {
37587
+ await this.adapter.sendTyping(chatId);
37588
+ } catch {}
37589
+ }
37590
+ };
37591
+ await triggerTyping();
37592
+ const typingTimer = setInterval(() => {
37593
+ if (typingActive)
37594
+ triggerTyping();
37595
+ }, this.options.typingIntervalMs);
37596
+ try {
37597
+ if (typeof this.adapter.editText === "function") {
37598
+ return await this.streamWithEdit(chatId, tokenStream, sendOptions);
37599
+ } else {
37600
+ return await this.streamWithoutEdit(chatId, tokenStream, sendOptions);
37601
+ }
37602
+ } finally {
37603
+ typingActive = false;
37604
+ clearInterval(typingTimer);
37605
+ }
37606
+ }
37607
+ async streamWithEdit(chatId, tokenStream, sendOptions) {
37608
+ let accumulated = "";
37609
+ let sentMsg = null;
37610
+ let lastEditTime = 0;
37611
+ let pendingEditTimeout = null;
37612
+ const performEdit = async (text) => {
37613
+ if (sentMsg && this.adapter.editText) {
37614
+ await this.adapter.editText(chatId, sentMsg.messageId, text);
37615
+ lastEditTime = Date.now();
37616
+ }
37617
+ };
37618
+ for await (const chunk of tokenStream) {
37619
+ accumulated += chunk;
37620
+ if (!sentMsg) {
37621
+ sentMsg = await this.adapter.sendText(chatId, accumulated.trim() || this.options.initialPlaceholder, sendOptions);
37622
+ lastEditTime = Date.now();
37623
+ continue;
37624
+ }
37625
+ const now = Date.now();
37626
+ const elapsed = now - lastEditTime;
37627
+ if (elapsed >= this.options.editDebounceMs) {
37628
+ if (pendingEditTimeout) {
37629
+ clearTimeout(pendingEditTimeout);
37630
+ pendingEditTimeout = null;
37631
+ }
37632
+ await performEdit(accumulated);
37633
+ } else if (!pendingEditTimeout) {
37634
+ pendingEditTimeout = setTimeout(async () => {
37635
+ pendingEditTimeout = null;
37636
+ await performEdit(accumulated);
37637
+ }, this.options.editDebounceMs - elapsed);
37638
+ }
37639
+ }
37640
+ if (pendingEditTimeout) {
37641
+ clearTimeout(pendingEditTimeout);
37642
+ pendingEditTimeout = null;
37643
+ }
37644
+ if (sentMsg && accumulated) {
37645
+ await performEdit(accumulated);
37646
+ return [sentMsg];
37647
+ } else if (!sentMsg && accumulated) {
37648
+ const res = await this.adapter.sendText(chatId, accumulated, sendOptions);
37649
+ return [res];
37650
+ }
37651
+ return sentMsg ? [sentMsg] : [];
37652
+ }
37653
+ async streamWithoutEdit(chatId, tokenStream, sendOptions) {
37654
+ const results = [];
37655
+ if (this.options.chunkMode === "accumulate") {
37656
+ let accumulated = "";
37657
+ for await (const chunk of tokenStream) {
37658
+ accumulated += chunk;
37659
+ }
37660
+ if (accumulated.trim()) {
37661
+ const res = await this.adapter.sendText(chatId, accumulated, sendOptions);
37662
+ results.push(res);
37663
+ }
37664
+ return results;
37665
+ }
37666
+ let buffer = "";
37667
+ const sentenceEndRegex = /[.?!;\n]\s*$/;
37668
+ for await (const chunk of tokenStream) {
37669
+ buffer += chunk;
37670
+ if (buffer.length >= this.options.minSentenceLength && sentenceEndRegex.test(buffer.trimEnd())) {
37671
+ const textToSend = buffer.trim();
37672
+ if (textToSend) {
37673
+ const res = await this.adapter.sendText(chatId, textToSend, sendOptions);
37674
+ results.push(res);
37675
+ buffer = "";
37676
+ }
37677
+ }
37678
+ }
37679
+ if (buffer.trim()) {
37680
+ const res = await this.adapter.sendText(chatId, buffer.trim(), sendOptions);
37681
+ results.push(res);
37682
+ }
37683
+ return results;
37684
+ }
37685
+ }
37686
+
37566
37687
  // src/core/context.ts
37567
37688
  function createMessageContext(message, channel) {
37568
37689
  return {
@@ -37580,6 +37701,17 @@ function createMessageContext(message, channel) {
37580
37701
  if (channel.addReaction) {
37581
37702
  await channel.addReaction(message.chat.id, message.id, emoji);
37582
37703
  }
37704
+ },
37705
+ sendTyping: async () => {
37706
+ if (channel.sendTyping) {
37707
+ await channel.sendTyping(message.chat.id);
37708
+ }
37709
+ },
37710
+ stream: async (tokenStream, options) => {
37711
+ const streamer = new SmartStreamer(channel, options);
37712
+ return await streamer.stream(message.chat.id, tokenStream, {
37713
+ replyToId: message.id
37714
+ });
37583
37715
  }
37584
37716
  };
37585
37717
  }
@@ -37877,6 +38009,14 @@ class ZaloChannelAdapter extends BaseChannel {
37877
38009
  await this.api.addReaction(chatId, messageId, cliMsgId, reactionCode, threadType);
37878
38010
  });
37879
38011
  }
38012
+ async sendTyping(chatId) {
38013
+ if (!this.api?.sendTypingEvent)
38014
+ return;
38015
+ const threadType = this.resolveThreadType(chatId);
38016
+ try {
38017
+ await this.api.sendTypingEvent(chatId, true, threadType);
38018
+ } catch {}
38019
+ }
37880
38020
  }
37881
38021
  // src/personal/client.ts
37882
38022
  init_dist();
@@ -38753,6 +38893,24 @@ class TelegramChannelAdapter extends BaseChannel {
38753
38893
  reaction: [{ type: "emoji", emoji }]
38754
38894
  });
38755
38895
  }
38896
+ async sendTyping(chatId) {
38897
+ await this.callApi("sendChatAction", {
38898
+ chat_id: chatId,
38899
+ action: "typing"
38900
+ });
38901
+ }
38902
+ async editText(chatId, messageId, text) {
38903
+ const res = await this.callApi("editMessageText", {
38904
+ chat_id: chatId,
38905
+ message_id: Number(messageId),
38906
+ text
38907
+ });
38908
+ return {
38909
+ messageId: String(res.message_id || messageId),
38910
+ chatId,
38911
+ timestamp: Date.now()
38912
+ };
38913
+ }
38756
38914
  }
38757
38915
  // src/channels/discord/adapter.ts
38758
38916
  class DiscordChannelAdapter extends BaseChannel {
@@ -38863,6 +39021,19 @@ class DiscordChannelAdapter extends BaseChannel {
38863
39021
  const encoded = encodeURIComponent(emoji);
38864
39022
  await this.callApi("PUT", `/channels/${chatId}/messages/${messageId}/reactions/${encoded}/@me`);
38865
39023
  }
39024
+ async sendTyping(chatId) {
39025
+ await this.callApi("POST", `/channels/${chatId}/typing`, {});
39026
+ }
39027
+ async editText(chatId, messageId, text) {
39028
+ const res = await this.callApi("PATCH", `/channels/${chatId}/messages/${messageId}`, {
39029
+ content: text
39030
+ });
39031
+ return {
39032
+ messageId: String(res.id || messageId),
39033
+ chatId,
39034
+ timestamp: Date.now()
39035
+ };
39036
+ }
38866
39037
  }
38867
39038
  // src/channels/slack/adapter.ts
38868
39039
  class SlackChannelAdapter extends BaseChannel {
@@ -38965,6 +39136,19 @@ class SlackChannelAdapter extends BaseChannel {
38965
39136
  name: cleanName
38966
39137
  });
38967
39138
  }
39139
+ async sendTyping(chatId) {}
39140
+ async editText(chatId, messageId, text) {
39141
+ const res = await this.callApi("chat.update", {
39142
+ channel: chatId,
39143
+ ts: messageId,
39144
+ text
39145
+ });
39146
+ return {
39147
+ messageId: String(res.ts || messageId),
39148
+ chatId,
39149
+ timestamp: Date.now()
39150
+ };
39151
+ }
38968
39152
  }
38969
39153
  // src/bridges/mcp/index.ts
38970
39154
  function getChannelHubMcpTools() {
package/dist/index.js CHANGED
@@ -20519,9 +20519,9 @@ var require_websocket_server = __commonJS(function(exports, module) {
20519
20519
  });
20520
20520
 
20521
20521
  // node_modules/ws/wrapper.mjs
20522
- var import_stream, import_extension, import_permessage_deflate, import_receiver, import_sender, import_subprotocol, import_websocket, import_websocket_server, wrapper_default;
20522
+ var import_stream2, import_extension, import_permessage_deflate, import_receiver, import_sender, import_subprotocol, import_websocket, import_websocket_server, wrapper_default;
20523
20523
  var init_wrapper = __esm(() => {
20524
- import_stream = __toESM(require_stream(), 1);
20524
+ import_stream2 = __toESM(require_stream(), 1);
20525
20525
  import_extension = __toESM(require_extension(), 1);
20526
20526
  import_permessage_deflate = __toESM(require_permessage_deflate(), 1);
20527
20527
  import_receiver = __toESM(require_receiver(), 1);
@@ -37499,6 +37499,126 @@ class ChannelEventBus extends EventEmitter2 {
37499
37499
  return this.emit("status", status);
37500
37500
  }
37501
37501
  }
37502
+ // src/core/stream.ts
37503
+ class SmartStreamer {
37504
+ adapter;
37505
+ options;
37506
+ constructor(adapter, options = {}) {
37507
+ this.adapter = adapter;
37508
+ this.options = {
37509
+ editDebounceMs: 1000,
37510
+ typingIntervalMs: 4000,
37511
+ chunkMode: "sentence",
37512
+ minSentenceLength: 60,
37513
+ initialPlaceholder: "...",
37514
+ ...options
37515
+ };
37516
+ }
37517
+ async stream(chatId, tokenStream, sendOptions) {
37518
+ let typingActive = true;
37519
+ const triggerTyping = async () => {
37520
+ if (this.adapter.sendTyping) {
37521
+ try {
37522
+ await this.adapter.sendTyping(chatId);
37523
+ } catch {}
37524
+ }
37525
+ };
37526
+ await triggerTyping();
37527
+ const typingTimer = setInterval(() => {
37528
+ if (typingActive)
37529
+ triggerTyping();
37530
+ }, this.options.typingIntervalMs);
37531
+ try {
37532
+ if (typeof this.adapter.editText === "function") {
37533
+ return await this.streamWithEdit(chatId, tokenStream, sendOptions);
37534
+ } else {
37535
+ return await this.streamWithoutEdit(chatId, tokenStream, sendOptions);
37536
+ }
37537
+ } finally {
37538
+ typingActive = false;
37539
+ clearInterval(typingTimer);
37540
+ }
37541
+ }
37542
+ async streamWithEdit(chatId, tokenStream, sendOptions) {
37543
+ let accumulated = "";
37544
+ let sentMsg = null;
37545
+ let lastEditTime = 0;
37546
+ let pendingEditTimeout = null;
37547
+ const performEdit = async (text) => {
37548
+ if (sentMsg && this.adapter.editText) {
37549
+ await this.adapter.editText(chatId, sentMsg.messageId, text);
37550
+ lastEditTime = Date.now();
37551
+ }
37552
+ };
37553
+ for await (const chunk of tokenStream) {
37554
+ accumulated += chunk;
37555
+ if (!sentMsg) {
37556
+ sentMsg = await this.adapter.sendText(chatId, accumulated.trim() || this.options.initialPlaceholder, sendOptions);
37557
+ lastEditTime = Date.now();
37558
+ continue;
37559
+ }
37560
+ const now = Date.now();
37561
+ const elapsed = now - lastEditTime;
37562
+ if (elapsed >= this.options.editDebounceMs) {
37563
+ if (pendingEditTimeout) {
37564
+ clearTimeout(pendingEditTimeout);
37565
+ pendingEditTimeout = null;
37566
+ }
37567
+ await performEdit(accumulated);
37568
+ } else if (!pendingEditTimeout) {
37569
+ pendingEditTimeout = setTimeout(async () => {
37570
+ pendingEditTimeout = null;
37571
+ await performEdit(accumulated);
37572
+ }, this.options.editDebounceMs - elapsed);
37573
+ }
37574
+ }
37575
+ if (pendingEditTimeout) {
37576
+ clearTimeout(pendingEditTimeout);
37577
+ pendingEditTimeout = null;
37578
+ }
37579
+ if (sentMsg && accumulated) {
37580
+ await performEdit(accumulated);
37581
+ return [sentMsg];
37582
+ } else if (!sentMsg && accumulated) {
37583
+ const res = await this.adapter.sendText(chatId, accumulated, sendOptions);
37584
+ return [res];
37585
+ }
37586
+ return sentMsg ? [sentMsg] : [];
37587
+ }
37588
+ async streamWithoutEdit(chatId, tokenStream, sendOptions) {
37589
+ const results = [];
37590
+ if (this.options.chunkMode === "accumulate") {
37591
+ let accumulated = "";
37592
+ for await (const chunk of tokenStream) {
37593
+ accumulated += chunk;
37594
+ }
37595
+ if (accumulated.trim()) {
37596
+ const res = await this.adapter.sendText(chatId, accumulated, sendOptions);
37597
+ results.push(res);
37598
+ }
37599
+ return results;
37600
+ }
37601
+ let buffer = "";
37602
+ const sentenceEndRegex = /[.?!;\n]\s*$/;
37603
+ for await (const chunk of tokenStream) {
37604
+ buffer += chunk;
37605
+ if (buffer.length >= this.options.minSentenceLength && sentenceEndRegex.test(buffer.trimEnd())) {
37606
+ const textToSend = buffer.trim();
37607
+ if (textToSend) {
37608
+ const res = await this.adapter.sendText(chatId, textToSend, sendOptions);
37609
+ results.push(res);
37610
+ buffer = "";
37611
+ }
37612
+ }
37613
+ }
37614
+ if (buffer.trim()) {
37615
+ const res = await this.adapter.sendText(chatId, buffer.trim(), sendOptions);
37616
+ results.push(res);
37617
+ }
37618
+ return results;
37619
+ }
37620
+ }
37621
+
37502
37622
  // src/core/context.ts
37503
37623
  function createMessageContext(message, channel) {
37504
37624
  return {
@@ -37516,6 +37636,17 @@ function createMessageContext(message, channel) {
37516
37636
  if (channel.addReaction) {
37517
37637
  await channel.addReaction(message.chat.id, message.id, emoji);
37518
37638
  }
37639
+ },
37640
+ sendTyping: async () => {
37641
+ if (channel.sendTyping) {
37642
+ await channel.sendTyping(message.chat.id);
37643
+ }
37644
+ },
37645
+ stream: async (tokenStream, options) => {
37646
+ const streamer = new SmartStreamer(channel, options);
37647
+ return await streamer.stream(message.chat.id, tokenStream, {
37648
+ replyToId: message.id
37649
+ });
37519
37650
  }
37520
37651
  };
37521
37652
  }
@@ -37813,6 +37944,14 @@ class ZaloChannelAdapter extends BaseChannel {
37813
37944
  await this.api.addReaction(chatId, messageId, cliMsgId, reactionCode, threadType);
37814
37945
  });
37815
37946
  }
37947
+ async sendTyping(chatId) {
37948
+ if (!this.api?.sendTypingEvent)
37949
+ return;
37950
+ const threadType = this.resolveThreadType(chatId);
37951
+ try {
37952
+ await this.api.sendTypingEvent(chatId, true, threadType);
37953
+ } catch {}
37954
+ }
37816
37955
  }
37817
37956
  // src/personal/client.ts
37818
37957
  init_dist();
@@ -38689,6 +38828,24 @@ class TelegramChannelAdapter extends BaseChannel {
38689
38828
  reaction: [{ type: "emoji", emoji }]
38690
38829
  });
38691
38830
  }
38831
+ async sendTyping(chatId) {
38832
+ await this.callApi("sendChatAction", {
38833
+ chat_id: chatId,
38834
+ action: "typing"
38835
+ });
38836
+ }
38837
+ async editText(chatId, messageId, text) {
38838
+ const res = await this.callApi("editMessageText", {
38839
+ chat_id: chatId,
38840
+ message_id: Number(messageId),
38841
+ text
38842
+ });
38843
+ return {
38844
+ messageId: String(res.message_id || messageId),
38845
+ chatId,
38846
+ timestamp: Date.now()
38847
+ };
38848
+ }
38692
38849
  }
38693
38850
  // src/channels/discord/adapter.ts
38694
38851
  class DiscordChannelAdapter extends BaseChannel {
@@ -38799,6 +38956,19 @@ class DiscordChannelAdapter extends BaseChannel {
38799
38956
  const encoded = encodeURIComponent(emoji);
38800
38957
  await this.callApi("PUT", `/channels/${chatId}/messages/${messageId}/reactions/${encoded}/@me`);
38801
38958
  }
38959
+ async sendTyping(chatId) {
38960
+ await this.callApi("POST", `/channels/${chatId}/typing`, {});
38961
+ }
38962
+ async editText(chatId, messageId, text) {
38963
+ const res = await this.callApi("PATCH", `/channels/${chatId}/messages/${messageId}`, {
38964
+ content: text
38965
+ });
38966
+ return {
38967
+ messageId: String(res.id || messageId),
38968
+ chatId,
38969
+ timestamp: Date.now()
38970
+ };
38971
+ }
38802
38972
  }
38803
38973
  // src/channels/slack/adapter.ts
38804
38974
  class SlackChannelAdapter extends BaseChannel {
@@ -38901,6 +39071,19 @@ class SlackChannelAdapter extends BaseChannel {
38901
39071
  name: cleanName
38902
39072
  });
38903
39073
  }
39074
+ async sendTyping(chatId) {}
39075
+ async editText(chatId, messageId, text) {
39076
+ const res = await this.callApi("chat.update", {
39077
+ channel: chatId,
39078
+ ts: messageId,
39079
+ text
39080
+ });
39081
+ return {
39082
+ messageId: String(res.ts || messageId),
39083
+ chatId,
39084
+ timestamp: Date.now()
39085
+ };
39086
+ }
38904
39087
  }
38905
39088
  // src/bridges/mcp/index.ts
38906
39089
  function getChannelHubMcpTools() {
@@ -39305,6 +39488,7 @@ export {
39305
39488
  CommandRouter,
39306
39489
  DiscordChannelAdapter,
39307
39490
  SlackChannelAdapter,
39491
+ SmartStreamer,
39308
39492
  TelegramChannelAdapter,
39309
39493
  WebhookBridge,
39310
39494
  Zalo,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theowlops/channelhub",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "ChannelHub SDK - Unified multi-channel messaging toolkit (Zalo, Telegram, Discord, Slack) for AI agents, standalone bots, and enterprise automation.",
5
5
  "module": "dist/index.js",
6
6
  "main": "dist/index.cjs",