kimaki 0.2.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/discordBot.js +87 -37
- package/dist/utils.js +10 -1
- package/package.json +3 -2
- package/src/discordBot.ts +84 -37
- package/src/utils.ts +16 -2
package/dist/discordBot.js
CHANGED
|
@@ -18,6 +18,10 @@ import { transcribeAudio } from './voice.js';
|
|
|
18
18
|
import { extractTagsArrays, extractNonXmlContent } from './xml.js';
|
|
19
19
|
import prettyMilliseconds from 'pretty-ms';
|
|
20
20
|
import { createLogger } from './logger.js';
|
|
21
|
+
import { isAbortError } from './utils.js';
|
|
22
|
+
import { setGlobalDispatcher, Agent } from 'undici';
|
|
23
|
+
// disables the automatic 5 minutes abort after no body
|
|
24
|
+
setGlobalDispatcher(new Agent({ headersTimeout: 0, bodyTimeout: 0 }));
|
|
21
25
|
const discordLogger = createLogger('DISCORD');
|
|
22
26
|
const voiceLogger = createLogger('VOICE');
|
|
23
27
|
const opencodeLogger = createLogger('OPENCODE');
|
|
@@ -615,6 +619,41 @@ export async function initializeOpencodeForDirectory(directory) {
|
|
|
615
619
|
cwd: directory,
|
|
616
620
|
env: {
|
|
617
621
|
...process.env,
|
|
622
|
+
OPENCODE_CONFIG_CONTENT: JSON.stringify({
|
|
623
|
+
$schema: 'https://opencode.ai/config.json',
|
|
624
|
+
lsp: {
|
|
625
|
+
typescript: { disabled: true },
|
|
626
|
+
eslint: { disabled: true },
|
|
627
|
+
gopls: { disabled: true },
|
|
628
|
+
'ruby-lsp': { disabled: true },
|
|
629
|
+
pyright: { disabled: true },
|
|
630
|
+
'elixir-ls': { disabled: true },
|
|
631
|
+
zls: { disabled: true },
|
|
632
|
+
csharp: { disabled: true },
|
|
633
|
+
vue: { disabled: true },
|
|
634
|
+
rust: { disabled: true },
|
|
635
|
+
clangd: { disabled: true },
|
|
636
|
+
svelte: { disabled: true },
|
|
637
|
+
},
|
|
638
|
+
formatter: {
|
|
639
|
+
prettier: { disabled: true },
|
|
640
|
+
biome: { disabled: true },
|
|
641
|
+
gofmt: { disabled: true },
|
|
642
|
+
mix: { disabled: true },
|
|
643
|
+
zig: { disabled: true },
|
|
644
|
+
'clang-format': { disabled: true },
|
|
645
|
+
ktlint: { disabled: true },
|
|
646
|
+
ruff: { disabled: true },
|
|
647
|
+
rubocop: { disabled: true },
|
|
648
|
+
standardrb: { disabled: true },
|
|
649
|
+
htmlbeautifier: { disabled: true },
|
|
650
|
+
},
|
|
651
|
+
permission: {
|
|
652
|
+
edit: 'allow',
|
|
653
|
+
bash: 'allow',
|
|
654
|
+
webfetch: 'allow',
|
|
655
|
+
},
|
|
656
|
+
}),
|
|
618
657
|
OPENCODE_PORT: port.toString(),
|
|
619
658
|
},
|
|
620
659
|
});
|
|
@@ -679,22 +718,29 @@ function formatPart(part) {
|
|
|
679
718
|
return `▪︎ thinking: ${escapeDiscordFormatting(part.text || '')}`;
|
|
680
719
|
case 'tool':
|
|
681
720
|
if (part.state.status === 'completed' || part.state.status === 'error') {
|
|
682
|
-
// console.log(part)
|
|
683
|
-
// Escape triple backticks so Discord does not break code blocks
|
|
684
721
|
let language = '';
|
|
685
722
|
let outputToDisplay = '';
|
|
723
|
+
let summaryText = '';
|
|
686
724
|
if (part.tool === 'bash') {
|
|
687
|
-
|
|
688
|
-
part.state.
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
725
|
+
const output = part.state.status === 'completed'
|
|
726
|
+
? part.state.output
|
|
727
|
+
: part.state.error;
|
|
728
|
+
const lines = (output || '').split('\n').filter((l) => l.trim());
|
|
729
|
+
summaryText = `(${lines.length} line${lines.length === 1 ? '' : 's'})`;
|
|
692
730
|
}
|
|
693
|
-
if (part.tool === 'edit') {
|
|
694
|
-
|
|
695
|
-
|
|
731
|
+
else if (part.tool === 'edit') {
|
|
732
|
+
const newString = part.state.input?.newString || '';
|
|
733
|
+
const oldString = part.state.input?.oldString || '';
|
|
734
|
+
const added = newString.split('\n').length;
|
|
735
|
+
const removed = oldString.split('\n').length;
|
|
736
|
+
summaryText = `(+${added}-${removed})`;
|
|
696
737
|
}
|
|
697
|
-
if (part.tool === '
|
|
738
|
+
else if (part.tool === 'write') {
|
|
739
|
+
const content = part.state.input?.content || '';
|
|
740
|
+
const lines = content.split('\n').length;
|
|
741
|
+
summaryText = `(${lines} line${lines === 1 ? '' : 's'})`;
|
|
742
|
+
}
|
|
743
|
+
else if (part.tool === 'todowrite') {
|
|
698
744
|
const todos = part.state.input?.todos || [];
|
|
699
745
|
outputToDisplay = todos
|
|
700
746
|
.map((todo) => {
|
|
@@ -717,20 +763,27 @@ function formatPart(part) {
|
|
|
717
763
|
})
|
|
718
764
|
.filter(Boolean)
|
|
719
765
|
.join('\n');
|
|
720
|
-
language = '';
|
|
721
766
|
}
|
|
722
|
-
if (part.tool === '
|
|
723
|
-
|
|
724
|
-
|
|
767
|
+
else if (part.tool === 'webfetch') {
|
|
768
|
+
const url = part.state.input?.url || '';
|
|
769
|
+
const urlWithoutProtocol = url.replace(/^https?:\/\//, '');
|
|
770
|
+
summaryText = urlWithoutProtocol ? `(${urlWithoutProtocol})` : '';
|
|
771
|
+
}
|
|
772
|
+
else if (part.state.input) {
|
|
773
|
+
const inputFields = Object.entries(part.state.input)
|
|
774
|
+
.map(([key, value]) => {
|
|
775
|
+
if (value === null || value === undefined)
|
|
776
|
+
return null;
|
|
777
|
+
const stringValue = typeof value === 'string' ? value : JSON.stringify(value);
|
|
778
|
+
const truncatedValue = stringValue.length > 100 ? stringValue.slice(0, 100) + '…' : stringValue;
|
|
779
|
+
return `${key}: ${truncatedValue}`;
|
|
780
|
+
})
|
|
781
|
+
.filter(Boolean);
|
|
782
|
+
if (inputFields.length > 0) {
|
|
783
|
+
outputToDisplay = inputFields.join('\n');
|
|
784
|
+
}
|
|
725
785
|
}
|
|
726
|
-
outputToDisplay =
|
|
727
|
-
outputToDisplay.length > 500
|
|
728
|
-
? outputToDisplay.slice(0, 497) + `…`
|
|
729
|
-
: outputToDisplay;
|
|
730
|
-
// Escape Discord formatting characters that could break code blocks
|
|
731
|
-
outputToDisplay = escapeDiscordFormatting(outputToDisplay);
|
|
732
786
|
let toolTitle = part.state.status === 'completed' ? part.state.title || '' : 'error';
|
|
733
|
-
// Escape backticks in the title before wrapping in backticks
|
|
734
787
|
if (toolTitle) {
|
|
735
788
|
toolTitle = `\`${escapeInlineCode(toolTitle)}\``;
|
|
736
789
|
}
|
|
@@ -739,19 +792,10 @@ function formatPart(part) {
|
|
|
739
792
|
: part.state.status === 'error'
|
|
740
793
|
? '⨯'
|
|
741
794
|
: '';
|
|
742
|
-
const title = `${icon} ${part.tool} ${toolTitle}`;
|
|
795
|
+
const title = `${icon} ${part.tool} ${toolTitle} ${summaryText}`;
|
|
743
796
|
let text = title;
|
|
744
797
|
if (outputToDisplay) {
|
|
745
|
-
|
|
746
|
-
if (part.tool === 'todowrite') {
|
|
747
|
-
text += '\n\n' + outputToDisplay;
|
|
748
|
-
}
|
|
749
|
-
else {
|
|
750
|
-
if (language.startsWith('.')) {
|
|
751
|
-
language = language.slice(1);
|
|
752
|
-
}
|
|
753
|
-
text += '\n\n```' + language + '\n' + outputToDisplay + '\n```';
|
|
754
|
-
}
|
|
798
|
+
text += '\n\n' + outputToDisplay;
|
|
755
799
|
}
|
|
756
800
|
return text;
|
|
757
801
|
}
|
|
@@ -1041,8 +1085,7 @@ async function handleOpencodeSession(prompt, thread, projectDirectory, originalM
|
|
|
1041
1085
|
}
|
|
1042
1086
|
}
|
|
1043
1087
|
catch (e) {
|
|
1044
|
-
if (e
|
|
1045
|
-
// Ignore abort controller errors as requested
|
|
1088
|
+
if (isAbortError(e, abortController.signal)) {
|
|
1046
1089
|
sessionLogger.log('AbortController aborted event handling (normal exit)');
|
|
1047
1090
|
return;
|
|
1048
1091
|
}
|
|
@@ -1118,7 +1161,7 @@ async function handleOpencodeSession(prompt, thread, projectDirectory, originalM
|
|
|
1118
1161
|
}
|
|
1119
1162
|
catch (error) {
|
|
1120
1163
|
sessionLogger.error(`ERROR: Failed to send prompt:`, error);
|
|
1121
|
-
if (!(error
|
|
1164
|
+
if (!isAbortError(error, abortController.signal)) {
|
|
1122
1165
|
abortController.abort('error');
|
|
1123
1166
|
if (originalMessage) {
|
|
1124
1167
|
try {
|
|
@@ -1130,7 +1173,14 @@ async function handleOpencodeSession(prompt, thread, projectDirectory, originalM
|
|
|
1130
1173
|
discordLogger.log(`Could not update reaction:`, e);
|
|
1131
1174
|
}
|
|
1132
1175
|
}
|
|
1133
|
-
|
|
1176
|
+
// Always log the error's constructor name (if any) and make error reporting more readable
|
|
1177
|
+
const errorName = error && typeof error === 'object' && 'constructor' in error && error.constructor && typeof error.constructor.name === 'string'
|
|
1178
|
+
? error.constructor.name
|
|
1179
|
+
: typeof error;
|
|
1180
|
+
const errorMsg = error instanceof Error
|
|
1181
|
+
? (error.stack || error.message)
|
|
1182
|
+
: String(error);
|
|
1183
|
+
await sendThreadMessage(thread, `✗ Unexpected bot Error: [${errorName}]\n${errorMsg}`);
|
|
1134
1184
|
}
|
|
1135
1185
|
}
|
|
1136
1186
|
}
|
package/dist/utils.js
CHANGED
|
@@ -30,7 +30,7 @@ export function generateBotInstallUrl({ clientId, permissions = [
|
|
|
30
30
|
}
|
|
31
31
|
export function deduplicateByKey(arr, keyFn) {
|
|
32
32
|
const seen = new Set();
|
|
33
|
-
return arr.filter(item => {
|
|
33
|
+
return arr.filter((item) => {
|
|
34
34
|
const key = keyFn(item);
|
|
35
35
|
if (seen.has(key)) {
|
|
36
36
|
return false;
|
|
@@ -39,3 +39,12 @@ export function deduplicateByKey(arr, keyFn) {
|
|
|
39
39
|
return true;
|
|
40
40
|
});
|
|
41
41
|
}
|
|
42
|
+
export function isAbortError(error, signal) {
|
|
43
|
+
return (error instanceof Error &&
|
|
44
|
+
(error.name === 'AbortError' ||
|
|
45
|
+
error.name === 'Aborterror' ||
|
|
46
|
+
error.name === 'aborterror' ||
|
|
47
|
+
error.name.toLowerCase() === 'aborterror' ||
|
|
48
|
+
error.message?.includes('aborted') ||
|
|
49
|
+
(signal?.aborted ?? false)));
|
|
50
|
+
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "kimaki",
|
|
3
3
|
"module": "index.ts",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.3.1",
|
|
6
6
|
"repository": "https://github.com/remorses/kimaki",
|
|
7
7
|
"bin": "bin.js",
|
|
8
8
|
"files": [
|
|
@@ -41,10 +41,11 @@
|
|
|
41
41
|
"pretty-ms": "^9.3.0",
|
|
42
42
|
"prism-media": "^1.3.5",
|
|
43
43
|
"string-dedent": "^3.0.2",
|
|
44
|
+
"undici": "^7.16.0",
|
|
44
45
|
"zod": "^4.0.17"
|
|
45
46
|
},
|
|
46
47
|
"scripts": {
|
|
47
|
-
"dev": "pnpm tsc &&
|
|
48
|
+
"dev": "pnpm tsc && tsx --env-file .env src/cli.ts",
|
|
48
49
|
"dev:bun": "DEBUG=1 bun --env-file .env src/cli.ts",
|
|
49
50
|
"test": "tsx scripts/test-opencode.ts",
|
|
50
51
|
"watch": "tsx scripts/watch-session.ts",
|
package/src/discordBot.ts
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
createOpencodeClient,
|
|
3
3
|
type OpencodeClient,
|
|
4
4
|
type Part,
|
|
5
|
+
type Config,
|
|
5
6
|
} from '@opencode-ai/sdk'
|
|
6
7
|
|
|
7
8
|
import { createGenAIWorker, type GenAIWorker } from './genai-worker-wrapper.js'
|
|
@@ -45,6 +46,10 @@ import { extractTagsArrays, extractNonXmlContent } from './xml.js'
|
|
|
45
46
|
import prettyMilliseconds from 'pretty-ms'
|
|
46
47
|
import type { Session } from '@google/genai'
|
|
47
48
|
import { createLogger } from './logger.js'
|
|
49
|
+
import { isAbortError } from './utils.js'
|
|
50
|
+
import { setGlobalDispatcher, Agent } from 'undici'
|
|
51
|
+
// disables the automatic 5 minutes abort after no body
|
|
52
|
+
setGlobalDispatcher(new Agent({ headersTimeout: 0, bodyTimeout: 0 }))
|
|
48
53
|
|
|
49
54
|
const discordLogger = createLogger('DISCORD')
|
|
50
55
|
const voiceLogger = createLogger('VOICE')
|
|
@@ -833,6 +838,41 @@ export async function initializeOpencodeForDirectory(directory: string) {
|
|
|
833
838
|
cwd: directory,
|
|
834
839
|
env: {
|
|
835
840
|
...process.env,
|
|
841
|
+
OPENCODE_CONFIG_CONTENT: JSON.stringify({
|
|
842
|
+
$schema: 'https://opencode.ai/config.json',
|
|
843
|
+
lsp: {
|
|
844
|
+
typescript: { disabled: true },
|
|
845
|
+
eslint: { disabled: true },
|
|
846
|
+
gopls: { disabled: true },
|
|
847
|
+
'ruby-lsp': { disabled: true },
|
|
848
|
+
pyright: { disabled: true },
|
|
849
|
+
'elixir-ls': { disabled: true },
|
|
850
|
+
zls: { disabled: true },
|
|
851
|
+
csharp: { disabled: true },
|
|
852
|
+
vue: { disabled: true },
|
|
853
|
+
rust: { disabled: true },
|
|
854
|
+
clangd: { disabled: true },
|
|
855
|
+
svelte: { disabled: true },
|
|
856
|
+
},
|
|
857
|
+
formatter: {
|
|
858
|
+
prettier: { disabled: true },
|
|
859
|
+
biome: { disabled: true },
|
|
860
|
+
gofmt: { disabled: true },
|
|
861
|
+
mix: { disabled: true },
|
|
862
|
+
zig: { disabled: true },
|
|
863
|
+
'clang-format': { disabled: true },
|
|
864
|
+
ktlint: { disabled: true },
|
|
865
|
+
ruff: { disabled: true },
|
|
866
|
+
rubocop: { disabled: true },
|
|
867
|
+
standardrb: { disabled: true },
|
|
868
|
+
htmlbeautifier: { disabled: true },
|
|
869
|
+
},
|
|
870
|
+
permission: {
|
|
871
|
+
edit: 'allow',
|
|
872
|
+
bash: 'allow',
|
|
873
|
+
webfetch: 'allow',
|
|
874
|
+
},
|
|
875
|
+
} satisfies Config),
|
|
836
876
|
OPENCODE_PORT: port.toString(),
|
|
837
877
|
},
|
|
838
878
|
},
|
|
@@ -914,22 +954,28 @@ function formatPart(part: Part): string {
|
|
|
914
954
|
return `▪︎ thinking: ${escapeDiscordFormatting(part.text || '')}`
|
|
915
955
|
case 'tool':
|
|
916
956
|
if (part.state.status === 'completed' || part.state.status === 'error') {
|
|
917
|
-
// console.log(part)
|
|
918
|
-
// Escape triple backticks so Discord does not break code blocks
|
|
919
957
|
let language = ''
|
|
920
958
|
let outputToDisplay = ''
|
|
959
|
+
let summaryText = ''
|
|
960
|
+
|
|
921
961
|
if (part.tool === 'bash') {
|
|
922
|
-
|
|
962
|
+
const output =
|
|
923
963
|
part.state.status === 'completed'
|
|
924
964
|
? part.state.output
|
|
925
965
|
: part.state.error
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
if (part.tool === 'edit') {
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
966
|
+
const lines = (output || '').split('\n').filter((l) => l.trim())
|
|
967
|
+
summaryText = `(${lines.length} line${lines.length === 1 ? '' : 's'})`
|
|
968
|
+
} else if (part.tool === 'edit') {
|
|
969
|
+
const newString = (part.state.input?.newString as string) || ''
|
|
970
|
+
const oldString = (part.state.input?.oldString as string) || ''
|
|
971
|
+
const added = newString.split('\n').length
|
|
972
|
+
const removed = oldString.split('\n').length
|
|
973
|
+
summaryText = `(+${added}-${removed})`
|
|
974
|
+
} else if (part.tool === 'write') {
|
|
975
|
+
const content = (part.state.input?.content as string) || ''
|
|
976
|
+
const lines = content.split('\n').length
|
|
977
|
+
summaryText = `(${lines} line${lines === 1 ? '' : 's'})`
|
|
978
|
+
} else if (part.tool === 'todowrite') {
|
|
933
979
|
const todos =
|
|
934
980
|
(part.state.input?.todos as {
|
|
935
981
|
content: string
|
|
@@ -956,23 +1002,26 @@ function formatPart(part: Part): string {
|
|
|
956
1002
|
})
|
|
957
1003
|
.filter(Boolean)
|
|
958
1004
|
.join('\n')
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
1005
|
+
} else if (part.tool === 'webfetch') {
|
|
1006
|
+
const url = (part.state.input?.url as string) || ''
|
|
1007
|
+
const urlWithoutProtocol = url.replace(/^https?:\/\//, '')
|
|
1008
|
+
summaryText = urlWithoutProtocol ? `(${urlWithoutProtocol})` : ''
|
|
1009
|
+
} else if (part.state.input) {
|
|
1010
|
+
const inputFields = Object.entries(part.state.input)
|
|
1011
|
+
.map(([key, value]) => {
|
|
1012
|
+
if (value === null || value === undefined) return null
|
|
1013
|
+
const stringValue = typeof value === 'string' ? value : JSON.stringify(value)
|
|
1014
|
+
const truncatedValue = stringValue.length > 100 ? stringValue.slice(0, 100) + '…' : stringValue
|
|
1015
|
+
return `${key}: ${truncatedValue}`
|
|
1016
|
+
})
|
|
1017
|
+
.filter(Boolean)
|
|
1018
|
+
if (inputFields.length > 0) {
|
|
1019
|
+
outputToDisplay = inputFields.join('\n')
|
|
1020
|
+
}
|
|
964
1021
|
}
|
|
965
|
-
outputToDisplay =
|
|
966
|
-
outputToDisplay.length > 500
|
|
967
|
-
? outputToDisplay.slice(0, 497) + `…`
|
|
968
|
-
: outputToDisplay
|
|
969
|
-
|
|
970
|
-
// Escape Discord formatting characters that could break code blocks
|
|
971
|
-
outputToDisplay = escapeDiscordFormatting(outputToDisplay)
|
|
972
1022
|
|
|
973
1023
|
let toolTitle =
|
|
974
1024
|
part.state.status === 'completed' ? part.state.title || '' : 'error'
|
|
975
|
-
// Escape backticks in the title before wrapping in backticks
|
|
976
1025
|
if (toolTitle) {
|
|
977
1026
|
toolTitle = `\`${escapeInlineCode(toolTitle)}\``
|
|
978
1027
|
}
|
|
@@ -982,20 +1031,12 @@ function formatPart(part: Part): string {
|
|
|
982
1031
|
: part.state.status === 'error'
|
|
983
1032
|
? '⨯'
|
|
984
1033
|
: ''
|
|
985
|
-
const title = `${icon} ${part.tool} ${toolTitle}`
|
|
1034
|
+
const title = `${icon} ${part.tool} ${toolTitle} ${summaryText}`
|
|
986
1035
|
|
|
987
1036
|
let text = title
|
|
988
1037
|
|
|
989
1038
|
if (outputToDisplay) {
|
|
990
|
-
|
|
991
|
-
if (part.tool === 'todowrite') {
|
|
992
|
-
text += '\n\n' + outputToDisplay
|
|
993
|
-
} else {
|
|
994
|
-
if (language.startsWith('.')) {
|
|
995
|
-
language = language.slice(1)
|
|
996
|
-
}
|
|
997
|
-
text += '\n\n```' + language + '\n' + outputToDisplay + '\n```'
|
|
998
|
-
}
|
|
1039
|
+
text += '\n\n' + outputToDisplay
|
|
999
1040
|
}
|
|
1000
1041
|
return text
|
|
1001
1042
|
}
|
|
@@ -1364,8 +1405,7 @@ async function handleOpencodeSession(
|
|
|
1364
1405
|
}
|
|
1365
1406
|
}
|
|
1366
1407
|
} catch (e) {
|
|
1367
|
-
if (e
|
|
1368
|
-
// Ignore abort controller errors as requested
|
|
1408
|
+
if (isAbortError(e, abortController.signal)) {
|
|
1369
1409
|
sessionLogger.log(
|
|
1370
1410
|
'AbortController aborted event handling (normal exit)',
|
|
1371
1411
|
)
|
|
@@ -1462,7 +1502,7 @@ async function handleOpencodeSession(
|
|
|
1462
1502
|
} catch (error) {
|
|
1463
1503
|
sessionLogger.error(`ERROR: Failed to send prompt:`, error)
|
|
1464
1504
|
|
|
1465
|
-
if (!(error
|
|
1505
|
+
if (!isAbortError(error, abortController.signal)) {
|
|
1466
1506
|
abortController.abort('error')
|
|
1467
1507
|
|
|
1468
1508
|
if (originalMessage) {
|
|
@@ -1474,9 +1514,16 @@ async function handleOpencodeSession(
|
|
|
1474
1514
|
discordLogger.log(`Could not update reaction:`, e)
|
|
1475
1515
|
}
|
|
1476
1516
|
}
|
|
1517
|
+
// Always log the error's constructor name (if any) and make error reporting more readable
|
|
1518
|
+
const errorName = error && typeof error === 'object' && 'constructor' in error && error.constructor && typeof error.constructor.name === 'string'
|
|
1519
|
+
? error.constructor.name
|
|
1520
|
+
: typeof error
|
|
1521
|
+
const errorMsg = error instanceof Error
|
|
1522
|
+
? (error.stack || error.message)
|
|
1523
|
+
: String(error)
|
|
1477
1524
|
await sendThreadMessage(
|
|
1478
1525
|
thread,
|
|
1479
|
-
`✗ Unexpected bot Error: ${
|
|
1526
|
+
`✗ Unexpected bot Error: [${errorName}]\n${errorMsg}`,
|
|
1480
1527
|
)
|
|
1481
1528
|
}
|
|
1482
1529
|
}
|
package/src/utils.ts
CHANGED
|
@@ -48,10 +48,9 @@ export function generateBotInstallUrl({
|
|
|
48
48
|
return url.toString()
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
|
|
52
51
|
export function deduplicateByKey<T, K>(arr: T[], keyFn: (item: T) => K): T[] {
|
|
53
52
|
const seen = new Set<K>()
|
|
54
|
-
return arr.filter(item => {
|
|
53
|
+
return arr.filter((item) => {
|
|
55
54
|
const key = keyFn(item)
|
|
56
55
|
if (seen.has(key)) {
|
|
57
56
|
return false
|
|
@@ -60,3 +59,18 @@ export function deduplicateByKey<T, K>(arr: T[], keyFn: (item: T) => K): T[] {
|
|
|
60
59
|
return true
|
|
61
60
|
})
|
|
62
61
|
}
|
|
62
|
+
|
|
63
|
+
export function isAbortError(
|
|
64
|
+
error: unknown,
|
|
65
|
+
signal?: AbortSignal,
|
|
66
|
+
): error is Error {
|
|
67
|
+
return (
|
|
68
|
+
error instanceof Error &&
|
|
69
|
+
(error.name === 'AbortError' ||
|
|
70
|
+
error.name === 'Aborterror' ||
|
|
71
|
+
error.name === 'aborterror' ||
|
|
72
|
+
error.name.toLowerCase() === 'aborterror' ||
|
|
73
|
+
error.message?.includes('aborted') ||
|
|
74
|
+
(signal?.aborted ?? false))
|
|
75
|
+
)
|
|
76
|
+
}
|