eniac-slack 0.1.49 → 0.1.50

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/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.50] - 2026-04-17
4
+
5
+ - feat(slack): 2500자 초과 시 메시지 자동 분할 (#61)
6
+
7
+
3
8
  ## [0.1.49] - 2026-04-17
4
9
 
5
10
  - feat(slack): 텍스트 스트리밍 실시간 전달 (#60)
@@ -1 +1 @@
1
- {"version":3,"file":"slack-messenger.d.ts","sourceRoot":"","sources":["../../src/services/slack-messenger.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAM7C;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,SAAS,EACjB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,cAAc,CAAC,SAAS,EAAE,IAAI,EAAE,OAAO,CAAC,EACrD,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,GACjC,OAAO,CAAC,MAAM,CAAC,CAwGjB"}
1
+ {"version":3,"file":"slack-messenger.d.ts","sourceRoot":"","sources":["../../src/services/slack-messenger.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAO7C;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,SAAS,EACjB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,cAAc,CAAC,SAAS,EAAE,IAAI,EAAE,OAAO,CAAC,EACrD,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,GACjC,OAAO,CAAC,MAAM,CAAC,CAyKjB"}
@@ -1,6 +1,7 @@
1
1
  import { markdownToSlackMrkdwn } from "../utils/slack-format.js";
2
2
  import { CLAUDE_ICON_URL } from "../constants.js";
3
3
  const THROTTLE_MS = 500;
4
+ const MAX_MESSAGE_LENGTH = 2500;
4
5
  /**
5
6
  * Post a streaming reply in a Slack thread, handling both text and tool events.
6
7
  *
@@ -15,25 +16,69 @@ export async function postStreamingReply(client, channel, threadTs, eventStream,
15
16
  text: ":hourglass_flowing_sand: 생각하는 중...",
16
17
  icon_url: CLAUDE_ICON_URL,
17
18
  });
18
- const messageTs = initial.ts;
19
- if (!messageTs) {
19
+ if (!initial.ts) {
20
20
  throw new Error("Failed to post initial message — no ts returned");
21
21
  }
22
+ let messageTs = initial.ts;
22
23
  let accumulated = "";
23
24
  let lastUpdateTime = 0;
24
25
  let pendingUpdate = false;
25
- // Trim accumulated text to the last word boundary to avoid
26
- // partial Korean characters being interpreted as punycode URLs by Slack
26
+ let committedLength = 0;
27
27
  const safeSlice = (text) => {
28
- // Find last whitespace or newline
29
28
  const lastBreak = Math.max(text.lastIndexOf(" "), text.lastIndexOf("\n"), text.lastIndexOf("\t"));
30
- // If break is near the end (within 20 chars), use it; otherwise send everything
31
29
  if (lastBreak > 0 && text.length - lastBreak <= 20) {
32
30
  return text.slice(0, lastBreak + 1);
33
31
  }
34
32
  return text;
35
33
  };
34
+ const findSplitPoint = (text, maxLen) => {
35
+ const region = text.slice(0, maxLen);
36
+ const lastParagraph = region.lastIndexOf("\n\n");
37
+ if (lastParagraph > maxLen * 0.5)
38
+ return lastParagraph + 2;
39
+ const lastLine = region.lastIndexOf("\n");
40
+ if (lastLine > maxLen * 0.5)
41
+ return lastLine + 1;
42
+ const lastSpace = region.lastIndexOf(" ");
43
+ if (lastSpace > maxLen * 0.5)
44
+ return lastSpace + 1;
45
+ return maxLen;
46
+ };
47
+ const commitCurrentMessage = async (text) => {
48
+ try {
49
+ await client.chat.update({
50
+ channel,
51
+ ts: messageTs,
52
+ text: markdownToSlackMrkdwn(text),
53
+ });
54
+ }
55
+ catch (error) {
56
+ console.warn("[slack-messenger] Failed to finalize split message:", error instanceof Error ? error.message : error);
57
+ }
58
+ };
59
+ const startNewMessage = async (initialText) => {
60
+ const next = await client.chat.postMessage({
61
+ channel,
62
+ thread_ts: threadTs,
63
+ text: initialText,
64
+ icon_url: CLAUDE_ICON_URL,
65
+ });
66
+ if (next.ts)
67
+ messageTs = next.ts;
68
+ lastUpdateTime = Date.now();
69
+ pendingUpdate = false;
70
+ };
36
71
  const doUpdate = async (text, isFinal) => {
72
+ const currentText = text.slice(committedLength);
73
+ if (!isFinal && currentText.length > MAX_MESSAGE_LENGTH) {
74
+ const splitPoint = findSplitPoint(currentText, MAX_MESSAGE_LENGTH);
75
+ const chunk = currentText.slice(0, splitPoint);
76
+ await commitCurrentMessage(chunk);
77
+ committedLength += splitPoint;
78
+ const remainder = currentText.slice(splitPoint);
79
+ await startNewMessage(remainder ? safeSlice(remainder) + "\n(생각 중)" : ":hourglass_flowing_sand: 이어서 작성 중...");
80
+ return;
81
+ }
37
82
  const now = Date.now();
38
83
  const elapsed = now - lastUpdateTime;
39
84
  if (!isFinal && elapsed < THROTTLE_MS) {
@@ -42,8 +87,7 @@ export async function postStreamingReply(client, channel, threadTs, eventStream,
42
87
  }
43
88
  pendingUpdate = false;
44
89
  lastUpdateTime = Date.now();
45
- // For intermediate updates, trim to word boundary to prevent garbled display
46
- const displayText = isFinal ? text : safeSlice(text) + "\n(생각 중)";
90
+ const displayText = isFinal ? currentText : safeSlice(currentText) + "\n(생각 중)";
47
91
  try {
48
92
  await client.chat.update({
49
93
  channel,
@@ -82,17 +126,28 @@ export async function postStreamingReply(client, channel, threadTs, eventStream,
82
126
  }
83
127
  // If aborted, edit in-place with interruption indicator.
84
128
  if (options?.signal?.aborted) {
85
- const abortText = accumulated
86
- ? markdownToSlackMrkdwn(accumulated) + "\n\n:no_entry_sign: 새로운 입력으로 인해 중단되었습니다."
129
+ const currentText = accumulated.slice(committedLength);
130
+ const abortText = currentText
131
+ ? markdownToSlackMrkdwn(currentText) + "\n\n:no_entry_sign: 새로운 입력으로 인해 중단되었습니다."
87
132
  : ":no_entry_sign: 새로운 입력으로 인해 중단되었습니다.";
88
- await doUpdate(abortText, true);
133
+ try {
134
+ await client.chat.update({ channel, ts: messageTs, text: abortText });
135
+ }
136
+ catch (error) {
137
+ console.warn("[slack-messenger] Failed to update abort message:", error instanceof Error ? error.message : error);
138
+ }
89
139
  return messageTs;
90
140
  }
91
141
  // Normal completion — always edit in-place (no delete+resend race conditions).
92
- const finalText = accumulated
93
- ? markdownToSlackMrkdwn(accumulated)
94
- : ":speech_balloon: 응답을 생성하지 못했어요. 다시 한번 말씀해 주시겠어요?";
95
- await doUpdate(finalText, true);
142
+ const currentText = accumulated.slice(committedLength);
143
+ const finalText = currentText
144
+ ? markdownToSlackMrkdwn(currentText)
145
+ : committedLength > 0
146
+ ? null
147
+ : ":speech_balloon: 응답을 생성하지 못했어요. 다시 한번 말씀해 주시겠어요?";
148
+ if (finalText) {
149
+ await doUpdate(finalText, true);
150
+ }
96
151
  return messageTs;
97
152
  }
98
153
  //# sourceMappingURL=slack-messenger.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"slack-messenger.js","sourceRoot":"","sources":["../../src/services/slack-messenger.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"slack-messenger.js","sourceRoot":"","sources":["../../src/services/slack-messenger.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eniac-slack",
3
- "version": "0.1.49",
3
+ "version": "0.1.50",
4
4
  "type": "module",
5
5
  "bin": "./dist/cli.js",
6
6
  "scripts": {
@@ -4,6 +4,7 @@ import { markdownToSlackMrkdwn } from "../utils/slack-format.js";
4
4
  import { CLAUDE_ICON_URL } from "../constants.js";
5
5
 
6
6
  const THROTTLE_MS = 500;
7
+ const MAX_MESSAGE_LENGTH = 2500;
7
8
 
8
9
  /**
9
10
  * Post a streaming reply in a Slack thread, handling both text and tool events.
@@ -26,32 +27,83 @@ export async function postStreamingReply(
26
27
  icon_url: CLAUDE_ICON_URL,
27
28
  });
28
29
 
29
- const messageTs = initial.ts;
30
- if (!messageTs) {
30
+ if (!initial.ts) {
31
31
  throw new Error("Failed to post initial message — no ts returned");
32
32
  }
33
+ let messageTs: string = initial.ts;
33
34
 
34
35
  let accumulated = "";
35
36
  let lastUpdateTime = 0;
36
37
  let pendingUpdate = false;
38
+ let committedLength = 0;
37
39
 
38
- // Trim accumulated text to the last word boundary to avoid
39
- // partial Korean characters being interpreted as punycode URLs by Slack
40
40
  const safeSlice = (text: string): string => {
41
- // Find last whitespace or newline
42
41
  const lastBreak = Math.max(
43
42
  text.lastIndexOf(" "),
44
43
  text.lastIndexOf("\n"),
45
44
  text.lastIndexOf("\t")
46
45
  );
47
- // If break is near the end (within 20 chars), use it; otherwise send everything
48
46
  if (lastBreak > 0 && text.length - lastBreak <= 20) {
49
47
  return text.slice(0, lastBreak + 1);
50
48
  }
51
49
  return text;
52
50
  };
53
51
 
52
+ const findSplitPoint = (text: string, maxLen: number): number => {
53
+ const region = text.slice(0, maxLen);
54
+ const lastParagraph = region.lastIndexOf("\n\n");
55
+ if (lastParagraph > maxLen * 0.5) return lastParagraph + 2;
56
+ const lastLine = region.lastIndexOf("\n");
57
+ if (lastLine > maxLen * 0.5) return lastLine + 1;
58
+ const lastSpace = region.lastIndexOf(" ");
59
+ if (lastSpace > maxLen * 0.5) return lastSpace + 1;
60
+ return maxLen;
61
+ };
62
+
63
+ const commitCurrentMessage = async (text: string) => {
64
+ try {
65
+ await client.chat.update({
66
+ channel,
67
+ ts: messageTs,
68
+ text: markdownToSlackMrkdwn(text),
69
+ });
70
+ } catch (error) {
71
+ console.warn(
72
+ "[slack-messenger] Failed to finalize split message:",
73
+ error instanceof Error ? error.message : error
74
+ );
75
+ }
76
+ };
77
+
78
+ const startNewMessage = async (initialText: string) => {
79
+ const next = await client.chat.postMessage({
80
+ channel,
81
+ thread_ts: threadTs,
82
+ text: initialText,
83
+ icon_url: CLAUDE_ICON_URL,
84
+ });
85
+ if (next.ts) messageTs = next.ts;
86
+ lastUpdateTime = Date.now();
87
+ pendingUpdate = false;
88
+ };
89
+
54
90
  const doUpdate = async (text: string, isFinal: boolean) => {
91
+ const currentText = text.slice(committedLength);
92
+
93
+ if (!isFinal && currentText.length > MAX_MESSAGE_LENGTH) {
94
+ const splitPoint = findSplitPoint(currentText, MAX_MESSAGE_LENGTH);
95
+ const chunk = currentText.slice(0, splitPoint);
96
+
97
+ await commitCurrentMessage(chunk);
98
+ committedLength += splitPoint;
99
+
100
+ const remainder = currentText.slice(splitPoint);
101
+ await startNewMessage(
102
+ remainder ? safeSlice(remainder) + "\n(생각 중)" : ":hourglass_flowing_sand: 이어서 작성 중..."
103
+ );
104
+ return;
105
+ }
106
+
55
107
  const now = Date.now();
56
108
  const elapsed = now - lastUpdateTime;
57
109
 
@@ -63,8 +115,7 @@ export async function postStreamingReply(
63
115
  pendingUpdate = false;
64
116
  lastUpdateTime = Date.now();
65
117
 
66
- // For intermediate updates, trim to word boundary to prevent garbled display
67
- const displayText = isFinal ? text : safeSlice(text) + "\n(생각 중)";
118
+ const displayText = isFinal ? currentText : safeSlice(currentText) + "\n(생각 중)";
68
119
 
69
120
  try {
70
121
  await client.chat.update({
@@ -109,17 +160,32 @@ export async function postStreamingReply(
109
160
 
110
161
  // If aborted, edit in-place with interruption indicator.
111
162
  if (options?.signal?.aborted) {
112
- const abortText = accumulated
113
- ? markdownToSlackMrkdwn(accumulated) + "\n\n:no_entry_sign: 새로운 입력으로 인해 중단되었습니다."
163
+ const currentText = accumulated.slice(committedLength);
164
+ const abortText = currentText
165
+ ? markdownToSlackMrkdwn(currentText) + "\n\n:no_entry_sign: 새로운 입력으로 인해 중단되었습니다."
114
166
  : ":no_entry_sign: 새로운 입력으로 인해 중단되었습니다.";
115
- await doUpdate(abortText, true);
167
+ try {
168
+ await client.chat.update({ channel, ts: messageTs, text: abortText });
169
+ } catch (error) {
170
+ console.warn(
171
+ "[slack-messenger] Failed to update abort message:",
172
+ error instanceof Error ? error.message : error
173
+ );
174
+ }
116
175
  return messageTs;
117
176
  }
118
177
 
119
178
  // Normal completion — always edit in-place (no delete+resend race conditions).
120
- const finalText = accumulated
121
- ? markdownToSlackMrkdwn(accumulated)
122
- : ":speech_balloon: 응답을 생성하지 못했어요. 다시 한번 말씀해 주시겠어요?";
123
- await doUpdate(finalText, true);
179
+ const currentText = accumulated.slice(committedLength);
180
+ const finalText = currentText
181
+ ? markdownToSlackMrkdwn(currentText)
182
+ : committedLength > 0
183
+ ? null
184
+ : ":speech_balloon: 응답을 생성하지 못했어요. 다시 한번 말씀해 주시겠어요?";
185
+
186
+ if (finalText) {
187
+ await doUpdate(finalText, true);
188
+ }
189
+
124
190
  return messageTs;
125
191
  }