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.
@@ -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 ? stringValue.slice(0, 100) + '…' : stringValue;
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('\n');
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 abortController = new AbortController();
898
- // Store this controller for this session
899
- abortControllers.set(session.id, abortController);
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 (abortController.signal.aborted) {
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 (!abortController.signal.aborted) {
977
- abortController.signal.addEventListener('abort', () => {
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 (abortController.signal.aborted)
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 (!abortController.signal.aborted ||
1125
- abortController.signal.reason === 'finished') {
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: ${abortController.signal.reason}), skipping duration message`);
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: abortController.signal,
1155
+ signal: promptAbortController.signal,
1145
1156
  });
1146
- abortController.abort('finished');
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, abortController.signal)) {
1165
- abortController.abort('error');
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 && typeof error === 'object' && 'constructor' in error && error.constructor && typeof error.constructor.name === 'string'
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.3.2",
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 = typeof value === 'string' ? value : JSON.stringify(value)
1014
- const truncatedValue = stringValue.length > 100 ? stringValue.slice(0, 100) + '…' : stringValue
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('\n')
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 abortController = new AbortController()
1166
- // Store this controller for this session
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 (abortController.signal.aborted) {
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 (!abortController.signal.aborted) {
1270
- abortController.signal.addEventListener(
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 (abortController.signal.aborted) return
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
- !abortController.signal.aborted ||
1454
- abortController.signal.reason === 'finished'
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: ${abortController.signal.reason}), skipping duration message`,
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: abortController.signal,
1486
+ signal: promptAbortController.signal,
1483
1487
  })
1484
- abortController.abort('finished')
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, abortController.signal)) {
1506
- abortController.abort('error')
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 = 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)
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}`,