@brianli/kimaki 0.4.72-brianli.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/bin.js +2 -0
- package/dist/ai-tool-to-genai.js +233 -0
- package/dist/ai-tool-to-genai.test.js +267 -0
- package/dist/ai-tool.js +6 -0
- package/dist/bin.js +87 -0
- package/dist/bot-token.js +121 -0
- package/dist/bot-token.test.js +134 -0
- package/dist/channel-management.js +101 -0
- package/dist/cli-parsing.test.js +89 -0
- package/dist/cli.js +2529 -0
- package/dist/commands/abort.js +82 -0
- package/dist/commands/action-buttons.js +257 -0
- package/dist/commands/add-project.js +114 -0
- package/dist/commands/agent.js +291 -0
- package/dist/commands/ask-question.js +223 -0
- package/dist/commands/compact.js +120 -0
- package/dist/commands/context-usage.js +140 -0
- package/dist/commands/create-new-project.js +118 -0
- package/dist/commands/diff.js +128 -0
- package/dist/commands/file-upload.js +275 -0
- package/dist/commands/fork.js +217 -0
- package/dist/commands/gemini-apikey.js +70 -0
- package/dist/commands/login.js +490 -0
- package/dist/commands/mention-mode.js +51 -0
- package/dist/commands/merge-worktree.js +124 -0
- package/dist/commands/model.js +694 -0
- package/dist/commands/permissions.js +163 -0
- package/dist/commands/queue.js +217 -0
- package/dist/commands/remove-project.js +115 -0
- package/dist/commands/restart-opencode-server.js +116 -0
- package/dist/commands/resume.js +159 -0
- package/dist/commands/run-command.js +79 -0
- package/dist/commands/session-id.js +78 -0
- package/dist/commands/session.js +192 -0
- package/dist/commands/share.js +80 -0
- package/dist/commands/types.js +2 -0
- package/dist/commands/undo-redo.js +159 -0
- package/dist/commands/unset-model.js +152 -0
- package/dist/commands/upgrade.js +42 -0
- package/dist/commands/user-command.js +148 -0
- package/dist/commands/verbosity.js +60 -0
- package/dist/commands/worktree-settings.js +50 -0
- package/dist/commands/worktree.js +299 -0
- package/dist/condense-memory.js +33 -0
- package/dist/config.js +110 -0
- package/dist/database.js +1050 -0
- package/dist/db.js +159 -0
- package/dist/db.test.js +49 -0
- package/dist/discord-api.js +28 -0
- package/dist/discord-auth.js +231 -0
- package/dist/discord-auth.test.js +80 -0
- package/dist/discord-bot.js +997 -0
- package/dist/discord-utils.js +560 -0
- package/dist/discord-utils.test.js +115 -0
- package/dist/errors.js +167 -0
- package/dist/escape-backticks.test.js +429 -0
- package/dist/format-tables.js +122 -0
- package/dist/format-tables.test.js +199 -0
- package/dist/forum-sync/config.js +79 -0
- package/dist/forum-sync/discord-operations.js +154 -0
- package/dist/forum-sync/index.js +5 -0
- package/dist/forum-sync/markdown.js +117 -0
- package/dist/forum-sync/sync-to-discord.js +417 -0
- package/dist/forum-sync/sync-to-files.js +190 -0
- package/dist/forum-sync/types.js +53 -0
- package/dist/forum-sync/watchers.js +307 -0
- package/dist/gateway-consumer.js +232 -0
- package/dist/gateway-consumer.test.js +18 -0
- package/dist/genai-worker-wrapper.js +111 -0
- package/dist/genai-worker.js +311 -0
- package/dist/genai.js +232 -0
- package/dist/generated/browser.js +17 -0
- package/dist/generated/client.js +35 -0
- package/dist/generated/commonInputTypes.js +10 -0
- package/dist/generated/enums.js +30 -0
- package/dist/generated/internal/class.js +41 -0
- package/dist/generated/internal/prismaNamespace.js +239 -0
- package/dist/generated/internal/prismaNamespaceBrowser.js +209 -0
- package/dist/generated/models/bot_api_keys.js +1 -0
- package/dist/generated/models/bot_tokens.js +1 -0
- package/dist/generated/models/channel_agents.js +1 -0
- package/dist/generated/models/channel_directories.js +1 -0
- package/dist/generated/models/channel_mention_mode.js +1 -0
- package/dist/generated/models/channel_models.js +1 -0
- package/dist/generated/models/channel_verbosity.js +1 -0
- package/dist/generated/models/channel_worktrees.js +1 -0
- package/dist/generated/models/forum_sync_configs.js +1 -0
- package/dist/generated/models/global_models.js +1 -0
- package/dist/generated/models/ipc_requests.js +1 -0
- package/dist/generated/models/part_messages.js +1 -0
- package/dist/generated/models/scheduled_tasks.js +1 -0
- package/dist/generated/models/session_agents.js +1 -0
- package/dist/generated/models/session_models.js +1 -0
- package/dist/generated/models/session_start_sources.js +1 -0
- package/dist/generated/models/thread_sessions.js +1 -0
- package/dist/generated/models/thread_worktrees.js +1 -0
- package/dist/generated/models.js +1 -0
- package/dist/heap-monitor.js +95 -0
- package/dist/hrana-server.js +416 -0
- package/dist/hrana-server.test.js +368 -0
- package/dist/image-utils.js +112 -0
- package/dist/interaction-handler.js +327 -0
- package/dist/ipc-polling.js +251 -0
- package/dist/kimaki-digital-twin.e2e.test.js +165 -0
- package/dist/limit-heading-depth.js +25 -0
- package/dist/limit-heading-depth.test.js +105 -0
- package/dist/logger.js +160 -0
- package/dist/markdown.js +342 -0
- package/dist/markdown.test.js +253 -0
- package/dist/message-formatting.js +433 -0
- package/dist/message-formatting.test.js +73 -0
- package/dist/openai-realtime.js +228 -0
- package/dist/opencode-plugin-loading.e2e.test.js +91 -0
- package/dist/opencode-plugin.js +536 -0
- package/dist/opencode-plugin.test.js +98 -0
- package/dist/opencode.js +409 -0
- package/dist/privacy-sanitizer.js +105 -0
- package/dist/runtime-mode.js +51 -0
- package/dist/runtime-mode.test.js +115 -0
- package/dist/sentry.js +127 -0
- package/dist/session-handler/state.js +151 -0
- package/dist/session-handler.js +1874 -0
- package/dist/session-search.js +100 -0
- package/dist/session-search.test.js +40 -0
- package/dist/startup-service.js +153 -0
- package/dist/system-message.js +499 -0
- package/dist/task-runner.js +282 -0
- package/dist/task-schedule.js +191 -0
- package/dist/task-schedule.test.js +71 -0
- package/dist/thinking-utils.js +35 -0
- package/dist/thread-message-queue.e2e.test.js +781 -0
- package/dist/tools.js +359 -0
- package/dist/unnest-code-blocks.js +136 -0
- package/dist/unnest-code-blocks.test.js +641 -0
- package/dist/upgrade.js +114 -0
- package/dist/utils.js +109 -0
- package/dist/voice-handler.js +606 -0
- package/dist/voice.js +304 -0
- package/dist/voice.test.js +187 -0
- package/dist/wait-session.js +94 -0
- package/dist/worker-types.js +4 -0
- package/dist/worktree-utils.js +727 -0
- package/dist/xml.js +92 -0
- package/dist/xml.test.js +32 -0
- package/package.json +82 -0
- package/schema.prisma +246 -0
- package/skills/batch/SKILL.md +87 -0
- package/skills/critique/SKILL.md +129 -0
- package/skills/errore/SKILL.md +589 -0
- package/skills/goke/.prettierrc +5 -0
- package/skills/goke/CHANGELOG.md +40 -0
- package/skills/goke/LICENSE +21 -0
- package/skills/goke/README.md +666 -0
- package/skills/goke/SKILL.md +458 -0
- package/skills/goke/package.json +43 -0
- package/skills/goke/src/__test__/coerce.test.ts +411 -0
- package/skills/goke/src/__test__/index.test.ts +1798 -0
- package/skills/goke/src/__test__/types.test-d.ts +111 -0
- package/skills/goke/src/coerce.ts +547 -0
- package/skills/goke/src/goke.ts +1362 -0
- package/skills/goke/src/index.ts +16 -0
- package/skills/goke/src/mri.ts +164 -0
- package/skills/goke/tsconfig.json +15 -0
- package/skills/jitter/EDITOR.md +219 -0
- package/skills/jitter/EXPORT-INTERNALS.md +309 -0
- package/skills/jitter/SKILL.md +158 -0
- package/skills/jitter/jitter-clipboard.json +1042 -0
- package/skills/jitter/package.json +14 -0
- package/skills/jitter/tsconfig.json +15 -0
- package/skills/jitter/utils/actions.ts +212 -0
- package/skills/jitter/utils/export.ts +114 -0
- package/skills/jitter/utils/index.ts +141 -0
- package/skills/jitter/utils/snapshot.ts +154 -0
- package/skills/jitter/utils/traverse.ts +246 -0
- package/skills/jitter/utils/types.ts +279 -0
- package/skills/jitter/utils/wait.ts +133 -0
- package/skills/playwriter/SKILL.md +31 -0
- package/skills/security-review/SKILL.md +208 -0
- package/skills/simplify/SKILL.md +58 -0
- package/skills/termcast/SKILL.md +945 -0
- package/skills/tuistory/SKILL.md +250 -0
- package/skills/zustand-centralized-state/SKILL.md +582 -0
- package/src/__snapshots__/compact-session-context-no-system.md +35 -0
- package/src/__snapshots__/compact-session-context.md +41 -0
- package/src/__snapshots__/first-session-no-info.md +17 -0
- package/src/__snapshots__/first-session-with-info.md +23 -0
- package/src/__snapshots__/session-1.md +17 -0
- package/src/__snapshots__/session-2.md +5871 -0
- package/src/__snapshots__/session-3.md +17 -0
- package/src/__snapshots__/session-with-tools.md +5871 -0
- package/src/ai-tool-to-genai.test.ts +296 -0
- package/src/ai-tool-to-genai.ts +282 -0
- package/src/ai-tool.ts +39 -0
- package/src/bin.ts +108 -0
- package/src/bot-token.test.ts +171 -0
- package/src/bot-token.ts +159 -0
- package/src/channel-management.ts +172 -0
- package/src/cli-parsing.test.ts +132 -0
- package/src/cli.ts +3605 -0
- package/src/commands/abort.ts +112 -0
- package/src/commands/action-buttons.ts +376 -0
- package/src/commands/add-project.ts +152 -0
- package/src/commands/agent.ts +404 -0
- package/src/commands/ask-question.ts +330 -0
- package/src/commands/compact.ts +157 -0
- package/src/commands/context-usage.ts +199 -0
- package/src/commands/create-new-project.ts +179 -0
- package/src/commands/diff.ts +165 -0
- package/src/commands/file-upload.ts +389 -0
- package/src/commands/fork.ts +320 -0
- package/src/commands/gemini-apikey.ts +104 -0
- package/src/commands/login.ts +634 -0
- package/src/commands/mention-mode.ts +77 -0
- package/src/commands/merge-worktree.ts +177 -0
- package/src/commands/model.ts +961 -0
- package/src/commands/permissions.ts +261 -0
- package/src/commands/queue.ts +296 -0
- package/src/commands/remove-project.ts +155 -0
- package/src/commands/restart-opencode-server.ts +162 -0
- package/src/commands/resume.ts +242 -0
- package/src/commands/run-command.ts +123 -0
- package/src/commands/session-id.ts +109 -0
- package/src/commands/session.ts +250 -0
- package/src/commands/share.ts +106 -0
- package/src/commands/types.ts +25 -0
- package/src/commands/undo-redo.ts +221 -0
- package/src/commands/unset-model.ts +189 -0
- package/src/commands/upgrade.ts +52 -0
- package/src/commands/user-command.ts +193 -0
- package/src/commands/verbosity.ts +88 -0
- package/src/commands/worktree-settings.ts +79 -0
- package/src/commands/worktree.ts +431 -0
- package/src/condense-memory.ts +36 -0
- package/src/config.ts +148 -0
- package/src/database.ts +1530 -0
- package/src/db.test.ts +60 -0
- package/src/db.ts +190 -0
- package/src/discord-api.ts +35 -0
- package/src/discord-bot.ts +1316 -0
- package/src/discord-utils.test.ts +132 -0
- package/src/discord-utils.ts +767 -0
- package/src/errors.ts +213 -0
- package/src/escape-backticks.test.ts +469 -0
- package/src/format-tables.test.ts +223 -0
- package/src/format-tables.ts +145 -0
- package/src/forum-sync/config.ts +92 -0
- package/src/forum-sync/discord-operations.ts +241 -0
- package/src/forum-sync/index.ts +9 -0
- package/src/forum-sync/markdown.ts +176 -0
- package/src/forum-sync/sync-to-discord.ts +595 -0
- package/src/forum-sync/sync-to-files.ts +294 -0
- package/src/forum-sync/types.ts +175 -0
- package/src/forum-sync/watchers.ts +454 -0
- package/src/genai-worker-wrapper.ts +164 -0
- package/src/genai-worker.ts +386 -0
- package/src/genai.ts +321 -0
- package/src/generated/browser.ts +109 -0
- package/src/generated/client.ts +131 -0
- package/src/generated/commonInputTypes.ts +512 -0
- package/src/generated/enums.ts +46 -0
- package/src/generated/internal/class.ts +362 -0
- package/src/generated/internal/prismaNamespace.ts +2251 -0
- package/src/generated/internal/prismaNamespaceBrowser.ts +308 -0
- package/src/generated/models/bot_api_keys.ts +1288 -0
- package/src/generated/models/bot_tokens.ts +1577 -0
- package/src/generated/models/channel_agents.ts +1256 -0
- package/src/generated/models/channel_directories.ts +2104 -0
- package/src/generated/models/channel_mention_mode.ts +1300 -0
- package/src/generated/models/channel_models.ts +1288 -0
- package/src/generated/models/channel_verbosity.ts +1224 -0
- package/src/generated/models/channel_worktrees.ts +1308 -0
- package/src/generated/models/forum_sync_configs.ts +1452 -0
- package/src/generated/models/global_models.ts +1288 -0
- package/src/generated/models/ipc_requests.ts +1485 -0
- package/src/generated/models/part_messages.ts +1302 -0
- package/src/generated/models/scheduled_tasks.ts +2320 -0
- package/src/generated/models/session_agents.ts +1086 -0
- package/src/generated/models/session_models.ts +1114 -0
- package/src/generated/models/session_start_sources.ts +1408 -0
- package/src/generated/models/thread_sessions.ts +1599 -0
- package/src/generated/models/thread_worktrees.ts +1352 -0
- package/src/generated/models.ts +29 -0
- package/src/heap-monitor.ts +121 -0
- package/src/hrana-server.test.ts +428 -0
- package/src/hrana-server.ts +547 -0
- package/src/image-utils.ts +149 -0
- package/src/interaction-handler.ts +461 -0
- package/src/ipc-polling.ts +325 -0
- package/src/kimaki-digital-twin.e2e.test.ts +201 -0
- package/src/limit-heading-depth.test.ts +116 -0
- package/src/limit-heading-depth.ts +26 -0
- package/src/logger.ts +203 -0
- package/src/markdown.test.ts +360 -0
- package/src/markdown.ts +410 -0
- package/src/message-formatting.test.ts +81 -0
- package/src/message-formatting.ts +549 -0
- package/src/openai-realtime.ts +362 -0
- package/src/opencode-plugin-loading.e2e.test.ts +112 -0
- package/src/opencode-plugin.test.ts +108 -0
- package/src/opencode-plugin.ts +652 -0
- package/src/opencode.ts +554 -0
- package/src/privacy-sanitizer.ts +142 -0
- package/src/schema.sql +158 -0
- package/src/sentry.ts +137 -0
- package/src/session-handler/state.ts +232 -0
- package/src/session-handler.ts +2668 -0
- package/src/session-search.test.ts +50 -0
- package/src/session-search.ts +148 -0
- package/src/startup-service.ts +200 -0
- package/src/system-message.ts +568 -0
- package/src/task-runner.ts +425 -0
- package/src/task-schedule.test.ts +84 -0
- package/src/task-schedule.ts +287 -0
- package/src/thinking-utils.ts +61 -0
- package/src/thread-message-queue.e2e.test.ts +997 -0
- package/src/tools.ts +432 -0
- package/src/unnest-code-blocks.test.ts +679 -0
- package/src/unnest-code-blocks.ts +168 -0
- package/src/upgrade.ts +127 -0
- package/src/utils.ts +145 -0
- package/src/voice-handler.ts +852 -0
- package/src/voice.test.ts +219 -0
- package/src/voice.ts +444 -0
- package/src/wait-session.ts +147 -0
- package/src/worker-types.ts +64 -0
- package/src/worktree-utils.ts +988 -0
- package/src/xml.test.ts +38 -0
- package/src/xml.ts +121 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// Permission button handler - Shows buttons for permission requests.
|
|
2
|
+
// When OpenCode asks for permission, this module renders 3 buttons:
|
|
3
|
+
// Accept, Accept Always, and Deny.
|
|
4
|
+
import { ButtonBuilder, ButtonStyle, ActionRowBuilder, } from 'discord.js';
|
|
5
|
+
import crypto from 'node:crypto';
|
|
6
|
+
import { getOpencodeClient } from '../opencode.js';
|
|
7
|
+
import { NOTIFY_MESSAGE_FLAGS } from '../discord-utils.js';
|
|
8
|
+
import { createLogger, LogPrefix } from '../logger.js';
|
|
9
|
+
const logger = createLogger(LogPrefix.PERMISSIONS);
|
|
10
|
+
function wildcardMatch({ value, pattern, }) {
|
|
11
|
+
let escapedPattern = pattern
|
|
12
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
13
|
+
.replace(/\*/g, '.*')
|
|
14
|
+
.replace(/\?/g, '.');
|
|
15
|
+
if (escapedPattern.endsWith(' .*')) {
|
|
16
|
+
escapedPattern = escapedPattern.slice(0, -3) + '( .*)?';
|
|
17
|
+
}
|
|
18
|
+
return new RegExp(`^${escapedPattern}$`, 's').test(value);
|
|
19
|
+
}
|
|
20
|
+
export function arePatternsCoveredBy({ patterns, coveringPatterns, }) {
|
|
21
|
+
return patterns.every((pattern) => {
|
|
22
|
+
return coveringPatterns.some((coveringPattern) => {
|
|
23
|
+
return wildcardMatch({ value: pattern, pattern: coveringPattern });
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
export function compactPermissionPatterns(patterns) {
|
|
28
|
+
const uniquePatterns = Array.from(new Set(patterns));
|
|
29
|
+
return uniquePatterns.filter((pattern, index) => {
|
|
30
|
+
return !uniquePatterns.some((candidate, candidateIndex) => {
|
|
31
|
+
if (candidateIndex === index) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
return wildcardMatch({ value: pattern, pattern: candidate });
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
// Store pending permission contexts by hash
|
|
39
|
+
export const pendingPermissionContexts = new Map();
|
|
40
|
+
/**
|
|
41
|
+
* Show permission buttons for a permission request.
|
|
42
|
+
* Displays 3 buttons in a row: Accept, Accept Always, Deny.
|
|
43
|
+
* Returns the message ID and context hash for tracking.
|
|
44
|
+
*/
|
|
45
|
+
export async function showPermissionButtons({ thread, permission, directory, permissionDirectory, subtaskLabel, }) {
|
|
46
|
+
const contextHash = crypto.randomBytes(8).toString('hex');
|
|
47
|
+
const context = {
|
|
48
|
+
permission,
|
|
49
|
+
requestIds: [permission.id],
|
|
50
|
+
directory,
|
|
51
|
+
permissionDirectory,
|
|
52
|
+
thread,
|
|
53
|
+
contextHash,
|
|
54
|
+
};
|
|
55
|
+
pendingPermissionContexts.set(contextHash, context);
|
|
56
|
+
const patternStr = compactPermissionPatterns(permission.patterns).join(', ');
|
|
57
|
+
// Build 3 buttons for permission actions
|
|
58
|
+
const acceptButton = new ButtonBuilder()
|
|
59
|
+
.setCustomId(`permission_once:${contextHash}`)
|
|
60
|
+
.setLabel('Accept')
|
|
61
|
+
.setStyle(ButtonStyle.Success);
|
|
62
|
+
const acceptAlwaysButton = new ButtonBuilder()
|
|
63
|
+
.setCustomId(`permission_always:${contextHash}`)
|
|
64
|
+
.setLabel('Accept Always')
|
|
65
|
+
.setStyle(ButtonStyle.Success);
|
|
66
|
+
const denyButton = new ButtonBuilder()
|
|
67
|
+
.setCustomId(`permission_reject:${contextHash}`)
|
|
68
|
+
.setLabel('Deny')
|
|
69
|
+
.setStyle(ButtonStyle.Secondary);
|
|
70
|
+
const actionRow = new ActionRowBuilder().addComponents(acceptButton, acceptAlwaysButton, denyButton);
|
|
71
|
+
const subtaskLine = subtaskLabel ? `**From:** \`${subtaskLabel}\`\n` : '';
|
|
72
|
+
const fullContent = `⚠️ **Permission Required**\n` +
|
|
73
|
+
subtaskLine +
|
|
74
|
+
`**Type:** \`${permission.permission}\`\n` +
|
|
75
|
+
(patternStr ? `**Pattern:** \`${patternStr}\`` : '');
|
|
76
|
+
const permissionMessage = await thread.send({
|
|
77
|
+
content: fullContent.slice(0, 1900),
|
|
78
|
+
components: [actionRow],
|
|
79
|
+
flags: NOTIFY_MESSAGE_FLAGS,
|
|
80
|
+
});
|
|
81
|
+
logger.log(`Showed permission buttons for ${permission.id}`);
|
|
82
|
+
return { messageId: permissionMessage.id, contextHash };
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Handle button click for permission.
|
|
86
|
+
*/
|
|
87
|
+
export async function handlePermissionButton(interaction) {
|
|
88
|
+
const customId = interaction.customId;
|
|
89
|
+
// Extract action and hash from customId (e.g., "permission_once:abc123")
|
|
90
|
+
const [actionPart, contextHash] = customId.split(':');
|
|
91
|
+
if (!actionPart || !contextHash) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const response = actionPart.replace('permission_', '');
|
|
95
|
+
const context = pendingPermissionContexts.get(contextHash);
|
|
96
|
+
if (!context) {
|
|
97
|
+
await interaction.update({ components: [] });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
await interaction.deferUpdate();
|
|
101
|
+
try {
|
|
102
|
+
const permClient = getOpencodeClient(context.directory);
|
|
103
|
+
if (!permClient) {
|
|
104
|
+
throw new Error('OpenCode server not found for directory');
|
|
105
|
+
}
|
|
106
|
+
const requestIds = context.requestIds.length > 0
|
|
107
|
+
? context.requestIds
|
|
108
|
+
: [context.permission.id];
|
|
109
|
+
await Promise.all(requestIds.map((requestId) => {
|
|
110
|
+
return permClient.permission.reply({
|
|
111
|
+
requestID: requestId,
|
|
112
|
+
directory: context.permissionDirectory,
|
|
113
|
+
reply: response,
|
|
114
|
+
});
|
|
115
|
+
}));
|
|
116
|
+
pendingPermissionContexts.delete(contextHash);
|
|
117
|
+
// Update message: show result and remove dropdown
|
|
118
|
+
const resultText = (() => {
|
|
119
|
+
switch (response) {
|
|
120
|
+
case 'once':
|
|
121
|
+
return '✅ Permission **accepted**';
|
|
122
|
+
case 'always':
|
|
123
|
+
return '✅ Permission **accepted** (auto-approve similar requests)';
|
|
124
|
+
case 'reject':
|
|
125
|
+
return '❌ Permission **rejected**';
|
|
126
|
+
}
|
|
127
|
+
})();
|
|
128
|
+
const patternStr = compactPermissionPatterns(context.permission.patterns).join(', ');
|
|
129
|
+
await interaction.editReply({
|
|
130
|
+
content: `⚠️ **Permission Required**\n` +
|
|
131
|
+
`**Type:** \`${context.permission.permission}\`\n` +
|
|
132
|
+
(patternStr ? `**Pattern:** \`${patternStr}\`\n` : '') +
|
|
133
|
+
resultText,
|
|
134
|
+
components: [], // Remove the buttons
|
|
135
|
+
});
|
|
136
|
+
logger.log(`Permission ${context.permission.id} ${response} (${requestIds.length} request(s))`);
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
logger.error('Error handling permission:', error);
|
|
140
|
+
await interaction.editReply({
|
|
141
|
+
content: `Failed to process permission: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
142
|
+
components: [],
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
export function addPermissionRequestToContext({ contextHash, requestId, }) {
|
|
147
|
+
const context = pendingPermissionContexts.get(contextHash);
|
|
148
|
+
if (!context) {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
if (context.requestIds.includes(requestId)) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
context.requestIds = [...context.requestIds, requestId];
|
|
155
|
+
pendingPermissionContexts.set(contextHash, context);
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Clean up a pending permission context (e.g., on auto-reject).
|
|
160
|
+
*/
|
|
161
|
+
export function cleanupPermissionContext(contextHash) {
|
|
162
|
+
pendingPermissionContexts.delete(contextHash);
|
|
163
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// Queue commands - /queue, /queue-command, /clear-queue
|
|
2
|
+
import { ChannelType, MessageFlags } from 'discord.js';
|
|
3
|
+
import { getThreadSession } from '../database.js';
|
|
4
|
+
import { resolveWorkingDirectory, sendThreadMessage, SILENT_MESSAGE_FLAGS, } from '../discord-utils.js';
|
|
5
|
+
import { handleOpencodeSession, abortControllers, addToQueue, getQueueLength, clearQueue, queueOrSendMessage, } from '../session-handler.js';
|
|
6
|
+
import { createLogger, LogPrefix } from '../logger.js';
|
|
7
|
+
import { notifyError } from '../sentry.js';
|
|
8
|
+
import { registeredUserCommands } from '../config.js';
|
|
9
|
+
const logger = createLogger(LogPrefix.QUEUE);
|
|
10
|
+
export async function handleQueueCommand({ command, appId, }) {
|
|
11
|
+
const message = command.options.getString('message', true);
|
|
12
|
+
const channel = command.channel;
|
|
13
|
+
if (!channel) {
|
|
14
|
+
await command.reply({
|
|
15
|
+
content: 'This command can only be used in a channel',
|
|
16
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
17
|
+
});
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const isThread = [
|
|
21
|
+
ChannelType.PublicThread,
|
|
22
|
+
ChannelType.PrivateThread,
|
|
23
|
+
ChannelType.AnnouncementThread,
|
|
24
|
+
].includes(channel.type);
|
|
25
|
+
if (!isThread) {
|
|
26
|
+
await command.reply({
|
|
27
|
+
content: 'This command can only be used in a thread with an active session',
|
|
28
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
29
|
+
});
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const result = await queueOrSendMessage({
|
|
33
|
+
thread: channel,
|
|
34
|
+
prompt: message,
|
|
35
|
+
userId: command.user.id,
|
|
36
|
+
username: command.user.displayName,
|
|
37
|
+
appId,
|
|
38
|
+
});
|
|
39
|
+
if (result.action === 'no-session') {
|
|
40
|
+
await command.reply({
|
|
41
|
+
content: 'No active session in this thread. Send a message directly instead.',
|
|
42
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
43
|
+
});
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (result.action === 'no-directory') {
|
|
47
|
+
await command.reply({
|
|
48
|
+
content: 'Could not determine project directory',
|
|
49
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
50
|
+
});
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (result.action === 'sent') {
|
|
54
|
+
await command.reply({
|
|
55
|
+
content: `» **${command.user.displayName}:** ${message.slice(0, 100)}${message.length > 100 ? '...' : ''}`,
|
|
56
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
57
|
+
});
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
await command.reply({
|
|
61
|
+
content: `Message queued (position: ${result.position}). Will be sent after current response.`,
|
|
62
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
export async function handleClearQueueCommand({ command, }) {
|
|
66
|
+
const channel = command.channel;
|
|
67
|
+
if (!channel) {
|
|
68
|
+
await command.reply({
|
|
69
|
+
content: 'This command can only be used in a channel',
|
|
70
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
71
|
+
});
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const isThread = [
|
|
75
|
+
ChannelType.PublicThread,
|
|
76
|
+
ChannelType.PrivateThread,
|
|
77
|
+
ChannelType.AnnouncementThread,
|
|
78
|
+
].includes(channel.type);
|
|
79
|
+
if (!isThread) {
|
|
80
|
+
await command.reply({
|
|
81
|
+
content: 'This command can only be used in a thread',
|
|
82
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
83
|
+
});
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const queueLength = getQueueLength(channel.id);
|
|
87
|
+
if (queueLength === 0) {
|
|
88
|
+
await command.reply({
|
|
89
|
+
content: 'No messages in queue',
|
|
90
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
91
|
+
});
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
clearQueue(channel.id);
|
|
95
|
+
await command.reply({
|
|
96
|
+
content: `🗑 Cleared ${queueLength} queued message${queueLength > 1 ? 's' : ''}`,
|
|
97
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
98
|
+
});
|
|
99
|
+
logger.log(`[QUEUE] User ${command.user.displayName} cleared queue in thread ${channel.id}`);
|
|
100
|
+
}
|
|
101
|
+
export async function handleQueueCommandCommand({ command, appId, }) {
|
|
102
|
+
const commandName = command.options.getString('command', true);
|
|
103
|
+
const args = command.options.getString('arguments') || '';
|
|
104
|
+
const channel = command.channel;
|
|
105
|
+
if (!channel) {
|
|
106
|
+
await command.reply({
|
|
107
|
+
content: 'This command can only be used in a channel',
|
|
108
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
109
|
+
});
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const isThread = [
|
|
113
|
+
ChannelType.PublicThread,
|
|
114
|
+
ChannelType.PrivateThread,
|
|
115
|
+
ChannelType.AnnouncementThread,
|
|
116
|
+
].includes(channel.type);
|
|
117
|
+
if (!isThread) {
|
|
118
|
+
await command.reply({
|
|
119
|
+
content: 'This command can only be used in a thread with an active session',
|
|
120
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
121
|
+
});
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const sessionId = await getThreadSession(channel.id);
|
|
125
|
+
if (!sessionId) {
|
|
126
|
+
await command.reply({
|
|
127
|
+
content: 'No active session in this thread. Send a message directly instead.',
|
|
128
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
129
|
+
});
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
// Validate command exists in registered user commands
|
|
133
|
+
const isKnownCommand = registeredUserCommands.some((cmd) => {
|
|
134
|
+
return cmd.name === commandName;
|
|
135
|
+
});
|
|
136
|
+
if (!isKnownCommand) {
|
|
137
|
+
await command.reply({
|
|
138
|
+
content: `Unknown command: /${commandName}. Use autocomplete to pick from available commands.`,
|
|
139
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
140
|
+
});
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const commandPayload = { name: commandName, arguments: args };
|
|
144
|
+
const displayText = `/${commandName}`;
|
|
145
|
+
// Check if there's an active request running
|
|
146
|
+
const existingController = abortControllers.get(sessionId);
|
|
147
|
+
const hasActiveRequest = Boolean(existingController && !existingController.signal.aborted);
|
|
148
|
+
if (existingController && existingController.signal.aborted) {
|
|
149
|
+
abortControllers.delete(sessionId);
|
|
150
|
+
}
|
|
151
|
+
if (!hasActiveRequest) {
|
|
152
|
+
const resolved = await resolveWorkingDirectory({
|
|
153
|
+
channel: channel,
|
|
154
|
+
});
|
|
155
|
+
if (!resolved) {
|
|
156
|
+
await command.reply({
|
|
157
|
+
content: 'Could not determine project directory',
|
|
158
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
159
|
+
});
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
await command.reply({
|
|
163
|
+
content: `» **${command.user.displayName}:** ${displayText}`,
|
|
164
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
165
|
+
});
|
|
166
|
+
logger.log(`[QUEUE] No active request, sending command immediately in thread ${channel.id}`);
|
|
167
|
+
handleOpencodeSession({
|
|
168
|
+
prompt: '',
|
|
169
|
+
thread: channel,
|
|
170
|
+
projectDirectory: resolved.projectDirectory,
|
|
171
|
+
channelId: channel.parentId || channel.id,
|
|
172
|
+
command: commandPayload,
|
|
173
|
+
appId,
|
|
174
|
+
}).catch(async (e) => {
|
|
175
|
+
logger.error(`[QUEUE] Failed to send command:`, e);
|
|
176
|
+
void notifyError(e, 'Queue: failed to send command');
|
|
177
|
+
const errorMsg = e instanceof Error ? e.message : String(e);
|
|
178
|
+
await sendThreadMessage(channel, `Failed: ${errorMsg.slice(0, 200)}`);
|
|
179
|
+
});
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
// Add to queue with command payload
|
|
183
|
+
const queuePosition = addToQueue({
|
|
184
|
+
threadId: channel.id,
|
|
185
|
+
message: {
|
|
186
|
+
prompt: '',
|
|
187
|
+
userId: command.user.id,
|
|
188
|
+
username: command.user.displayName,
|
|
189
|
+
queuedAt: Date.now(),
|
|
190
|
+
appId,
|
|
191
|
+
command: commandPayload,
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
await command.reply({
|
|
195
|
+
content: `Command queued (position: ${queuePosition}): ${displayText}`,
|
|
196
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
197
|
+
});
|
|
198
|
+
logger.log(`[QUEUE] User ${command.user.displayName} queued command /${commandName} in thread ${channel.id}`);
|
|
199
|
+
}
|
|
200
|
+
export async function handleQueueCommandAutocomplete({ interaction, }) {
|
|
201
|
+
const focused = interaction.options.getFocused(true);
|
|
202
|
+
if (focused.name !== 'command') {
|
|
203
|
+
await interaction.respond([]);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const query = focused.value.toLowerCase();
|
|
207
|
+
const choices = registeredUserCommands
|
|
208
|
+
.filter((cmd) => {
|
|
209
|
+
return cmd.name.toLowerCase().includes(query);
|
|
210
|
+
})
|
|
211
|
+
.slice(0, 25)
|
|
212
|
+
.map((cmd) => ({
|
|
213
|
+
name: `/${cmd.name} - ${cmd.description}`.slice(0, 100),
|
|
214
|
+
value: cmd.name.slice(0, 100),
|
|
215
|
+
}));
|
|
216
|
+
await interaction.respond(choices);
|
|
217
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// /remove-project command - Remove Discord channels for a project.
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import * as errore from 'errore';
|
|
4
|
+
import { findChannelsByDirectory, deleteChannelDirectoriesByDirectory, getAllTextChannelDirectories, } from '../database.js';
|
|
5
|
+
import { createLogger, LogPrefix } from '../logger.js';
|
|
6
|
+
import { abbreviatePath } from '../utils.js';
|
|
7
|
+
const logger = createLogger(LogPrefix.REMOVE_PROJECT);
|
|
8
|
+
export async function handleRemoveProjectCommand({ command, appId, }) {
|
|
9
|
+
await command.deferReply({ ephemeral: false });
|
|
10
|
+
const directory = command.options.getString('project', true);
|
|
11
|
+
const guild = command.guild;
|
|
12
|
+
if (!guild) {
|
|
13
|
+
await command.editReply('This command can only be used in a guild');
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
// Get channel IDs for this directory
|
|
18
|
+
const channels = await findChannelsByDirectory({ directory });
|
|
19
|
+
if (channels.length === 0) {
|
|
20
|
+
await command.editReply(`No channels found for directory: \`${directory}\``);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const deletedChannels = [];
|
|
24
|
+
const failedChannels = [];
|
|
25
|
+
for (const { channel_id, channel_type } of channels) {
|
|
26
|
+
const channel = await errore.tryAsync({
|
|
27
|
+
try: () => guild.channels.fetch(channel_id),
|
|
28
|
+
catch: (e) => e,
|
|
29
|
+
});
|
|
30
|
+
if (channel instanceof Error) {
|
|
31
|
+
logger.error(`Failed to fetch channel ${channel_id}:`, channel);
|
|
32
|
+
failedChannels.push(`${channel_type}: ${channel_id}`);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (channel) {
|
|
36
|
+
try {
|
|
37
|
+
await channel.delete(`Removed by /remove-project command`);
|
|
38
|
+
deletedChannels.push(`${channel_type}: ${channel_id}`);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
logger.error(`Failed to delete channel ${channel_id}:`, error);
|
|
42
|
+
failedChannels.push(`${channel_type}: ${channel_id}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
deletedChannels.push(`${channel_type}: ${channel_id} (already deleted)`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Remove from database
|
|
50
|
+
await deleteChannelDirectoriesByDirectory(directory);
|
|
51
|
+
const projectName = path.basename(directory);
|
|
52
|
+
let message = `Removed project **${projectName}**\n`;
|
|
53
|
+
message += `Directory: \`${directory}\`\n\n`;
|
|
54
|
+
if (deletedChannels.length > 0) {
|
|
55
|
+
message += `Deleted channels:\n${deletedChannels.map((c) => `- ${c}`).join('\n')}`;
|
|
56
|
+
}
|
|
57
|
+
if (failedChannels.length > 0) {
|
|
58
|
+
message += `\n\nFailed to delete (may be in another server):\n${failedChannels.map((c) => `- ${c}`).join('\n')}`;
|
|
59
|
+
}
|
|
60
|
+
await command.editReply(message);
|
|
61
|
+
logger.log(`Removed project ${projectName} at ${directory}`);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
logger.error('[REMOVE-PROJECT] Error:', error);
|
|
65
|
+
await command.editReply(`Failed to remove project: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export async function handleRemoveProjectAutocomplete({ interaction, appId, }) {
|
|
69
|
+
const focusedValue = interaction.options.getFocused();
|
|
70
|
+
const guild = interaction.guild;
|
|
71
|
+
if (!guild) {
|
|
72
|
+
await interaction.respond([]);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
// Get all directories with channels
|
|
77
|
+
const allChannels = (await findChannelsByDirectory({
|
|
78
|
+
channelType: 'text',
|
|
79
|
+
}));
|
|
80
|
+
// Filter to only channels that exist in this guild
|
|
81
|
+
const projectsInGuild = [];
|
|
82
|
+
for (const { directory, channel_id } of allChannels) {
|
|
83
|
+
const channel = await errore.tryAsync({
|
|
84
|
+
try: () => guild.channels.fetch(channel_id),
|
|
85
|
+
catch: (e) => e,
|
|
86
|
+
});
|
|
87
|
+
if (channel instanceof Error) {
|
|
88
|
+
// Channel not in this guild, skip
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (channel) {
|
|
92
|
+
projectsInGuild.push({ directory, channelId: channel_id });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const projects = projectsInGuild
|
|
96
|
+
.filter(({ directory }) => {
|
|
97
|
+
const baseName = path.basename(directory);
|
|
98
|
+
const searchText = `${baseName} ${directory}`.toLowerCase();
|
|
99
|
+
return searchText.includes(focusedValue.toLowerCase());
|
|
100
|
+
})
|
|
101
|
+
.slice(0, 25)
|
|
102
|
+
.map(({ directory }) => {
|
|
103
|
+
const name = `${path.basename(directory)} (${abbreviatePath(directory)})`;
|
|
104
|
+
return {
|
|
105
|
+
name: name.length > 100 ? name.slice(0, 99) + '...' : name,
|
|
106
|
+
value: directory,
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
await interaction.respond(projects);
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
logger.error('[AUTOCOMPLETE] Error fetching projects:', error);
|
|
113
|
+
await interaction.respond([]);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// /restart-opencode-server command - Restart the opencode server for the current channel.
|
|
2
|
+
// Used for resolving opencode state issues, internal bugs, refreshing auth state, plugins, etc.
|
|
3
|
+
// Aborts all in-progress sessions in this channel before restarting to avoid orphaned requests.
|
|
4
|
+
import { ChannelType, MessageFlags, } from 'discord.js';
|
|
5
|
+
import { initializeOpencodeForDirectory, restartOpencodeServer, } from '../opencode.js';
|
|
6
|
+
import { resolveWorkingDirectory, SILENT_MESSAGE_FLAGS, } from '../discord-utils.js';
|
|
7
|
+
import { createLogger, LogPrefix } from '../logger.js';
|
|
8
|
+
import { getAllThreadSessionIds, getThreadIdBySessionId } from '../database.js';
|
|
9
|
+
import { abortControllers } from '../session-handler.js';
|
|
10
|
+
import { SessionAbortError } from '../errors.js';
|
|
11
|
+
import * as errore from 'errore';
|
|
12
|
+
const logger = createLogger(LogPrefix.OPENCODE);
|
|
13
|
+
export async function handleRestartOpencodeServerCommand({ command, appId, }) {
|
|
14
|
+
const channel = command.channel;
|
|
15
|
+
if (!channel) {
|
|
16
|
+
await command.reply({
|
|
17
|
+
content: 'This command can only be used in a channel',
|
|
18
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
19
|
+
});
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const isThread = [
|
|
23
|
+
ChannelType.PublicThread,
|
|
24
|
+
ChannelType.PrivateThread,
|
|
25
|
+
ChannelType.AnnouncementThread,
|
|
26
|
+
].includes(channel.type);
|
|
27
|
+
const isTextChannel = channel.type === ChannelType.GuildText;
|
|
28
|
+
if (!isThread && !isTextChannel) {
|
|
29
|
+
await command.reply({
|
|
30
|
+
content: 'This command can only be used in text channels or threads',
|
|
31
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
32
|
+
});
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const resolved = await resolveWorkingDirectory({
|
|
36
|
+
channel: channel,
|
|
37
|
+
});
|
|
38
|
+
if (!resolved) {
|
|
39
|
+
await command.reply({
|
|
40
|
+
content: 'Could not determine project directory for this channel',
|
|
41
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
42
|
+
});
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const { projectDirectory, channelAppId } = resolved;
|
|
46
|
+
if (channelAppId && channelAppId !== appId) {
|
|
47
|
+
await command.reply({
|
|
48
|
+
content: 'This channel is not configured for this bot',
|
|
49
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
50
|
+
});
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
// Defer reply since restart may take a moment
|
|
54
|
+
await command.deferReply({ flags: SILENT_MESSAGE_FLAGS });
|
|
55
|
+
// Abort all in-progress sessions in this channel before restarting.
|
|
56
|
+
// Find sessions with active abort controllers, check if their thread belongs
|
|
57
|
+
// to this channel (thread parentId matches, or command was run in the thread itself).
|
|
58
|
+
const parentChannelId = isThread
|
|
59
|
+
? channel.parentId
|
|
60
|
+
: channel.id;
|
|
61
|
+
const activeSessionIds = [...abortControllers.keys()];
|
|
62
|
+
let abortedCount = 0;
|
|
63
|
+
if (activeSessionIds.length > 0) {
|
|
64
|
+
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
65
|
+
const client = !(getClient instanceof Error) ? getClient : null;
|
|
66
|
+
for (const sessionId of activeSessionIds) {
|
|
67
|
+
const threadId = await getThreadIdBySessionId(sessionId);
|
|
68
|
+
if (!threadId) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
// Check if thread belongs to this channel: either the thread IS this channel,
|
|
72
|
+
// or the thread's parent matches the parent channel
|
|
73
|
+
const threadChannel = await errore.tryAsync(() => {
|
|
74
|
+
return command.client.channels.fetch(threadId);
|
|
75
|
+
});
|
|
76
|
+
if (threadChannel instanceof Error || !threadChannel) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const threadParentId = 'parentId' in threadChannel ? threadChannel.parentId : null;
|
|
80
|
+
if (threadId !== channel.id && threadParentId !== parentChannelId) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const controller = abortControllers.get(sessionId);
|
|
84
|
+
if (controller) {
|
|
85
|
+
logger.log(`[RESTART] Aborting session ${sessionId} in thread ${threadId}`);
|
|
86
|
+
controller.abort(new SessionAbortError({ reason: 'server-restart' }));
|
|
87
|
+
abortControllers.delete(sessionId);
|
|
88
|
+
abortedCount++;
|
|
89
|
+
}
|
|
90
|
+
if (client) {
|
|
91
|
+
await errore.tryAsync(() => {
|
|
92
|
+
return client().session.abort({ sessionID: sessionId });
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (abortedCount > 0) {
|
|
98
|
+
logger.log(`[RESTART] Aborted ${abortedCount} active session(s) before restart`);
|
|
99
|
+
}
|
|
100
|
+
logger.log(`[RESTART] Restarting opencode server for directory: ${projectDirectory}`);
|
|
101
|
+
const result = await restartOpencodeServer(projectDirectory);
|
|
102
|
+
if (result instanceof Error) {
|
|
103
|
+
logger.error('[RESTART] Failed:', result);
|
|
104
|
+
await command.editReply({
|
|
105
|
+
content: `Failed to restart opencode server: ${result.message}`,
|
|
106
|
+
});
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const abortMsg = abortedCount > 0
|
|
110
|
+
? ` (aborted ${abortedCount} active session${abortedCount > 1 ? 's' : ''})`
|
|
111
|
+
: '';
|
|
112
|
+
await command.editReply({
|
|
113
|
+
content: `Opencode server **restarted** successfully${abortMsg}`,
|
|
114
|
+
});
|
|
115
|
+
logger.log(`[RESTART] Opencode server restarted for directory: ${projectDirectory}`);
|
|
116
|
+
}
|