kimaki 0.3.2 → 0.4.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 +43 -30
- package/package.json +13 -12
- package/src/cli.ts +0 -0
- package/src/discordBot.ts +43 -35
package/dist/discordBot.js
CHANGED
|
@@ -718,7 +718,6 @@ function formatPart(part) {
|
|
|
718
718
|
return `▪︎ thinking: ${escapeDiscordFormatting(part.text || '')}`;
|
|
719
719
|
case 'tool':
|
|
720
720
|
if (part.state.status === 'completed' || part.state.status === 'error') {
|
|
721
|
-
let language = '';
|
|
722
721
|
let outputToDisplay = '';
|
|
723
722
|
let summaryText = '';
|
|
724
723
|
if (part.tool === 'bash') {
|
|
@@ -740,6 +739,23 @@ function formatPart(part) {
|
|
|
740
739
|
const lines = content.split('\n').length;
|
|
741
740
|
summaryText = `(${lines} line${lines === 1 ? '' : 's'})`;
|
|
742
741
|
}
|
|
742
|
+
else if (part.tool === 'read') {
|
|
743
|
+
}
|
|
744
|
+
else if (part.tool === 'write') {
|
|
745
|
+
}
|
|
746
|
+
else if (part.tool === 'edit') {
|
|
747
|
+
}
|
|
748
|
+
else if (part.tool === 'list') {
|
|
749
|
+
}
|
|
750
|
+
else if (part.tool === 'glob') {
|
|
751
|
+
}
|
|
752
|
+
else if (part.tool === 'grep') {
|
|
753
|
+
}
|
|
754
|
+
else if (part.tool === 'task') {
|
|
755
|
+
}
|
|
756
|
+
else if (part.tool === 'todoread') {
|
|
757
|
+
// Special handling for read - don't show arguments
|
|
758
|
+
}
|
|
743
759
|
else if (part.tool === 'todowrite') {
|
|
744
760
|
const todos = part.state.input?.todos || [];
|
|
745
761
|
outputToDisplay = todos
|
|
@@ -775,12 +791,14 @@ function formatPart(part) {
|
|
|
775
791
|
if (value === null || value === undefined)
|
|
776
792
|
return null;
|
|
777
793
|
const stringValue = typeof value === 'string' ? value : JSON.stringify(value);
|
|
778
|
-
const truncatedValue = stringValue.length > 100
|
|
794
|
+
const truncatedValue = stringValue.length > 100
|
|
795
|
+
? stringValue.slice(0, 100) + '…'
|
|
796
|
+
: stringValue;
|
|
779
797
|
return `${key}: ${truncatedValue}`;
|
|
780
798
|
})
|
|
781
799
|
.filter(Boolean);
|
|
782
800
|
if (inputFields.length > 0) {
|
|
783
|
-
outputToDisplay = inputFields.join('
|
|
801
|
+
outputToDisplay = inputFields.join(', ');
|
|
784
802
|
}
|
|
785
803
|
}
|
|
786
804
|
let toolTitle = part.state.status === 'completed' ? part.state.title || '' : 'error';
|
|
@@ -889,17 +907,14 @@ async function handleOpencodeSession(prompt, thread, projectDirectory, originalM
|
|
|
889
907
|
const existingController = abortControllers.get(session.id);
|
|
890
908
|
if (existingController) {
|
|
891
909
|
voiceLogger.log(`[ABORT] Cancelling existing request for session: ${session.id}`);
|
|
892
|
-
existingController.abort('New request started');
|
|
910
|
+
existingController.abort(new Error('New request started'));
|
|
893
911
|
}
|
|
894
912
|
if (abortControllers.has(session.id)) {
|
|
895
|
-
abortControllers.get(session.id)?.abort('new reply');
|
|
913
|
+
abortControllers.get(session.id)?.abort(new Error('new reply'));
|
|
896
914
|
}
|
|
897
|
-
const
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
const eventsResult = await getClient().event.subscribe({
|
|
901
|
-
signal: abortController.signal,
|
|
902
|
-
});
|
|
915
|
+
const promptAbortController = new AbortController();
|
|
916
|
+
abortControllers.set(session.id, promptAbortController);
|
|
917
|
+
const eventsResult = await getClient().event.subscribe({});
|
|
903
918
|
const events = eventsResult.stream;
|
|
904
919
|
sessionLogger.log(`Subscribed to OpenCode events`);
|
|
905
920
|
// Load existing part-message mappings from database
|
|
@@ -951,7 +966,7 @@ async function handleOpencodeSession(prompt, thread, projectDirectory, originalM
|
|
|
951
966
|
// Outer-scoped interval for typing notifications. Only one at a time.
|
|
952
967
|
let typingInterval = null;
|
|
953
968
|
function startTyping(thread) {
|
|
954
|
-
if (
|
|
969
|
+
if (promptAbortController.signal.aborted) {
|
|
955
970
|
discordLogger.log(`Not starting typing, already aborted`);
|
|
956
971
|
return () => { };
|
|
957
972
|
}
|
|
@@ -973,8 +988,8 @@ async function handleOpencodeSession(prompt, thread, projectDirectory, originalM
|
|
|
973
988
|
});
|
|
974
989
|
}, 8000);
|
|
975
990
|
// Only add listener if not already aborted
|
|
976
|
-
if (!
|
|
977
|
-
|
|
991
|
+
if (!promptAbortController.signal.aborted) {
|
|
992
|
+
promptAbortController.signal.addEventListener('abort', () => {
|
|
978
993
|
if (typingInterval) {
|
|
979
994
|
clearInterval(typingInterval);
|
|
980
995
|
typingInterval = null;
|
|
@@ -1046,7 +1061,7 @@ async function handleOpencodeSession(prompt, thread, projectDirectory, originalM
|
|
|
1046
1061
|
}
|
|
1047
1062
|
// start typing in a moment, so that if the session finished, because step-finish is at the end of the message, we do not show typing status
|
|
1048
1063
|
setTimeout(() => {
|
|
1049
|
-
if (
|
|
1064
|
+
if (promptAbortController.signal.aborted)
|
|
1050
1065
|
return;
|
|
1051
1066
|
stopTyping = startTyping(thread);
|
|
1052
1067
|
}, 300);
|
|
@@ -1085,10 +1100,6 @@ async function handleOpencodeSession(prompt, thread, projectDirectory, originalM
|
|
|
1085
1100
|
}
|
|
1086
1101
|
}
|
|
1087
1102
|
catch (e) {
|
|
1088
|
-
if (isAbortError(e, abortController.signal)) {
|
|
1089
|
-
sessionLogger.log('AbortController aborted event handling (normal exit)');
|
|
1090
|
-
return;
|
|
1091
|
-
}
|
|
1092
1103
|
sessionLogger.error(`Unexpected error in event handling code`, e);
|
|
1093
1104
|
throw e;
|
|
1094
1105
|
}
|
|
@@ -1121,14 +1132,14 @@ async function handleOpencodeSession(prompt, thread, projectDirectory, originalM
|
|
|
1121
1132
|
sessionLogger.log(`Stopped typing for session`);
|
|
1122
1133
|
}
|
|
1123
1134
|
// Only send duration message if request was not aborted or was aborted with 'finished' reason
|
|
1124
|
-
if (!
|
|
1125
|
-
|
|
1135
|
+
if (!promptAbortController.signal.aborted ||
|
|
1136
|
+
promptAbortController.signal.reason === 'finished') {
|
|
1126
1137
|
const sessionDuration = prettyMilliseconds(Date.now() - sessionStartTime);
|
|
1127
1138
|
await sendThreadMessage(thread, `_Completed in ${sessionDuration}_`);
|
|
1128
1139
|
sessionLogger.log(`DURATION: Session completed in ${sessionDuration}`);
|
|
1129
1140
|
}
|
|
1130
1141
|
else {
|
|
1131
|
-
sessionLogger.log(`Session was aborted (reason: ${
|
|
1142
|
+
sessionLogger.log(`Session was aborted (reason: ${promptAbortController.signal.reason}), skipping duration message`);
|
|
1132
1143
|
}
|
|
1133
1144
|
}
|
|
1134
1145
|
};
|
|
@@ -1141,9 +1152,9 @@ async function handleOpencodeSession(prompt, thread, projectDirectory, originalM
|
|
|
1141
1152
|
body: {
|
|
1142
1153
|
parts: [{ type: 'text', text: prompt }],
|
|
1143
1154
|
},
|
|
1144
|
-
signal:
|
|
1155
|
+
signal: promptAbortController.signal,
|
|
1145
1156
|
});
|
|
1146
|
-
|
|
1157
|
+
promptAbortController.abort(new Error('finished'));
|
|
1147
1158
|
sessionLogger.log(`Successfully sent prompt, got response`);
|
|
1148
1159
|
abortControllers.delete(session.id);
|
|
1149
1160
|
// Update reaction to success
|
|
@@ -1161,8 +1172,8 @@ async function handleOpencodeSession(prompt, thread, projectDirectory, originalM
|
|
|
1161
1172
|
}
|
|
1162
1173
|
catch (error) {
|
|
1163
1174
|
sessionLogger.error(`ERROR: Failed to send prompt:`, error);
|
|
1164
|
-
if (!isAbortError(error,
|
|
1165
|
-
|
|
1175
|
+
if (!isAbortError(error, promptAbortController.signal)) {
|
|
1176
|
+
promptAbortController.abort(new Error('error'));
|
|
1166
1177
|
if (originalMessage) {
|
|
1167
1178
|
try {
|
|
1168
1179
|
await originalMessage.reactions.removeAll();
|
|
@@ -1174,12 +1185,14 @@ async function handleOpencodeSession(prompt, thread, projectDirectory, originalM
|
|
|
1174
1185
|
}
|
|
1175
1186
|
}
|
|
1176
1187
|
// Always log the error's constructor name (if any) and make error reporting more readable
|
|
1177
|
-
const errorName = error &&
|
|
1188
|
+
const errorName = error &&
|
|
1189
|
+
typeof error === 'object' &&
|
|
1190
|
+
'constructor' in error &&
|
|
1191
|
+
error.constructor &&
|
|
1192
|
+
typeof error.constructor.name === 'string'
|
|
1178
1193
|
? error.constructor.name
|
|
1179
1194
|
: typeof error;
|
|
1180
|
-
const errorMsg = error instanceof Error
|
|
1181
|
-
? (error.stack || error.message)
|
|
1182
|
-
: String(error);
|
|
1195
|
+
const errorMsg = error instanceof Error ? error.stack || error.message : String(error);
|
|
1183
1196
|
await sendThreadMessage(thread, `✗ Unexpected bot Error: [${errorName}]\n${errorMsg}`);
|
|
1184
1197
|
}
|
|
1185
1198
|
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,18 @@
|
|
|
2
2
|
"name": "kimaki",
|
|
3
3
|
"module": "index.ts",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.4.1",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "pnpm tsc && tsx --env-file .env src/cli.ts",
|
|
8
|
+
"prepublishOnly": "pnpm tsc",
|
|
9
|
+
"dev:bun": "DEBUG=1 bun --env-file .env src/cli.ts",
|
|
10
|
+
"test": "tsx scripts/test-opencode.ts",
|
|
11
|
+
"watch": "tsx scripts/watch-session.ts",
|
|
12
|
+
"test:events": "tsx test-events.ts",
|
|
13
|
+
"pcm-to-mp3": "bun scripts/pcm-to-mp3",
|
|
14
|
+
"test:send": "tsx send-test-message.ts",
|
|
15
|
+
"register-commands": "tsx scripts/register-commands.ts"
|
|
16
|
+
},
|
|
6
17
|
"repository": "https://github.com/remorses/kimaki",
|
|
7
18
|
"bin": "bin.js",
|
|
8
19
|
"files": [
|
|
@@ -43,15 +54,5 @@
|
|
|
43
54
|
"string-dedent": "^3.0.2",
|
|
44
55
|
"undici": "^7.16.0",
|
|
45
56
|
"zod": "^4.0.17"
|
|
46
|
-
},
|
|
47
|
-
"scripts": {
|
|
48
|
-
"dev": "pnpm tsc && tsx --env-file .env src/cli.ts",
|
|
49
|
-
"dev:bun": "DEBUG=1 bun --env-file .env src/cli.ts",
|
|
50
|
-
"test": "tsx scripts/test-opencode.ts",
|
|
51
|
-
"watch": "tsx scripts/watch-session.ts",
|
|
52
|
-
"test:events": "tsx test-events.ts",
|
|
53
|
-
"pcm-to-mp3": "bun scripts/pcm-to-mp3",
|
|
54
|
-
"test:send": "tsx send-test-message.ts",
|
|
55
|
-
"register-commands": "tsx scripts/register-commands.ts"
|
|
56
57
|
}
|
|
57
|
-
}
|
|
58
|
+
}
|
package/src/cli.ts
CHANGED
|
File without changes
|
package/src/discordBot.ts
CHANGED
|
@@ -945,6 +945,7 @@ export async function initializeOpencodeForDirectory(directory: string) {
|
|
|
945
945
|
}
|
|
946
946
|
}
|
|
947
947
|
|
|
948
|
+
|
|
948
949
|
function formatPart(part: Part): string {
|
|
949
950
|
switch (part.type) {
|
|
950
951
|
case 'text':
|
|
@@ -954,7 +955,6 @@ function formatPart(part: Part): string {
|
|
|
954
955
|
return `▪︎ thinking: ${escapeDiscordFormatting(part.text || '')}`
|
|
955
956
|
case 'tool':
|
|
956
957
|
if (part.state.status === 'completed' || part.state.status === 'error') {
|
|
957
|
-
let language = ''
|
|
958
958
|
let outputToDisplay = ''
|
|
959
959
|
let summaryText = ''
|
|
960
960
|
|
|
@@ -975,6 +975,15 @@ function formatPart(part: Part): string {
|
|
|
975
975
|
const content = (part.state.input?.content as string) || ''
|
|
976
976
|
const lines = content.split('\n').length
|
|
977
977
|
summaryText = `(${lines} line${lines === 1 ? '' : 's'})`
|
|
978
|
+
} else if (part.tool === 'read') {
|
|
979
|
+
} else if (part.tool === 'write') {
|
|
980
|
+
} else if (part.tool === 'edit') {
|
|
981
|
+
} else if (part.tool === 'list') {
|
|
982
|
+
} else if (part.tool === 'glob') {
|
|
983
|
+
} else if (part.tool === 'grep') {
|
|
984
|
+
} else if (part.tool === 'task') {
|
|
985
|
+
} else if (part.tool === 'todoread') {
|
|
986
|
+
// Special handling for read - don't show arguments
|
|
978
987
|
} else if (part.tool === 'todowrite') {
|
|
979
988
|
const todos =
|
|
980
989
|
(part.state.input?.todos as {
|
|
@@ -1010,13 +1019,17 @@ function formatPart(part: Part): string {
|
|
|
1010
1019
|
const inputFields = Object.entries(part.state.input)
|
|
1011
1020
|
.map(([key, value]) => {
|
|
1012
1021
|
if (value === null || value === undefined) return null
|
|
1013
|
-
const stringValue =
|
|
1014
|
-
|
|
1022
|
+
const stringValue =
|
|
1023
|
+
typeof value === 'string' ? value : JSON.stringify(value)
|
|
1024
|
+
const truncatedValue =
|
|
1025
|
+
stringValue.length > 100
|
|
1026
|
+
? stringValue.slice(0, 100) + '…'
|
|
1027
|
+
: stringValue
|
|
1015
1028
|
return `${key}: ${truncatedValue}`
|
|
1016
1029
|
})
|
|
1017
1030
|
.filter(Boolean)
|
|
1018
1031
|
if (inputFields.length > 0) {
|
|
1019
|
-
outputToDisplay = inputFields.join('
|
|
1032
|
+
outputToDisplay = inputFields.join(', ')
|
|
1020
1033
|
}
|
|
1021
1034
|
}
|
|
1022
1035
|
|
|
@@ -1156,19 +1169,16 @@ async function handleOpencodeSession(
|
|
|
1156
1169
|
voiceLogger.log(
|
|
1157
1170
|
`[ABORT] Cancelling existing request for session: ${session.id}`,
|
|
1158
1171
|
)
|
|
1159
|
-
existingController.abort('New request started')
|
|
1172
|
+
existingController.abort(new Error('New request started'))
|
|
1160
1173
|
}
|
|
1161
1174
|
|
|
1162
1175
|
if (abortControllers.has(session.id)) {
|
|
1163
|
-
abortControllers.get(session.id)?.abort('new reply')
|
|
1176
|
+
abortControllers.get(session.id)?.abort(new Error('new reply'))
|
|
1164
1177
|
}
|
|
1165
|
-
const
|
|
1166
|
-
|
|
1167
|
-
abortControllers.set(session.id, abortController)
|
|
1178
|
+
const promptAbortController = new AbortController()
|
|
1179
|
+
abortControllers.set(session.id, promptAbortController)
|
|
1168
1180
|
|
|
1169
|
-
const eventsResult = await getClient().event.subscribe({
|
|
1170
|
-
signal: abortController.signal,
|
|
1171
|
-
})
|
|
1181
|
+
const eventsResult = await getClient().event.subscribe({})
|
|
1172
1182
|
const events = eventsResult.stream
|
|
1173
1183
|
sessionLogger.log(`Subscribed to OpenCode events`)
|
|
1174
1184
|
|
|
@@ -1240,7 +1250,7 @@ async function handleOpencodeSession(
|
|
|
1240
1250
|
let typingInterval: NodeJS.Timeout | null = null
|
|
1241
1251
|
|
|
1242
1252
|
function startTyping(thread: ThreadChannel): () => void {
|
|
1243
|
-
if (
|
|
1253
|
+
if (promptAbortController.signal.aborted) {
|
|
1244
1254
|
discordLogger.log(`Not starting typing, already aborted`)
|
|
1245
1255
|
return () => {}
|
|
1246
1256
|
}
|
|
@@ -1266,8 +1276,8 @@ async function handleOpencodeSession(
|
|
|
1266
1276
|
}, 8000)
|
|
1267
1277
|
|
|
1268
1278
|
// Only add listener if not already aborted
|
|
1269
|
-
if (!
|
|
1270
|
-
|
|
1279
|
+
if (!promptAbortController.signal.aborted) {
|
|
1280
|
+
promptAbortController.signal.addEventListener(
|
|
1271
1281
|
'abort',
|
|
1272
1282
|
() => {
|
|
1273
1283
|
if (typingInterval) {
|
|
@@ -1365,7 +1375,7 @@ async function handleOpencodeSession(
|
|
|
1365
1375
|
}
|
|
1366
1376
|
// start typing in a moment, so that if the session finished, because step-finish is at the end of the message, we do not show typing status
|
|
1367
1377
|
setTimeout(() => {
|
|
1368
|
-
if (
|
|
1378
|
+
if (promptAbortController.signal.aborted) return
|
|
1369
1379
|
stopTyping = startTyping(thread)
|
|
1370
1380
|
}, 300)
|
|
1371
1381
|
}
|
|
@@ -1405,12 +1415,6 @@ async function handleOpencodeSession(
|
|
|
1405
1415
|
}
|
|
1406
1416
|
}
|
|
1407
1417
|
} catch (e) {
|
|
1408
|
-
if (isAbortError(e, abortController.signal)) {
|
|
1409
|
-
sessionLogger.log(
|
|
1410
|
-
'AbortController aborted event handling (normal exit)',
|
|
1411
|
-
)
|
|
1412
|
-
return
|
|
1413
|
-
}
|
|
1414
1418
|
sessionLogger.error(`Unexpected error in event handling code`, e)
|
|
1415
1419
|
throw e
|
|
1416
1420
|
} finally {
|
|
@@ -1450,8 +1454,8 @@ async function handleOpencodeSession(
|
|
|
1450
1454
|
|
|
1451
1455
|
// Only send duration message if request was not aborted or was aborted with 'finished' reason
|
|
1452
1456
|
if (
|
|
1453
|
-
!
|
|
1454
|
-
|
|
1457
|
+
!promptAbortController.signal.aborted ||
|
|
1458
|
+
promptAbortController.signal.reason === 'finished'
|
|
1455
1459
|
) {
|
|
1456
1460
|
const sessionDuration = prettyMilliseconds(
|
|
1457
1461
|
Date.now() - sessionStartTime,
|
|
@@ -1460,7 +1464,7 @@ async function handleOpencodeSession(
|
|
|
1460
1464
|
sessionLogger.log(`DURATION: Session completed in ${sessionDuration}`)
|
|
1461
1465
|
} else {
|
|
1462
1466
|
sessionLogger.log(
|
|
1463
|
-
`Session was aborted (reason: ${
|
|
1467
|
+
`Session was aborted (reason: ${promptAbortController.signal.reason}), skipping duration message`,
|
|
1464
1468
|
)
|
|
1465
1469
|
}
|
|
1466
1470
|
}
|
|
@@ -1479,9 +1483,9 @@ async function handleOpencodeSession(
|
|
|
1479
1483
|
body: {
|
|
1480
1484
|
parts: [{ type: 'text', text: prompt }],
|
|
1481
1485
|
},
|
|
1482
|
-
signal:
|
|
1486
|
+
signal: promptAbortController.signal,
|
|
1483
1487
|
})
|
|
1484
|
-
|
|
1488
|
+
promptAbortController.abort(new Error('finished'))
|
|
1485
1489
|
|
|
1486
1490
|
sessionLogger.log(`Successfully sent prompt, got response`)
|
|
1487
1491
|
|
|
@@ -1502,8 +1506,8 @@ async function handleOpencodeSession(
|
|
|
1502
1506
|
} catch (error) {
|
|
1503
1507
|
sessionLogger.error(`ERROR: Failed to send prompt:`, error)
|
|
1504
1508
|
|
|
1505
|
-
if (!isAbortError(error,
|
|
1506
|
-
|
|
1509
|
+
if (!isAbortError(error, promptAbortController.signal)) {
|
|
1510
|
+
promptAbortController.abort(new Error('error'))
|
|
1507
1511
|
|
|
1508
1512
|
if (originalMessage) {
|
|
1509
1513
|
try {
|
|
@@ -1515,12 +1519,16 @@ async function handleOpencodeSession(
|
|
|
1515
1519
|
}
|
|
1516
1520
|
}
|
|
1517
1521
|
// Always log the error's constructor name (if any) and make error reporting more readable
|
|
1518
|
-
const errorName =
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1522
|
+
const errorName =
|
|
1523
|
+
error &&
|
|
1524
|
+
typeof error === 'object' &&
|
|
1525
|
+
'constructor' in error &&
|
|
1526
|
+
error.constructor &&
|
|
1527
|
+
typeof error.constructor.name === 'string'
|
|
1528
|
+
? error.constructor.name
|
|
1529
|
+
: typeof error
|
|
1530
|
+
const errorMsg =
|
|
1531
|
+
error instanceof Error ? error.stack || error.message : String(error)
|
|
1524
1532
|
await sendThreadMessage(
|
|
1525
1533
|
thread,
|
|
1526
1534
|
`✗ Unexpected bot Error: [${errorName}]\n${errorMsg}`,
|