kimaki 0.4.26 → 0.4.28
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/cli.js +0 -12
- package/dist/commands/ask-question.js +2 -1
- package/dist/commands/permissions.js +105 -109
- package/dist/interaction-handler.js +5 -8
- package/dist/session-handler.js +20 -13
- package/package.json +1 -1
- package/src/cli.ts +0 -12
- package/src/commands/ask-question.ts +2 -1
- package/src/commands/permissions.ts +143 -118
- package/src/interaction-handler.ts +6 -10
- package/src/session-handler.ts +20 -17
package/dist/cli.js
CHANGED
|
@@ -156,18 +156,6 @@ async function registerCommands(token, appId, userCommands = []) {
|
|
|
156
156
|
return option;
|
|
157
157
|
})
|
|
158
158
|
.toJSON(),
|
|
159
|
-
new SlashCommandBuilder()
|
|
160
|
-
.setName('accept')
|
|
161
|
-
.setDescription('Accept a pending permission request (this request only)')
|
|
162
|
-
.toJSON(),
|
|
163
|
-
new SlashCommandBuilder()
|
|
164
|
-
.setName('accept-always')
|
|
165
|
-
.setDescription('Accept and auto-approve future requests matching this pattern')
|
|
166
|
-
.toJSON(),
|
|
167
|
-
new SlashCommandBuilder()
|
|
168
|
-
.setName('reject')
|
|
169
|
-
.setDescription('Reject a pending permission request')
|
|
170
|
-
.toJSON(),
|
|
171
159
|
new SlashCommandBuilder()
|
|
172
160
|
.setName('abort')
|
|
173
161
|
.setDescription('Abort the current OpenCode request in this thread')
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// for each question and collects user responses.
|
|
4
4
|
import { StringSelectMenuBuilder, StringSelectMenuInteraction, ActionRowBuilder, } from 'discord.js';
|
|
5
5
|
import crypto from 'node:crypto';
|
|
6
|
-
import { sendThreadMessage } from '../discord-utils.js';
|
|
6
|
+
import { sendThreadMessage, NOTIFY_MESSAGE_FLAGS } from '../discord-utils.js';
|
|
7
7
|
import { getOpencodeServerPort } from '../opencode.js';
|
|
8
8
|
import { createLogger } from '../logger.js';
|
|
9
9
|
const logger = createLogger('ASK_QUESTION');
|
|
@@ -57,6 +57,7 @@ export async function showAskUserQuestionDropdowns({ thread, sessionId, director
|
|
|
57
57
|
await thread.send({
|
|
58
58
|
content: `**${q.header}**\n${q.question}`,
|
|
59
59
|
components: [actionRow],
|
|
60
|
+
flags: NOTIFY_MESSAGE_FLAGS,
|
|
60
61
|
});
|
|
61
62
|
}
|
|
62
63
|
logger.log(`Showed ${input.questions.length} question dropdown(s) for session ${sessionId}`);
|
|
@@ -1,126 +1,122 @@
|
|
|
1
|
-
// Permission
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
1
|
+
// Permission dropdown handler - Shows dropdown for permission requests.
|
|
2
|
+
// When OpenCode asks for permission, this module renders a dropdown
|
|
3
|
+
// with Accept, Accept Always, and Deny options.
|
|
4
|
+
import { StringSelectMenuBuilder, StringSelectMenuInteraction, ActionRowBuilder, } from 'discord.js';
|
|
5
|
+
import crypto from 'node:crypto';
|
|
6
|
+
import { getOpencodeClientV2 } from '../opencode.js';
|
|
7
|
+
import { NOTIFY_MESSAGE_FLAGS } from '../discord-utils.js';
|
|
6
8
|
import { createLogger } from '../logger.js';
|
|
7
9
|
const logger = createLogger('PERMISSIONS');
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
},
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
await command.reply({ content: msg, flags: SILENT_MESSAGE_FLAGS });
|
|
57
|
-
logger.log(`Permission ${pending.permission.id} accepted with scope: ${scope}`);
|
|
58
|
-
}
|
|
59
|
-
catch (error) {
|
|
60
|
-
logger.error('[ACCEPT] Error:', error);
|
|
61
|
-
await command.reply({
|
|
62
|
-
content: `Failed to accept permission: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
63
|
-
ephemeral: true,
|
|
64
|
-
flags: SILENT_MESSAGE_FLAGS,
|
|
65
|
-
});
|
|
66
|
-
}
|
|
10
|
+
// Store pending permission contexts by hash
|
|
11
|
+
export const pendingPermissionContexts = new Map();
|
|
12
|
+
/**
|
|
13
|
+
* Show permission dropdown for a permission request.
|
|
14
|
+
* Returns the message ID and context hash for tracking.
|
|
15
|
+
*/
|
|
16
|
+
export async function showPermissionDropdown({ thread, permission, directory, }) {
|
|
17
|
+
const contextHash = crypto.randomBytes(8).toString('hex');
|
|
18
|
+
const context = {
|
|
19
|
+
permission,
|
|
20
|
+
directory,
|
|
21
|
+
thread,
|
|
22
|
+
contextHash,
|
|
23
|
+
};
|
|
24
|
+
pendingPermissionContexts.set(contextHash, context);
|
|
25
|
+
const patternStr = permission.patterns.join(', ');
|
|
26
|
+
// Build dropdown options
|
|
27
|
+
const options = [
|
|
28
|
+
{
|
|
29
|
+
label: 'Accept',
|
|
30
|
+
value: 'once',
|
|
31
|
+
description: 'Allow this request only',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
label: 'Accept Always',
|
|
35
|
+
value: 'always',
|
|
36
|
+
description: 'Auto-approve similar requests',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
label: 'Deny',
|
|
40
|
+
value: 'reject',
|
|
41
|
+
description: 'Reject this permission request',
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
const selectMenu = new StringSelectMenuBuilder()
|
|
45
|
+
.setCustomId(`permission:${contextHash}`)
|
|
46
|
+
.setPlaceholder('Choose an action')
|
|
47
|
+
.addOptions(options);
|
|
48
|
+
const actionRow = new ActionRowBuilder().addComponents(selectMenu);
|
|
49
|
+
const permissionMessage = await thread.send({
|
|
50
|
+
content: `⚠️ **Permission Required**\n\n` +
|
|
51
|
+
`**Type:** \`${permission.permission}\`\n` +
|
|
52
|
+
(patternStr ? `**Pattern:** \`${patternStr}\`` : ''),
|
|
53
|
+
components: [actionRow],
|
|
54
|
+
flags: NOTIFY_MESSAGE_FLAGS,
|
|
55
|
+
});
|
|
56
|
+
logger.log(`Showed permission dropdown for ${permission.id}`);
|
|
57
|
+
return { messageId: permissionMessage.id, contextHash };
|
|
67
58
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
flags: SILENT_MESSAGE_FLAGS,
|
|
75
|
-
});
|
|
59
|
+
/**
|
|
60
|
+
* Handle dropdown selection for permission.
|
|
61
|
+
*/
|
|
62
|
+
export async function handlePermissionSelectMenu(interaction) {
|
|
63
|
+
const customId = interaction.customId;
|
|
64
|
+
if (!customId.startsWith('permission:')) {
|
|
76
65
|
return;
|
|
77
66
|
}
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (!isThread) {
|
|
84
|
-
await command.reply({
|
|
85
|
-
content: 'This command can only be used in a thread with an active session',
|
|
67
|
+
const contextHash = customId.replace('permission:', '');
|
|
68
|
+
const context = pendingPermissionContexts.get(contextHash);
|
|
69
|
+
if (!context) {
|
|
70
|
+
await interaction.reply({
|
|
71
|
+
content: 'This permission request has expired or was already handled.',
|
|
86
72
|
ephemeral: true,
|
|
87
|
-
flags: SILENT_MESSAGE_FLAGS,
|
|
88
|
-
});
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
const pending = pendingPermissions.get(channel.id);
|
|
92
|
-
if (!pending) {
|
|
93
|
-
await command.reply({
|
|
94
|
-
content: 'No pending permission request in this thread',
|
|
95
|
-
ephemeral: true,
|
|
96
|
-
flags: SILENT_MESSAGE_FLAGS,
|
|
97
73
|
});
|
|
98
74
|
return;
|
|
99
75
|
}
|
|
76
|
+
await interaction.deferUpdate();
|
|
77
|
+
const response = interaction.values[0];
|
|
100
78
|
try {
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
response: 'reject',
|
|
109
|
-
},
|
|
79
|
+
const clientV2 = getOpencodeClientV2(context.directory);
|
|
80
|
+
if (!clientV2) {
|
|
81
|
+
throw new Error('OpenCode server not found for directory');
|
|
82
|
+
}
|
|
83
|
+
await clientV2.permission.reply({
|
|
84
|
+
requestID: context.permission.id,
|
|
85
|
+
reply: response,
|
|
110
86
|
});
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
87
|
+
pendingPermissionContexts.delete(contextHash);
|
|
88
|
+
// Update message: show result and remove dropdown
|
|
89
|
+
const resultText = (() => {
|
|
90
|
+
switch (response) {
|
|
91
|
+
case 'once':
|
|
92
|
+
return '✅ Permission **accepted**';
|
|
93
|
+
case 'always':
|
|
94
|
+
return '✅ Permission **accepted** (auto-approve similar requests)';
|
|
95
|
+
case 'reject':
|
|
96
|
+
return '❌ Permission **rejected**';
|
|
97
|
+
}
|
|
98
|
+
})();
|
|
99
|
+
const patternStr = context.permission.patterns.join(', ');
|
|
100
|
+
await interaction.editReply({
|
|
101
|
+
content: `⚠️ **Permission Required**\n\n` +
|
|
102
|
+
`**Type:** \`${context.permission.permission}\`\n` +
|
|
103
|
+
(patternStr ? `**Pattern:** \`${patternStr}\`\n\n` : '\n') +
|
|
104
|
+
resultText,
|
|
105
|
+
components: [], // Remove the dropdown
|
|
115
106
|
});
|
|
116
|
-
logger.log(`Permission ${
|
|
107
|
+
logger.log(`Permission ${context.permission.id} ${response}`);
|
|
117
108
|
}
|
|
118
109
|
catch (error) {
|
|
119
|
-
logger.error('
|
|
120
|
-
await
|
|
121
|
-
content: `Failed to
|
|
122
|
-
|
|
123
|
-
flags: SILENT_MESSAGE_FLAGS,
|
|
110
|
+
logger.error('Error handling permission:', error);
|
|
111
|
+
await interaction.editReply({
|
|
112
|
+
content: `Failed to process permission: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
113
|
+
components: [],
|
|
124
114
|
});
|
|
125
115
|
}
|
|
126
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Clean up a pending permission context (e.g., on auto-reject).
|
|
119
|
+
*/
|
|
120
|
+
export function cleanupPermissionContext(contextHash) {
|
|
121
|
+
pendingPermissionContexts.delete(contextHash);
|
|
122
|
+
}
|
|
@@ -6,7 +6,7 @@ import { handleSessionCommand, handleSessionAutocomplete } from './commands/sess
|
|
|
6
6
|
import { handleResumeCommand, handleResumeAutocomplete } from './commands/resume.js';
|
|
7
7
|
import { handleAddProjectCommand, handleAddProjectAutocomplete } from './commands/add-project.js';
|
|
8
8
|
import { handleCreateNewProjectCommand } from './commands/create-new-project.js';
|
|
9
|
-
import {
|
|
9
|
+
import { handlePermissionSelectMenu } from './commands/permissions.js';
|
|
10
10
|
import { handleAbortCommand } from './commands/abort.js';
|
|
11
11
|
import { handleShareCommand } from './commands/share.js';
|
|
12
12
|
import { handleForkCommand, handleForkSelectMenu } from './commands/fork.js';
|
|
@@ -58,13 +58,6 @@ export function registerInteractionHandler({ discordClient, appId, }) {
|
|
|
58
58
|
case 'create-new-project':
|
|
59
59
|
await handleCreateNewProjectCommand({ command: interaction, appId });
|
|
60
60
|
return;
|
|
61
|
-
case 'accept':
|
|
62
|
-
case 'accept-always':
|
|
63
|
-
await handleAcceptCommand({ command: interaction, appId });
|
|
64
|
-
return;
|
|
65
|
-
case 'reject':
|
|
66
|
-
await handleRejectCommand({ command: interaction, appId });
|
|
67
|
-
return;
|
|
68
61
|
case 'abort':
|
|
69
62
|
case 'stop':
|
|
70
63
|
await handleAbortCommand({ command: interaction, appId });
|
|
@@ -123,6 +116,10 @@ export function registerInteractionHandler({ discordClient, appId, }) {
|
|
|
123
116
|
await handleAskQuestionSelectMenu(interaction);
|
|
124
117
|
return;
|
|
125
118
|
}
|
|
119
|
+
if (customId.startsWith('permission:')) {
|
|
120
|
+
await handlePermissionSelectMenu(interaction);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
126
123
|
return;
|
|
127
124
|
}
|
|
128
125
|
}
|
package/dist/session-handler.js
CHANGED
|
@@ -10,6 +10,7 @@ import { getOpencodeSystemMessage } from './system-message.js';
|
|
|
10
10
|
import { createLogger } from './logger.js';
|
|
11
11
|
import { isAbortError } from './utils.js';
|
|
12
12
|
import { showAskUserQuestionDropdowns } from './commands/ask-question.js';
|
|
13
|
+
import { showPermissionDropdown, cleanupPermissionContext } from './commands/permissions.js';
|
|
13
14
|
const sessionLogger = createLogger('SESSION');
|
|
14
15
|
const voiceLogger = createLogger('VOICE');
|
|
15
16
|
const discordLogger = createLogger('DISCORD');
|
|
@@ -135,18 +136,21 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
135
136
|
if (pendingPerm) {
|
|
136
137
|
try {
|
|
137
138
|
sessionLogger.log(`[PERMISSION] Auto-rejecting pending permission ${pendingPerm.permission.id} due to new message`);
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
139
|
+
const clientV2 = getOpencodeClientV2(directory);
|
|
140
|
+
if (clientV2) {
|
|
141
|
+
await clientV2.permission.reply({
|
|
142
|
+
requestID: pendingPerm.permission.id,
|
|
143
|
+
reply: 'reject',
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
// Clean up both the pending permission and its dropdown context
|
|
147
|
+
cleanupPermissionContext(pendingPerm.contextHash);
|
|
145
148
|
pendingPermissions.delete(thread.id);
|
|
146
149
|
await sendThreadMessage(thread, `⚠️ Previous permission request auto-rejected due to new message`);
|
|
147
150
|
}
|
|
148
151
|
catch (e) {
|
|
149
152
|
sessionLogger.log(`[PERMISSION] Failed to auto-reject permission:`, e);
|
|
153
|
+
cleanupPermissionContext(pendingPerm.contextHash);
|
|
150
154
|
pendingPermissions.delete(thread.id);
|
|
151
155
|
}
|
|
152
156
|
}
|
|
@@ -350,15 +354,17 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
350
354
|
continue;
|
|
351
355
|
}
|
|
352
356
|
sessionLogger.log(`Permission requested: permission=${permission.permission}, patterns=${permission.patterns.join(', ')}`);
|
|
353
|
-
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
357
|
+
// Show dropdown instead of text message
|
|
358
|
+
const { messageId, contextHash } = await showPermissionDropdown({
|
|
359
|
+
thread,
|
|
360
|
+
permission,
|
|
361
|
+
directory,
|
|
362
|
+
});
|
|
358
363
|
pendingPermissions.set(thread.id, {
|
|
359
364
|
permission,
|
|
360
|
-
messageId
|
|
365
|
+
messageId,
|
|
361
366
|
directory,
|
|
367
|
+
contextHash,
|
|
362
368
|
});
|
|
363
369
|
}
|
|
364
370
|
else if (event.type === 'permission.replied') {
|
|
@@ -369,6 +375,7 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
369
375
|
sessionLogger.log(`Permission ${requestID} replied with: ${reply}`);
|
|
370
376
|
const pending = pendingPermissions.get(thread.id);
|
|
371
377
|
if (pending && pending.permission.id === requestID) {
|
|
378
|
+
cleanupPermissionContext(pending.contextHash);
|
|
372
379
|
pendingPermissions.delete(thread.id);
|
|
373
380
|
}
|
|
374
381
|
}
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -219,18 +219,6 @@ async function registerCommands(token: string, appId: string, userCommands: Open
|
|
|
219
219
|
return option
|
|
220
220
|
})
|
|
221
221
|
.toJSON(),
|
|
222
|
-
new SlashCommandBuilder()
|
|
223
|
-
.setName('accept')
|
|
224
|
-
.setDescription('Accept a pending permission request (this request only)')
|
|
225
|
-
.toJSON(),
|
|
226
|
-
new SlashCommandBuilder()
|
|
227
|
-
.setName('accept-always')
|
|
228
|
-
.setDescription('Accept and auto-approve future requests matching this pattern')
|
|
229
|
-
.toJSON(),
|
|
230
|
-
new SlashCommandBuilder()
|
|
231
|
-
.setName('reject')
|
|
232
|
-
.setDescription('Reject a pending permission request')
|
|
233
|
-
.toJSON(),
|
|
234
222
|
new SlashCommandBuilder()
|
|
235
223
|
.setName('abort')
|
|
236
224
|
.setDescription('Abort the current OpenCode request in this thread')
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
type ThreadChannel,
|
|
10
10
|
} from 'discord.js'
|
|
11
11
|
import crypto from 'node:crypto'
|
|
12
|
-
import { sendThreadMessage } from '../discord-utils.js'
|
|
12
|
+
import { sendThreadMessage, NOTIFY_MESSAGE_FLAGS } from '../discord-utils.js'
|
|
13
13
|
import { getOpencodeServerPort } from '../opencode.js'
|
|
14
14
|
import { createLogger } from '../logger.js'
|
|
15
15
|
|
|
@@ -111,6 +111,7 @@ export async function showAskUserQuestionDropdowns({
|
|
|
111
111
|
await thread.send({
|
|
112
112
|
content: `**${q.header}**\n${q.question}`,
|
|
113
113
|
components: [actionRow],
|
|
114
|
+
flags: NOTIFY_MESSAGE_FLAGS,
|
|
114
115
|
})
|
|
115
116
|
}
|
|
116
117
|
|
|
@@ -1,146 +1,171 @@
|
|
|
1
|
-
// Permission
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
// Permission dropdown handler - Shows dropdown for permission requests.
|
|
2
|
+
// When OpenCode asks for permission, this module renders a dropdown
|
|
3
|
+
// with Accept, Accept Always, and Deny options.
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
StringSelectMenuBuilder,
|
|
7
|
+
StringSelectMenuInteraction,
|
|
8
|
+
ActionRowBuilder,
|
|
9
|
+
type ThreadChannel,
|
|
10
|
+
} from 'discord.js'
|
|
11
|
+
import crypto from 'node:crypto'
|
|
12
|
+
import type { PermissionRequest } from '@opencode-ai/sdk/v2'
|
|
13
|
+
import { getOpencodeClientV2 } from '../opencode.js'
|
|
14
|
+
import { NOTIFY_MESSAGE_FLAGS } from '../discord-utils.js'
|
|
8
15
|
import { createLogger } from '../logger.js'
|
|
9
16
|
|
|
10
17
|
const logger = createLogger('PERMISSIONS')
|
|
11
18
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (!channel) {
|
|
19
|
-
await command.reply({
|
|
20
|
-
content: 'This command can only be used in a channel',
|
|
21
|
-
ephemeral: true,
|
|
22
|
-
flags: SILENT_MESSAGE_FLAGS,
|
|
23
|
-
})
|
|
24
|
-
return
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const isThread = [
|
|
28
|
-
ChannelType.PublicThread,
|
|
29
|
-
ChannelType.PrivateThread,
|
|
30
|
-
ChannelType.AnnouncementThread,
|
|
31
|
-
].includes(channel.type)
|
|
32
|
-
|
|
33
|
-
if (!isThread) {
|
|
34
|
-
await command.reply({
|
|
35
|
-
content: 'This command can only be used in a thread with an active session',
|
|
36
|
-
ephemeral: true,
|
|
37
|
-
flags: SILENT_MESSAGE_FLAGS,
|
|
38
|
-
})
|
|
39
|
-
return
|
|
40
|
-
}
|
|
19
|
+
type PendingPermissionContext = {
|
|
20
|
+
permission: PermissionRequest
|
|
21
|
+
directory: string
|
|
22
|
+
thread: ThreadChannel
|
|
23
|
+
contextHash: string
|
|
24
|
+
}
|
|
41
25
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
26
|
+
// Store pending permission contexts by hash
|
|
27
|
+
export const pendingPermissionContexts = new Map<string, PendingPermissionContext>()
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Show permission dropdown for a permission request.
|
|
31
|
+
* Returns the message ID and context hash for tracking.
|
|
32
|
+
*/
|
|
33
|
+
export async function showPermissionDropdown({
|
|
34
|
+
thread,
|
|
35
|
+
permission,
|
|
36
|
+
directory,
|
|
37
|
+
}: {
|
|
38
|
+
thread: ThreadChannel
|
|
39
|
+
permission: PermissionRequest
|
|
40
|
+
directory: string
|
|
41
|
+
}): Promise<{ messageId: string; contextHash: string }> {
|
|
42
|
+
const contextHash = crypto.randomBytes(8).toString('hex')
|
|
43
|
+
|
|
44
|
+
const context: PendingPermissionContext = {
|
|
45
|
+
permission,
|
|
46
|
+
directory,
|
|
47
|
+
thread,
|
|
48
|
+
contextHash,
|
|
50
49
|
}
|
|
51
50
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
51
|
+
pendingPermissionContexts.set(contextHash, context)
|
|
52
|
+
|
|
53
|
+
const patternStr = permission.patterns.join(', ')
|
|
54
|
+
|
|
55
|
+
// Build dropdown options
|
|
56
|
+
const options = [
|
|
57
|
+
{
|
|
58
|
+
label: 'Accept',
|
|
59
|
+
value: 'once',
|
|
60
|
+
description: 'Allow this request only',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
label: 'Accept Always',
|
|
64
|
+
value: 'always',
|
|
65
|
+
description: 'Auto-approve similar requests',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
label: 'Deny',
|
|
69
|
+
value: 'reject',
|
|
70
|
+
description: 'Reject this permission request',
|
|
71
|
+
},
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
const selectMenu = new StringSelectMenuBuilder()
|
|
75
|
+
.setCustomId(`permission:${contextHash}`)
|
|
76
|
+
.setPlaceholder('Choose an action')
|
|
77
|
+
.addOptions(options)
|
|
78
|
+
|
|
79
|
+
const actionRow = new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(selectMenu)
|
|
80
|
+
|
|
81
|
+
const permissionMessage = await thread.send({
|
|
82
|
+
content:
|
|
83
|
+
`⚠️ **Permission Required**\n\n` +
|
|
84
|
+
`**Type:** \`${permission.permission}\`\n` +
|
|
85
|
+
(patternStr ? `**Pattern:** \`${patternStr}\`` : ''),
|
|
86
|
+
components: [actionRow],
|
|
87
|
+
flags: NOTIFY_MESSAGE_FLAGS,
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
logger.log(`Showed permission dropdown for ${permission.id}`)
|
|
91
|
+
|
|
92
|
+
return { messageId: permissionMessage.id, contextHash }
|
|
79
93
|
}
|
|
80
94
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
95
|
+
/**
|
|
96
|
+
* Handle dropdown selection for permission.
|
|
97
|
+
*/
|
|
98
|
+
export async function handlePermissionSelectMenu(
|
|
99
|
+
interaction: StringSelectMenuInteraction
|
|
100
|
+
): Promise<void> {
|
|
101
|
+
const customId = interaction.customId
|
|
85
102
|
|
|
86
|
-
if (!
|
|
87
|
-
await command.reply({
|
|
88
|
-
content: 'This command can only be used in a channel',
|
|
89
|
-
ephemeral: true,
|
|
90
|
-
flags: SILENT_MESSAGE_FLAGS,
|
|
91
|
-
})
|
|
103
|
+
if (!customId.startsWith('permission:')) {
|
|
92
104
|
return
|
|
93
105
|
}
|
|
94
106
|
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
ChannelType.PrivateThread,
|
|
98
|
-
ChannelType.AnnouncementThread,
|
|
99
|
-
].includes(channel.type)
|
|
107
|
+
const contextHash = customId.replace('permission:', '')
|
|
108
|
+
const context = pendingPermissionContexts.get(contextHash)
|
|
100
109
|
|
|
101
|
-
if (!
|
|
102
|
-
await
|
|
103
|
-
content: 'This
|
|
110
|
+
if (!context) {
|
|
111
|
+
await interaction.reply({
|
|
112
|
+
content: 'This permission request has expired or was already handled.',
|
|
104
113
|
ephemeral: true,
|
|
105
|
-
flags: SILENT_MESSAGE_FLAGS,
|
|
106
114
|
})
|
|
107
115
|
return
|
|
108
116
|
}
|
|
109
117
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
content: 'No pending permission request in this thread',
|
|
114
|
-
ephemeral: true,
|
|
115
|
-
flags: SILENT_MESSAGE_FLAGS,
|
|
116
|
-
})
|
|
117
|
-
return
|
|
118
|
-
}
|
|
118
|
+
await interaction.deferUpdate()
|
|
119
|
+
|
|
120
|
+
const response = interaction.values[0] as 'once' | 'always' | 'reject'
|
|
119
121
|
|
|
120
122
|
try {
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
response: 'reject',
|
|
129
|
-
},
|
|
123
|
+
const clientV2 = getOpencodeClientV2(context.directory)
|
|
124
|
+
if (!clientV2) {
|
|
125
|
+
throw new Error('OpenCode server not found for directory')
|
|
126
|
+
}
|
|
127
|
+
await clientV2.permission.reply({
|
|
128
|
+
requestID: context.permission.id,
|
|
129
|
+
reply: response,
|
|
130
130
|
})
|
|
131
131
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
132
|
+
pendingPermissionContexts.delete(contextHash)
|
|
133
|
+
|
|
134
|
+
// Update message: show result and remove dropdown
|
|
135
|
+
const resultText = (() => {
|
|
136
|
+
switch (response) {
|
|
137
|
+
case 'once':
|
|
138
|
+
return '✅ Permission **accepted**'
|
|
139
|
+
case 'always':
|
|
140
|
+
return '✅ Permission **accepted** (auto-approve similar requests)'
|
|
141
|
+
case 'reject':
|
|
142
|
+
return '❌ Permission **rejected**'
|
|
143
|
+
}
|
|
144
|
+
})()
|
|
145
|
+
|
|
146
|
+
const patternStr = context.permission.patterns.join(', ')
|
|
147
|
+
await interaction.editReply({
|
|
148
|
+
content:
|
|
149
|
+
`⚠️ **Permission Required**\n\n` +
|
|
150
|
+
`**Type:** \`${context.permission.permission}\`\n` +
|
|
151
|
+
(patternStr ? `**Pattern:** \`${patternStr}\`\n\n` : '\n') +
|
|
152
|
+
resultText,
|
|
153
|
+
components: [], // Remove the dropdown
|
|
136
154
|
})
|
|
137
|
-
|
|
155
|
+
|
|
156
|
+
logger.log(`Permission ${context.permission.id} ${response}`)
|
|
138
157
|
} catch (error) {
|
|
139
|
-
logger.error('
|
|
140
|
-
await
|
|
141
|
-
content: `Failed to
|
|
142
|
-
|
|
143
|
-
flags: SILENT_MESSAGE_FLAGS,
|
|
158
|
+
logger.error('Error handling permission:', error)
|
|
159
|
+
await interaction.editReply({
|
|
160
|
+
content: `Failed to process permission: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
161
|
+
components: [],
|
|
144
162
|
})
|
|
145
163
|
}
|
|
146
164
|
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Clean up a pending permission context (e.g., on auto-reject).
|
|
168
|
+
*/
|
|
169
|
+
export function cleanupPermissionContext(contextHash: string): void {
|
|
170
|
+
pendingPermissionContexts.delete(contextHash)
|
|
171
|
+
}
|
|
@@ -7,7 +7,7 @@ import { handleSessionCommand, handleSessionAutocomplete } from './commands/sess
|
|
|
7
7
|
import { handleResumeCommand, handleResumeAutocomplete } from './commands/resume.js'
|
|
8
8
|
import { handleAddProjectCommand, handleAddProjectAutocomplete } from './commands/add-project.js'
|
|
9
9
|
import { handleCreateNewProjectCommand } from './commands/create-new-project.js'
|
|
10
|
-
import {
|
|
10
|
+
import { handlePermissionSelectMenu } from './commands/permissions.js'
|
|
11
11
|
import { handleAbortCommand } from './commands/abort.js'
|
|
12
12
|
import { handleShareCommand } from './commands/share.js'
|
|
13
13
|
import { handleForkCommand, handleForkSelectMenu } from './commands/fork.js'
|
|
@@ -85,15 +85,6 @@ export function registerInteractionHandler({
|
|
|
85
85
|
await handleCreateNewProjectCommand({ command: interaction, appId })
|
|
86
86
|
return
|
|
87
87
|
|
|
88
|
-
case 'accept':
|
|
89
|
-
case 'accept-always':
|
|
90
|
-
await handleAcceptCommand({ command: interaction, appId })
|
|
91
|
-
return
|
|
92
|
-
|
|
93
|
-
case 'reject':
|
|
94
|
-
await handleRejectCommand({ command: interaction, appId })
|
|
95
|
-
return
|
|
96
|
-
|
|
97
88
|
case 'abort':
|
|
98
89
|
case 'stop':
|
|
99
90
|
await handleAbortCommand({ command: interaction, appId })
|
|
@@ -167,6 +158,11 @@ export function registerInteractionHandler({
|
|
|
167
158
|
await handleAskQuestionSelectMenu(interaction)
|
|
168
159
|
return
|
|
169
160
|
}
|
|
161
|
+
|
|
162
|
+
if (customId.startsWith('permission:')) {
|
|
163
|
+
await handlePermissionSelectMenu(interaction)
|
|
164
|
+
return
|
|
165
|
+
}
|
|
170
166
|
return
|
|
171
167
|
}
|
|
172
168
|
} catch (error) {
|
package/src/session-handler.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { getOpencodeSystemMessage } from './system-message.js'
|
|
|
14
14
|
import { createLogger } from './logger.js'
|
|
15
15
|
import { isAbortError } from './utils.js'
|
|
16
16
|
import { showAskUserQuestionDropdowns } from './commands/ask-question.js'
|
|
17
|
+
import { showPermissionDropdown, cleanupPermissionContext } from './commands/permissions.js'
|
|
17
18
|
|
|
18
19
|
const sessionLogger = createLogger('SESSION')
|
|
19
20
|
const voiceLogger = createLogger('VOICE')
|
|
@@ -23,7 +24,7 @@ export const abortControllers = new Map<string, AbortController>()
|
|
|
23
24
|
|
|
24
25
|
export const pendingPermissions = new Map<
|
|
25
26
|
string,
|
|
26
|
-
{ permission: PermissionRequest; messageId: string; directory: string }
|
|
27
|
+
{ permission: PermissionRequest; messageId: string; directory: string; contextHash: string }
|
|
27
28
|
>()
|
|
28
29
|
|
|
29
30
|
export type QueuedMessage = {
|
|
@@ -220,17 +221,20 @@ export async function handleOpencodeSession({
|
|
|
220
221
|
if (pendingPerm) {
|
|
221
222
|
try {
|
|
222
223
|
sessionLogger.log(`[PERMISSION] Auto-rejecting pending permission ${pendingPerm.permission.id} due to new message`)
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
}
|
|
224
|
+
const clientV2 = getOpencodeClientV2(directory)
|
|
225
|
+
if (clientV2) {
|
|
226
|
+
await clientV2.permission.reply({
|
|
227
|
+
requestID: pendingPerm.permission.id,
|
|
228
|
+
reply: 'reject',
|
|
229
|
+
})
|
|
230
|
+
}
|
|
231
|
+
// Clean up both the pending permission and its dropdown context
|
|
232
|
+
cleanupPermissionContext(pendingPerm.contextHash)
|
|
230
233
|
pendingPermissions.delete(thread.id)
|
|
231
234
|
await sendThreadMessage(thread, `⚠️ Previous permission request auto-rejected due to new message`)
|
|
232
235
|
} catch (e) {
|
|
233
236
|
sessionLogger.log(`[PERMISSION] Failed to auto-reject permission:`, e)
|
|
237
|
+
cleanupPermissionContext(pendingPerm.contextHash)
|
|
234
238
|
pendingPermissions.delete(thread.id)
|
|
235
239
|
}
|
|
236
240
|
}
|
|
@@ -484,20 +488,18 @@ export async function handleOpencodeSession({
|
|
|
484
488
|
`Permission requested: permission=${permission.permission}, patterns=${permission.patterns.join(', ')}`,
|
|
485
489
|
)
|
|
486
490
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
const permissionMessage = await sendThreadMessage(
|
|
491
|
+
// Show dropdown instead of text message
|
|
492
|
+
const { messageId, contextHash } = await showPermissionDropdown({
|
|
490
493
|
thread,
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
`\nUse \`/accept\` or \`/reject\` to respond.`,
|
|
495
|
-
)
|
|
494
|
+
permission,
|
|
495
|
+
directory,
|
|
496
|
+
})
|
|
496
497
|
|
|
497
498
|
pendingPermissions.set(thread.id, {
|
|
498
499
|
permission,
|
|
499
|
-
messageId
|
|
500
|
+
messageId,
|
|
500
501
|
directory,
|
|
502
|
+
contextHash,
|
|
501
503
|
})
|
|
502
504
|
} else if (event.type === 'permission.replied') {
|
|
503
505
|
const { requestID, reply, sessionID } = event.properties
|
|
@@ -511,6 +513,7 @@ export async function handleOpencodeSession({
|
|
|
511
513
|
|
|
512
514
|
const pending = pendingPermissions.get(thread.id)
|
|
513
515
|
if (pending && pending.permission.id === requestID) {
|
|
516
|
+
cleanupPermissionContext(pending.contextHash)
|
|
514
517
|
pendingPermissions.delete(thread.id)
|
|
515
518
|
}
|
|
516
519
|
} else if (event.type === 'question.asked') {
|