kimaki 0.4.20 → 0.4.21
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 +110 -80
- package/package.json +1 -1
- package/src/discordBot.ts +131 -91
package/dist/discordBot.js
CHANGED
|
@@ -43,6 +43,32 @@ The user cannot see bash tool outputs. If there is important information in bash
|
|
|
43
43
|
|
|
44
44
|
Your current OpenCode session ID is: ${sessionId}
|
|
45
45
|
|
|
46
|
+
## permissions
|
|
47
|
+
|
|
48
|
+
Only users with these Discord permissions can send messages to the bot:
|
|
49
|
+
- Server Owner
|
|
50
|
+
- Administrator permission
|
|
51
|
+
- Manage Server permission
|
|
52
|
+
- "Kimaki" role (case-insensitive)
|
|
53
|
+
|
|
54
|
+
## changing the model
|
|
55
|
+
|
|
56
|
+
To change the model used by OpenCode, edit the project's \`opencode.json\` config file and set the \`model\` field:
|
|
57
|
+
|
|
58
|
+
\`\`\`json
|
|
59
|
+
{
|
|
60
|
+
"model": "anthropic/claude-sonnet-4-20250514"
|
|
61
|
+
}
|
|
62
|
+
\`\`\`
|
|
63
|
+
|
|
64
|
+
Examples:
|
|
65
|
+
- \`"anthropic/claude-sonnet-4-20250514"\` - Claude Sonnet 4
|
|
66
|
+
- \`"anthropic/claude-opus-4-20250514"\` - Claude Opus 4
|
|
67
|
+
- \`"openai/gpt-4o"\` - GPT-4o
|
|
68
|
+
- \`"google/gemini-2.5-pro"\` - Gemini 2.5 Pro
|
|
69
|
+
|
|
70
|
+
Format is \`provider/model-name\`. You can also set \`small_model\` for tasks like title generation.
|
|
71
|
+
|
|
46
72
|
## uploading files to discord
|
|
47
73
|
|
|
48
74
|
To upload files to the Discord thread (images, screenshots, long files that would clutter the chat), run:
|
|
@@ -585,7 +611,6 @@ async function processVoiceAttachment({ message, thread, projectDirectory, isNew
|
|
|
585
611
|
if (!audioAttachment)
|
|
586
612
|
return null;
|
|
587
613
|
voiceLogger.log(`Detected audio attachment: ${audioAttachment.name} (${audioAttachment.contentType})`);
|
|
588
|
-
await message.react('⏳');
|
|
589
614
|
await sendThreadMessage(thread, '🎤 Transcribing voice message...');
|
|
590
615
|
const audioResponse = await fetch(audioAttachment.url);
|
|
591
616
|
const audioBuffer = Buffer.from(await audioResponse.arrayBuffer());
|
|
@@ -940,30 +965,45 @@ function getToolSummaryText(part) {
|
|
|
940
965
|
if (part.type !== 'tool')
|
|
941
966
|
return '';
|
|
942
967
|
if (part.tool === 'edit') {
|
|
968
|
+
const filePath = part.state.input?.filePath || '';
|
|
943
969
|
const newString = part.state.input?.newString || '';
|
|
944
970
|
const oldString = part.state.input?.oldString || '';
|
|
945
971
|
const added = newString.split('\n').length;
|
|
946
972
|
const removed = oldString.split('\n').length;
|
|
947
|
-
|
|
973
|
+
const fileName = filePath.split('/').pop() || '';
|
|
974
|
+
return fileName ? `*${fileName}* (+${added}-${removed})` : `(+${added}-${removed})`;
|
|
948
975
|
}
|
|
949
976
|
if (part.tool === 'write') {
|
|
977
|
+
const filePath = part.state.input?.filePath || '';
|
|
950
978
|
const content = part.state.input?.content || '';
|
|
951
979
|
const lines = content.split('\n').length;
|
|
952
|
-
|
|
980
|
+
const fileName = filePath.split('/').pop() || '';
|
|
981
|
+
return fileName ? `*${fileName}* (${lines} line${lines === 1 ? '' : 's'})` : `(${lines} line${lines === 1 ? '' : 's'})`;
|
|
953
982
|
}
|
|
954
983
|
if (part.tool === 'webfetch') {
|
|
955
984
|
const url = part.state.input?.url || '';
|
|
956
985
|
const urlWithoutProtocol = url.replace(/^https?:\/\//, '');
|
|
957
|
-
return urlWithoutProtocol ?
|
|
986
|
+
return urlWithoutProtocol ? `*${urlWithoutProtocol}*` : '';
|
|
987
|
+
}
|
|
988
|
+
if (part.tool === 'read') {
|
|
989
|
+
const filePath = part.state.input?.filePath || '';
|
|
990
|
+
const fileName = filePath.split('/').pop() || '';
|
|
991
|
+
return fileName ? `*${fileName}*` : '';
|
|
992
|
+
}
|
|
993
|
+
if (part.tool === 'list') {
|
|
994
|
+
const path = part.state.input?.path || '';
|
|
995
|
+
const dirName = path.split('/').pop() || path;
|
|
996
|
+
return dirName ? `*${dirName}*` : '';
|
|
958
997
|
}
|
|
959
|
-
if (part.tool === '
|
|
960
|
-
part.
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
part.
|
|
965
|
-
|
|
966
|
-
|
|
998
|
+
if (part.tool === 'glob') {
|
|
999
|
+
const pattern = part.state.input?.pattern || '';
|
|
1000
|
+
return pattern ? `*${pattern}*` : '';
|
|
1001
|
+
}
|
|
1002
|
+
if (part.tool === 'grep') {
|
|
1003
|
+
const pattern = part.state.input?.pattern || '';
|
|
1004
|
+
return pattern ? `*${pattern}*` : '';
|
|
1005
|
+
}
|
|
1006
|
+
if (part.tool === 'bash' || part.tool === 'task' || part.tool === 'todoread' || part.tool === 'todowrite') {
|
|
967
1007
|
return '';
|
|
968
1008
|
}
|
|
969
1009
|
if (!part.state.input)
|
|
@@ -1036,19 +1076,18 @@ function formatPart(part) {
|
|
|
1036
1076
|
}
|
|
1037
1077
|
else if (part.tool === 'bash') {
|
|
1038
1078
|
const command = part.state.input?.command || '';
|
|
1079
|
+
const description = part.state.input?.description || '';
|
|
1039
1080
|
const isSingleLine = !command.includes('\n');
|
|
1040
1081
|
const hasBackticks = command.includes('`');
|
|
1041
|
-
if (isSingleLine && command.length <=
|
|
1042
|
-
toolTitle =
|
|
1082
|
+
if (isSingleLine && !hasBackticks && command.length <= 50) {
|
|
1083
|
+
toolTitle = `\`${command}\``;
|
|
1043
1084
|
}
|
|
1044
|
-
else {
|
|
1045
|
-
toolTitle =
|
|
1085
|
+
else if (description) {
|
|
1086
|
+
toolTitle = `_${description}_`;
|
|
1087
|
+
}
|
|
1088
|
+
else if (stateTitle) {
|
|
1089
|
+
toolTitle = `_${stateTitle}_`;
|
|
1046
1090
|
}
|
|
1047
|
-
}
|
|
1048
|
-
else if (part.tool === 'edit' || part.tool === 'write') {
|
|
1049
|
-
const filePath = part.state.input?.filePath || '';
|
|
1050
|
-
const fileName = filePath.split('/').pop() || filePath;
|
|
1051
|
-
toolTitle = fileName ? `_${fileName}_` : '';
|
|
1052
1091
|
}
|
|
1053
1092
|
else if (stateTitle) {
|
|
1054
1093
|
toolTitle = `_${stateTitle}_`;
|
|
@@ -1160,6 +1199,39 @@ async function handleOpencodeSession({ prompt, thread, projectDirectory, origina
|
|
|
1160
1199
|
let usedModel;
|
|
1161
1200
|
let usedProviderID;
|
|
1162
1201
|
let tokensUsedInSession = 0;
|
|
1202
|
+
let typingInterval = null;
|
|
1203
|
+
function startTyping() {
|
|
1204
|
+
if (abortController.signal.aborted) {
|
|
1205
|
+
discordLogger.log(`Not starting typing, already aborted`);
|
|
1206
|
+
return () => { };
|
|
1207
|
+
}
|
|
1208
|
+
if (typingInterval) {
|
|
1209
|
+
clearInterval(typingInterval);
|
|
1210
|
+
typingInterval = null;
|
|
1211
|
+
}
|
|
1212
|
+
thread.sendTyping().catch((e) => {
|
|
1213
|
+
discordLogger.log(`Failed to send initial typing: ${e}`);
|
|
1214
|
+
});
|
|
1215
|
+
typingInterval = setInterval(() => {
|
|
1216
|
+
thread.sendTyping().catch((e) => {
|
|
1217
|
+
discordLogger.log(`Failed to send periodic typing: ${e}`);
|
|
1218
|
+
});
|
|
1219
|
+
}, 8000);
|
|
1220
|
+
if (!abortController.signal.aborted) {
|
|
1221
|
+
abortController.signal.addEventListener('abort', () => {
|
|
1222
|
+
if (typingInterval) {
|
|
1223
|
+
clearInterval(typingInterval);
|
|
1224
|
+
typingInterval = null;
|
|
1225
|
+
}
|
|
1226
|
+
}, { once: true });
|
|
1227
|
+
}
|
|
1228
|
+
return () => {
|
|
1229
|
+
if (typingInterval) {
|
|
1230
|
+
clearInterval(typingInterval);
|
|
1231
|
+
typingInterval = null;
|
|
1232
|
+
}
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1163
1235
|
const sendPartMessage = async (part) => {
|
|
1164
1236
|
const content = formatPart(part) + '\n\n';
|
|
1165
1237
|
if (!content.trim() || content.length === 0) {
|
|
@@ -1183,48 +1255,6 @@ async function handleOpencodeSession({ prompt, thread, projectDirectory, origina
|
|
|
1183
1255
|
}
|
|
1184
1256
|
};
|
|
1185
1257
|
const eventHandler = async () => {
|
|
1186
|
-
// Local typing function for this session
|
|
1187
|
-
// Outer-scoped interval for typing notifications. Only one at a time.
|
|
1188
|
-
let typingInterval = null;
|
|
1189
|
-
function startTyping(thread) {
|
|
1190
|
-
if (abortController.signal.aborted) {
|
|
1191
|
-
discordLogger.log(`Not starting typing, already aborted`);
|
|
1192
|
-
return () => { };
|
|
1193
|
-
}
|
|
1194
|
-
// Clear any previous typing interval
|
|
1195
|
-
if (typingInterval) {
|
|
1196
|
-
clearInterval(typingInterval);
|
|
1197
|
-
typingInterval = null;
|
|
1198
|
-
}
|
|
1199
|
-
// Send initial typing
|
|
1200
|
-
thread.sendTyping().catch((e) => {
|
|
1201
|
-
discordLogger.log(`Failed to send initial typing: ${e}`);
|
|
1202
|
-
});
|
|
1203
|
-
// Set up interval to send typing every 8 seconds
|
|
1204
|
-
typingInterval = setInterval(() => {
|
|
1205
|
-
thread.sendTyping().catch((e) => {
|
|
1206
|
-
discordLogger.log(`Failed to send periodic typing: ${e}`);
|
|
1207
|
-
});
|
|
1208
|
-
}, 8000);
|
|
1209
|
-
// Only add listener if not already aborted
|
|
1210
|
-
if (!abortController.signal.aborted) {
|
|
1211
|
-
abortController.signal.addEventListener('abort', () => {
|
|
1212
|
-
if (typingInterval) {
|
|
1213
|
-
clearInterval(typingInterval);
|
|
1214
|
-
typingInterval = null;
|
|
1215
|
-
}
|
|
1216
|
-
}, {
|
|
1217
|
-
once: true,
|
|
1218
|
-
});
|
|
1219
|
-
}
|
|
1220
|
-
// Return stop function
|
|
1221
|
-
return () => {
|
|
1222
|
-
if (typingInterval) {
|
|
1223
|
-
clearInterval(typingInterval);
|
|
1224
|
-
typingInterval = null;
|
|
1225
|
-
}
|
|
1226
|
-
};
|
|
1227
|
-
}
|
|
1228
1258
|
try {
|
|
1229
1259
|
let assistantMessageId;
|
|
1230
1260
|
for await (const event of events) {
|
|
@@ -1262,7 +1292,7 @@ async function handleOpencodeSession({ prompt, thread, projectDirectory, origina
|
|
|
1262
1292
|
}
|
|
1263
1293
|
// Start typing on step-start
|
|
1264
1294
|
if (part.type === 'step-start') {
|
|
1265
|
-
stopTyping = startTyping(
|
|
1295
|
+
stopTyping = startTyping();
|
|
1266
1296
|
}
|
|
1267
1297
|
// Send tool parts immediately when they start running
|
|
1268
1298
|
if (part.type === 'tool' && part.state.status === 'running') {
|
|
@@ -1285,7 +1315,7 @@ async function handleOpencodeSession({ prompt, thread, projectDirectory, origina
|
|
|
1285
1315
|
setTimeout(() => {
|
|
1286
1316
|
if (abortController.signal.aborted)
|
|
1287
1317
|
return;
|
|
1288
|
-
stopTyping = startTyping(
|
|
1318
|
+
stopTyping = startTyping();
|
|
1289
1319
|
}, 300);
|
|
1290
1320
|
}
|
|
1291
1321
|
}
|
|
@@ -1405,14 +1435,7 @@ async function handleOpencodeSession({ prompt, thread, projectDirectory, origina
|
|
|
1405
1435
|
sessionLogger.log(`[DEBOUNCE] Aborted before prompt, exiting`);
|
|
1406
1436
|
return;
|
|
1407
1437
|
}
|
|
1408
|
-
|
|
1409
|
-
try {
|
|
1410
|
-
await originalMessage.react('⏳');
|
|
1411
|
-
}
|
|
1412
|
-
catch (e) {
|
|
1413
|
-
discordLogger.log(`Could not add processing reaction:`, e);
|
|
1414
|
-
}
|
|
1415
|
-
}
|
|
1438
|
+
stopTyping = startTyping();
|
|
1416
1439
|
let response;
|
|
1417
1440
|
if (parsedCommand?.isCommand) {
|
|
1418
1441
|
sessionLogger.log(`[COMMAND] Sending command /${parsedCommand.command} to session ${session.id} with args: "${parsedCommand.arguments.slice(0, 100)}${parsedCommand.arguments.length > 100 ? '...' : ''}"`);
|
|
@@ -1579,11 +1602,13 @@ export async function startDiscordBot({ token, appId, discordClient, }) {
|
|
|
1579
1602
|
return;
|
|
1580
1603
|
}
|
|
1581
1604
|
}
|
|
1582
|
-
// Check if user is authoritative (server owner or has
|
|
1605
|
+
// Check if user is authoritative (server owner, admin, manage server, or has Kimaki role)
|
|
1583
1606
|
if (message.guild && message.member) {
|
|
1584
1607
|
const isOwner = message.member.id === message.guild.ownerId;
|
|
1585
1608
|
const isAdmin = message.member.permissions.has(PermissionsBitField.Flags.Administrator);
|
|
1586
|
-
|
|
1609
|
+
const canManageServer = message.member.permissions.has(PermissionsBitField.Flags.ManageGuild);
|
|
1610
|
+
const hasKimakiRole = message.member.roles.cache.some((role) => role.name.toLowerCase() === 'kimaki');
|
|
1611
|
+
if (!isOwner && !isAdmin && !canManageServer && !hasKimakiRole) {
|
|
1587
1612
|
return;
|
|
1588
1613
|
}
|
|
1589
1614
|
}
|
|
@@ -2496,12 +2521,13 @@ export async function startDiscordBot({ token, appId, discordClient, }) {
|
|
|
2496
2521
|
const member = newState.member || oldState.member;
|
|
2497
2522
|
if (!member)
|
|
2498
2523
|
return;
|
|
2499
|
-
// Check if user is admin
|
|
2524
|
+
// Check if user is admin, server owner, can manage server, or has Kimaki role
|
|
2500
2525
|
const guild = newState.guild || oldState.guild;
|
|
2501
2526
|
const isOwner = member.id === guild.ownerId;
|
|
2502
2527
|
const isAdmin = member.permissions.has(PermissionsBitField.Flags.Administrator);
|
|
2503
|
-
|
|
2504
|
-
|
|
2528
|
+
const canManageServer = member.permissions.has(PermissionsBitField.Flags.ManageGuild);
|
|
2529
|
+
const hasKimakiRole = member.roles.cache.some((role) => role.name.toLowerCase() === 'kimaki');
|
|
2530
|
+
if (!isOwner && !isAdmin && !canManageServer && !hasKimakiRole) {
|
|
2505
2531
|
return;
|
|
2506
2532
|
}
|
|
2507
2533
|
// Handle admin leaving voice channel
|
|
@@ -2520,7 +2546,9 @@ export async function startDiscordBot({ token, appId, discordClient, }) {
|
|
|
2520
2546
|
if (m.id === member.id || m.user.bot)
|
|
2521
2547
|
return false;
|
|
2522
2548
|
return (m.id === guild.ownerId ||
|
|
2523
|
-
m.permissions.has(PermissionsBitField.Flags.Administrator)
|
|
2549
|
+
m.permissions.has(PermissionsBitField.Flags.Administrator) ||
|
|
2550
|
+
m.permissions.has(PermissionsBitField.Flags.ManageGuild) ||
|
|
2551
|
+
m.roles.cache.some((role) => role.name.toLowerCase() === 'kimaki'));
|
|
2524
2552
|
});
|
|
2525
2553
|
if (!hasOtherAdmins) {
|
|
2526
2554
|
voiceLogger.log(`No other admins in channel, bot leaving voice channel in guild: ${guild.name}`);
|
|
@@ -2550,7 +2578,9 @@ export async function startDiscordBot({ token, appId, discordClient, }) {
|
|
|
2550
2578
|
if (m.id === member.id || m.user.bot)
|
|
2551
2579
|
return false;
|
|
2552
2580
|
return (m.id === guild.ownerId ||
|
|
2553
|
-
m.permissions.has(PermissionsBitField.Flags.Administrator)
|
|
2581
|
+
m.permissions.has(PermissionsBitField.Flags.Administrator) ||
|
|
2582
|
+
m.permissions.has(PermissionsBitField.Flags.ManageGuild) ||
|
|
2583
|
+
m.roles.cache.some((role) => role.name.toLowerCase() === 'kimaki'));
|
|
2554
2584
|
});
|
|
2555
2585
|
if (!hasOtherAdmins) {
|
|
2556
2586
|
voiceLogger.log(`Following admin to new channel: ${newState.channel?.name}`);
|
package/package.json
CHANGED
package/src/discordBot.ts
CHANGED
|
@@ -61,7 +61,6 @@ type ParsedCommand = {
|
|
|
61
61
|
} | {
|
|
62
62
|
isCommand: false
|
|
63
63
|
}
|
|
64
|
-
|
|
65
64
|
function parseSlashCommand(text: string): ParsedCommand {
|
|
66
65
|
const trimmed = text.trim()
|
|
67
66
|
if (!trimmed.startsWith('/')) {
|
|
@@ -84,6 +83,32 @@ The user cannot see bash tool outputs. If there is important information in bash
|
|
|
84
83
|
|
|
85
84
|
Your current OpenCode session ID is: ${sessionId}
|
|
86
85
|
|
|
86
|
+
## permissions
|
|
87
|
+
|
|
88
|
+
Only users with these Discord permissions can send messages to the bot:
|
|
89
|
+
- Server Owner
|
|
90
|
+
- Administrator permission
|
|
91
|
+
- Manage Server permission
|
|
92
|
+
- "Kimaki" role (case-insensitive)
|
|
93
|
+
|
|
94
|
+
## changing the model
|
|
95
|
+
|
|
96
|
+
To change the model used by OpenCode, edit the project's \`opencode.json\` config file and set the \`model\` field:
|
|
97
|
+
|
|
98
|
+
\`\`\`json
|
|
99
|
+
{
|
|
100
|
+
"model": "anthropic/claude-sonnet-4-20250514"
|
|
101
|
+
}
|
|
102
|
+
\`\`\`
|
|
103
|
+
|
|
104
|
+
Examples:
|
|
105
|
+
- \`"anthropic/claude-sonnet-4-20250514"\` - Claude Sonnet 4
|
|
106
|
+
- \`"anthropic/claude-opus-4-20250514"\` - Claude Opus 4
|
|
107
|
+
- \`"openai/gpt-4o"\` - GPT-4o
|
|
108
|
+
- \`"google/gemini-2.5-pro"\` - Gemini 2.5 Pro
|
|
109
|
+
|
|
110
|
+
Format is \`provider/model-name\`. You can also set \`small_model\` for tasks like title generation.
|
|
111
|
+
|
|
87
112
|
## uploading files to discord
|
|
88
113
|
|
|
89
114
|
To upload files to the Discord thread (images, screenshots, long files that would clutter the chat), run:
|
|
@@ -805,7 +830,6 @@ async function processVoiceAttachment({
|
|
|
805
830
|
`Detected audio attachment: ${audioAttachment.name} (${audioAttachment.contentType})`,
|
|
806
831
|
)
|
|
807
832
|
|
|
808
|
-
await message.react('⏳')
|
|
809
833
|
await sendThreadMessage(thread, '🎤 Transcribing voice message...')
|
|
810
834
|
|
|
811
835
|
const audioResponse = await fetch(audioAttachment.url)
|
|
@@ -1259,35 +1283,52 @@ function getToolSummaryText(part: Part): string {
|
|
|
1259
1283
|
if (part.type !== 'tool') return ''
|
|
1260
1284
|
|
|
1261
1285
|
if (part.tool === 'edit') {
|
|
1286
|
+
const filePath = (part.state.input?.filePath as string) || ''
|
|
1262
1287
|
const newString = (part.state.input?.newString as string) || ''
|
|
1263
1288
|
const oldString = (part.state.input?.oldString as string) || ''
|
|
1264
1289
|
const added = newString.split('\n').length
|
|
1265
1290
|
const removed = oldString.split('\n').length
|
|
1266
|
-
|
|
1291
|
+
const fileName = filePath.split('/').pop() || ''
|
|
1292
|
+
return fileName ? `*${fileName}* (+${added}-${removed})` : `(+${added}-${removed})`
|
|
1267
1293
|
}
|
|
1268
1294
|
|
|
1269
1295
|
if (part.tool === 'write') {
|
|
1296
|
+
const filePath = (part.state.input?.filePath as string) || ''
|
|
1270
1297
|
const content = (part.state.input?.content as string) || ''
|
|
1271
1298
|
const lines = content.split('\n').length
|
|
1272
|
-
|
|
1299
|
+
const fileName = filePath.split('/').pop() || ''
|
|
1300
|
+
return fileName ? `*${fileName}* (${lines} line${lines === 1 ? '' : 's'})` : `(${lines} line${lines === 1 ? '' : 's'})`
|
|
1273
1301
|
}
|
|
1274
1302
|
|
|
1275
1303
|
if (part.tool === 'webfetch') {
|
|
1276
1304
|
const url = (part.state.input?.url as string) || ''
|
|
1277
1305
|
const urlWithoutProtocol = url.replace(/^https?:\/\//, '')
|
|
1278
|
-
return urlWithoutProtocol ?
|
|
1306
|
+
return urlWithoutProtocol ? `*${urlWithoutProtocol}*` : ''
|
|
1279
1307
|
}
|
|
1280
1308
|
|
|
1281
|
-
if (
|
|
1282
|
-
part.
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
part.
|
|
1289
|
-
|
|
1290
|
-
|
|
1309
|
+
if (part.tool === 'read') {
|
|
1310
|
+
const filePath = (part.state.input?.filePath as string) || ''
|
|
1311
|
+
const fileName = filePath.split('/').pop() || ''
|
|
1312
|
+
return fileName ? `*${fileName}*` : ''
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
if (part.tool === 'list') {
|
|
1316
|
+
const path = (part.state.input?.path as string) || ''
|
|
1317
|
+
const dirName = path.split('/').pop() || path
|
|
1318
|
+
return dirName ? `*${dirName}*` : ''
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
if (part.tool === 'glob') {
|
|
1322
|
+
const pattern = (part.state.input?.pattern as string) || ''
|
|
1323
|
+
return pattern ? `*${pattern}*` : ''
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
if (part.tool === 'grep') {
|
|
1327
|
+
const pattern = (part.state.input?.pattern as string) || ''
|
|
1328
|
+
return pattern ? `*${pattern}*` : ''
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
if (part.tool === 'bash' || part.tool === 'task' || part.tool === 'todoread' || part.tool === 'todowrite') {
|
|
1291
1332
|
return ''
|
|
1292
1333
|
}
|
|
1293
1334
|
|
|
@@ -1372,17 +1413,16 @@ function formatPart(part: Part): string {
|
|
|
1372
1413
|
toolTitle = part.state.error || 'error'
|
|
1373
1414
|
} else if (part.tool === 'bash') {
|
|
1374
1415
|
const command = (part.state.input?.command as string) || ''
|
|
1416
|
+
const description = (part.state.input?.description as string) || ''
|
|
1375
1417
|
const isSingleLine = !command.includes('\n')
|
|
1376
1418
|
const hasBackticks = command.includes('`')
|
|
1377
|
-
if (isSingleLine && command.length <=
|
|
1378
|
-
toolTitle =
|
|
1379
|
-
} else {
|
|
1380
|
-
toolTitle =
|
|
1419
|
+
if (isSingleLine && !hasBackticks && command.length <= 50) {
|
|
1420
|
+
toolTitle = `\`${command}\``
|
|
1421
|
+
} else if (description) {
|
|
1422
|
+
toolTitle = `_${description}_`
|
|
1423
|
+
} else if (stateTitle) {
|
|
1424
|
+
toolTitle = `_${stateTitle}_`
|
|
1381
1425
|
}
|
|
1382
|
-
} else if (part.tool === 'edit' || part.tool === 'write') {
|
|
1383
|
-
const filePath = (part.state.input?.filePath as string) || ''
|
|
1384
|
-
const fileName = filePath.split('/').pop() || filePath
|
|
1385
|
-
toolTitle = fileName ? `_${fileName}_` : ''
|
|
1386
1426
|
} else if (stateTitle) {
|
|
1387
1427
|
toolTitle = `_${stateTitle}_`
|
|
1388
1428
|
}
|
|
@@ -1542,6 +1582,49 @@ async function handleOpencodeSession({
|
|
|
1542
1582
|
let usedProviderID: string | undefined
|
|
1543
1583
|
let tokensUsedInSession = 0
|
|
1544
1584
|
|
|
1585
|
+
let typingInterval: NodeJS.Timeout | null = null
|
|
1586
|
+
|
|
1587
|
+
function startTyping(): () => void {
|
|
1588
|
+
if (abortController.signal.aborted) {
|
|
1589
|
+
discordLogger.log(`Not starting typing, already aborted`)
|
|
1590
|
+
return () => {}
|
|
1591
|
+
}
|
|
1592
|
+
if (typingInterval) {
|
|
1593
|
+
clearInterval(typingInterval)
|
|
1594
|
+
typingInterval = null
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
thread.sendTyping().catch((e) => {
|
|
1598
|
+
discordLogger.log(`Failed to send initial typing: ${e}`)
|
|
1599
|
+
})
|
|
1600
|
+
|
|
1601
|
+
typingInterval = setInterval(() => {
|
|
1602
|
+
thread.sendTyping().catch((e) => {
|
|
1603
|
+
discordLogger.log(`Failed to send periodic typing: ${e}`)
|
|
1604
|
+
})
|
|
1605
|
+
}, 8000)
|
|
1606
|
+
|
|
1607
|
+
if (!abortController.signal.aborted) {
|
|
1608
|
+
abortController.signal.addEventListener(
|
|
1609
|
+
'abort',
|
|
1610
|
+
() => {
|
|
1611
|
+
if (typingInterval) {
|
|
1612
|
+
clearInterval(typingInterval)
|
|
1613
|
+
typingInterval = null
|
|
1614
|
+
}
|
|
1615
|
+
},
|
|
1616
|
+
{ once: true },
|
|
1617
|
+
)
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
return () => {
|
|
1621
|
+
if (typingInterval) {
|
|
1622
|
+
clearInterval(typingInterval)
|
|
1623
|
+
typingInterval = null
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1545
1628
|
const sendPartMessage = async (part: Part) => {
|
|
1546
1629
|
const content = formatPart(part) + '\n\n'
|
|
1547
1630
|
if (!content.trim() || content.length === 0) {
|
|
@@ -1570,58 +1653,6 @@ async function handleOpencodeSession({
|
|
|
1570
1653
|
}
|
|
1571
1654
|
|
|
1572
1655
|
const eventHandler = async () => {
|
|
1573
|
-
// Local typing function for this session
|
|
1574
|
-
// Outer-scoped interval for typing notifications. Only one at a time.
|
|
1575
|
-
let typingInterval: NodeJS.Timeout | null = null
|
|
1576
|
-
|
|
1577
|
-
function startTyping(thread: ThreadChannel): () => void {
|
|
1578
|
-
if (abortController.signal.aborted) {
|
|
1579
|
-
discordLogger.log(`Not starting typing, already aborted`)
|
|
1580
|
-
return () => {}
|
|
1581
|
-
}
|
|
1582
|
-
// Clear any previous typing interval
|
|
1583
|
-
if (typingInterval) {
|
|
1584
|
-
clearInterval(typingInterval)
|
|
1585
|
-
typingInterval = null
|
|
1586
|
-
}
|
|
1587
|
-
|
|
1588
|
-
// Send initial typing
|
|
1589
|
-
thread.sendTyping().catch((e) => {
|
|
1590
|
-
discordLogger.log(`Failed to send initial typing: ${e}`)
|
|
1591
|
-
})
|
|
1592
|
-
|
|
1593
|
-
// Set up interval to send typing every 8 seconds
|
|
1594
|
-
typingInterval = setInterval(() => {
|
|
1595
|
-
thread.sendTyping().catch((e) => {
|
|
1596
|
-
discordLogger.log(`Failed to send periodic typing: ${e}`)
|
|
1597
|
-
})
|
|
1598
|
-
}, 8000)
|
|
1599
|
-
|
|
1600
|
-
// Only add listener if not already aborted
|
|
1601
|
-
if (!abortController.signal.aborted) {
|
|
1602
|
-
abortController.signal.addEventListener(
|
|
1603
|
-
'abort',
|
|
1604
|
-
() => {
|
|
1605
|
-
if (typingInterval) {
|
|
1606
|
-
clearInterval(typingInterval)
|
|
1607
|
-
typingInterval = null
|
|
1608
|
-
}
|
|
1609
|
-
},
|
|
1610
|
-
{
|
|
1611
|
-
once: true,
|
|
1612
|
-
},
|
|
1613
|
-
)
|
|
1614
|
-
}
|
|
1615
|
-
|
|
1616
|
-
// Return stop function
|
|
1617
|
-
return () => {
|
|
1618
|
-
if (typingInterval) {
|
|
1619
|
-
clearInterval(typingInterval)
|
|
1620
|
-
typingInterval = null
|
|
1621
|
-
}
|
|
1622
|
-
}
|
|
1623
|
-
}
|
|
1624
|
-
|
|
1625
1656
|
try {
|
|
1626
1657
|
let assistantMessageId: string | undefined
|
|
1627
1658
|
|
|
@@ -1672,7 +1703,7 @@ async function handleOpencodeSession({
|
|
|
1672
1703
|
|
|
1673
1704
|
// Start typing on step-start
|
|
1674
1705
|
if (part.type === 'step-start') {
|
|
1675
|
-
stopTyping = startTyping(
|
|
1706
|
+
stopTyping = startTyping()
|
|
1676
1707
|
}
|
|
1677
1708
|
|
|
1678
1709
|
// Send tool parts immediately when they start running
|
|
@@ -1698,7 +1729,7 @@ async function handleOpencodeSession({
|
|
|
1698
1729
|
// 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
|
|
1699
1730
|
setTimeout(() => {
|
|
1700
1731
|
if (abortController.signal.aborted) return
|
|
1701
|
-
stopTyping = startTyping(
|
|
1732
|
+
stopTyping = startTyping()
|
|
1702
1733
|
}, 300)
|
|
1703
1734
|
}
|
|
1704
1735
|
} else if (event.type === 'session.error') {
|
|
@@ -1847,13 +1878,7 @@ async function handleOpencodeSession({
|
|
|
1847
1878
|
return
|
|
1848
1879
|
}
|
|
1849
1880
|
|
|
1850
|
-
|
|
1851
|
-
try {
|
|
1852
|
-
await originalMessage.react('⏳')
|
|
1853
|
-
} catch (e) {
|
|
1854
|
-
discordLogger.log(`Could not add processing reaction:`, e)
|
|
1855
|
-
}
|
|
1856
|
-
}
|
|
1881
|
+
stopTyping = startTyping()
|
|
1857
1882
|
|
|
1858
1883
|
let response: { data?: unknown; error?: unknown; response: Response }
|
|
1859
1884
|
if (parsedCommand?.isCommand) {
|
|
@@ -2074,14 +2099,20 @@ export async function startDiscordBot({
|
|
|
2074
2099
|
}
|
|
2075
2100
|
}
|
|
2076
2101
|
|
|
2077
|
-
// Check if user is authoritative (server owner or has
|
|
2102
|
+
// Check if user is authoritative (server owner, admin, manage server, or has Kimaki role)
|
|
2078
2103
|
if (message.guild && message.member) {
|
|
2079
2104
|
const isOwner = message.member.id === message.guild.ownerId
|
|
2080
2105
|
const isAdmin = message.member.permissions.has(
|
|
2081
2106
|
PermissionsBitField.Flags.Administrator,
|
|
2082
2107
|
)
|
|
2108
|
+
const canManageServer = message.member.permissions.has(
|
|
2109
|
+
PermissionsBitField.Flags.ManageGuild,
|
|
2110
|
+
)
|
|
2111
|
+
const hasKimakiRole = message.member.roles.cache.some(
|
|
2112
|
+
(role) => role.name.toLowerCase() === 'kimaki',
|
|
2113
|
+
)
|
|
2083
2114
|
|
|
2084
|
-
if (!isOwner && !isAdmin) {
|
|
2115
|
+
if (!isOwner && !isAdmin && !canManageServer && !hasKimakiRole) {
|
|
2085
2116
|
return
|
|
2086
2117
|
}
|
|
2087
2118
|
}
|
|
@@ -3270,15 +3301,20 @@ export async function startDiscordBot({
|
|
|
3270
3301
|
const member = newState.member || oldState.member
|
|
3271
3302
|
if (!member) return
|
|
3272
3303
|
|
|
3273
|
-
// Check if user is admin
|
|
3304
|
+
// Check if user is admin, server owner, can manage server, or has Kimaki role
|
|
3274
3305
|
const guild = newState.guild || oldState.guild
|
|
3275
3306
|
const isOwner = member.id === guild.ownerId
|
|
3276
3307
|
const isAdmin = member.permissions.has(
|
|
3277
3308
|
PermissionsBitField.Flags.Administrator,
|
|
3278
3309
|
)
|
|
3310
|
+
const canManageServer = member.permissions.has(
|
|
3311
|
+
PermissionsBitField.Flags.ManageGuild,
|
|
3312
|
+
)
|
|
3313
|
+
const hasKimakiRole = member.roles.cache.some(
|
|
3314
|
+
(role) => role.name.toLowerCase() === 'kimaki',
|
|
3315
|
+
)
|
|
3279
3316
|
|
|
3280
|
-
if (!isOwner && !isAdmin) {
|
|
3281
|
-
// Not an admin user, ignore
|
|
3317
|
+
if (!isOwner && !isAdmin && !canManageServer && !hasKimakiRole) {
|
|
3282
3318
|
return
|
|
3283
3319
|
}
|
|
3284
3320
|
|
|
@@ -3304,7 +3340,9 @@ export async function startDiscordBot({
|
|
|
3304
3340
|
if (m.id === member.id || m.user.bot) return false
|
|
3305
3341
|
return (
|
|
3306
3342
|
m.id === guild.ownerId ||
|
|
3307
|
-
m.permissions.has(PermissionsBitField.Flags.Administrator)
|
|
3343
|
+
m.permissions.has(PermissionsBitField.Flags.Administrator) ||
|
|
3344
|
+
m.permissions.has(PermissionsBitField.Flags.ManageGuild) ||
|
|
3345
|
+
m.roles.cache.some((role) => role.name.toLowerCase() === 'kimaki')
|
|
3308
3346
|
)
|
|
3309
3347
|
})
|
|
3310
3348
|
|
|
@@ -3349,7 +3387,9 @@ export async function startDiscordBot({
|
|
|
3349
3387
|
if (m.id === member.id || m.user.bot) return false
|
|
3350
3388
|
return (
|
|
3351
3389
|
m.id === guild.ownerId ||
|
|
3352
|
-
m.permissions.has(PermissionsBitField.Flags.Administrator)
|
|
3390
|
+
m.permissions.has(PermissionsBitField.Flags.Administrator) ||
|
|
3391
|
+
m.permissions.has(PermissionsBitField.Flags.ManageGuild) ||
|
|
3392
|
+
m.roles.cache.some((role) => role.name.toLowerCase() === 'kimaki')
|
|
3353
3393
|
)
|
|
3354
3394
|
})
|
|
3355
3395
|
|