@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,261 @@
|
|
|
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
|
+
|
|
5
|
+
import {
|
|
6
|
+
ButtonBuilder,
|
|
7
|
+
ButtonStyle,
|
|
8
|
+
type ButtonInteraction,
|
|
9
|
+
ActionRowBuilder,
|
|
10
|
+
type ThreadChannel,
|
|
11
|
+
} from 'discord.js'
|
|
12
|
+
import crypto from 'node:crypto'
|
|
13
|
+
import type { PermissionRequest } from '@opencode-ai/sdk/v2'
|
|
14
|
+
import { getOpencodeClient } from '../opencode.js'
|
|
15
|
+
import { NOTIFY_MESSAGE_FLAGS } from '../discord-utils.js'
|
|
16
|
+
import { createLogger, LogPrefix } from '../logger.js'
|
|
17
|
+
|
|
18
|
+
const logger = createLogger(LogPrefix.PERMISSIONS)
|
|
19
|
+
|
|
20
|
+
function wildcardMatch({
|
|
21
|
+
value,
|
|
22
|
+
pattern,
|
|
23
|
+
}: {
|
|
24
|
+
value: string
|
|
25
|
+
pattern: string
|
|
26
|
+
}): boolean {
|
|
27
|
+
let escapedPattern = pattern
|
|
28
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
29
|
+
.replace(/\*/g, '.*')
|
|
30
|
+
.replace(/\?/g, '.')
|
|
31
|
+
|
|
32
|
+
if (escapedPattern.endsWith(' .*')) {
|
|
33
|
+
escapedPattern = escapedPattern.slice(0, -3) + '( .*)?'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return new RegExp(`^${escapedPattern}$`, 's').test(value)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function arePatternsCoveredBy({
|
|
40
|
+
patterns,
|
|
41
|
+
coveringPatterns,
|
|
42
|
+
}: {
|
|
43
|
+
patterns: string[]
|
|
44
|
+
coveringPatterns: string[]
|
|
45
|
+
}): boolean {
|
|
46
|
+
return patterns.every((pattern) => {
|
|
47
|
+
return coveringPatterns.some((coveringPattern) => {
|
|
48
|
+
return wildcardMatch({ value: pattern, pattern: coveringPattern })
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function compactPermissionPatterns(patterns: string[]): string[] {
|
|
54
|
+
const uniquePatterns = Array.from(new Set(patterns))
|
|
55
|
+
return uniquePatterns.filter((pattern, index) => {
|
|
56
|
+
return !uniquePatterns.some((candidate, candidateIndex) => {
|
|
57
|
+
if (candidateIndex === index) {
|
|
58
|
+
return false
|
|
59
|
+
}
|
|
60
|
+
return wildcardMatch({ value: pattern, pattern: candidate })
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
type PendingPermissionContext = {
|
|
66
|
+
permission: PermissionRequest
|
|
67
|
+
requestIds: string[]
|
|
68
|
+
directory: string
|
|
69
|
+
permissionDirectory: string
|
|
70
|
+
thread: ThreadChannel
|
|
71
|
+
contextHash: string
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Store pending permission contexts by hash
|
|
75
|
+
export const pendingPermissionContexts = new Map<
|
|
76
|
+
string,
|
|
77
|
+
PendingPermissionContext
|
|
78
|
+
>()
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Show permission buttons for a permission request.
|
|
82
|
+
* Displays 3 buttons in a row: Accept, Accept Always, Deny.
|
|
83
|
+
* Returns the message ID and context hash for tracking.
|
|
84
|
+
*/
|
|
85
|
+
export async function showPermissionButtons({
|
|
86
|
+
thread,
|
|
87
|
+
permission,
|
|
88
|
+
directory,
|
|
89
|
+
permissionDirectory,
|
|
90
|
+
subtaskLabel,
|
|
91
|
+
}: {
|
|
92
|
+
thread: ThreadChannel
|
|
93
|
+
permission: PermissionRequest
|
|
94
|
+
directory: string
|
|
95
|
+
permissionDirectory: string
|
|
96
|
+
subtaskLabel?: string
|
|
97
|
+
}): Promise<{ messageId: string; contextHash: string }> {
|
|
98
|
+
const contextHash = crypto.randomBytes(8).toString('hex')
|
|
99
|
+
|
|
100
|
+
const context: PendingPermissionContext = {
|
|
101
|
+
permission,
|
|
102
|
+
requestIds: [permission.id],
|
|
103
|
+
directory,
|
|
104
|
+
permissionDirectory,
|
|
105
|
+
thread,
|
|
106
|
+
contextHash,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
pendingPermissionContexts.set(contextHash, context)
|
|
110
|
+
|
|
111
|
+
const patternStr = compactPermissionPatterns(permission.patterns).join(', ')
|
|
112
|
+
|
|
113
|
+
// Build 3 buttons for permission actions
|
|
114
|
+
const acceptButton = new ButtonBuilder()
|
|
115
|
+
.setCustomId(`permission_once:${contextHash}`)
|
|
116
|
+
.setLabel('Accept')
|
|
117
|
+
.setStyle(ButtonStyle.Success)
|
|
118
|
+
|
|
119
|
+
const acceptAlwaysButton = new ButtonBuilder()
|
|
120
|
+
.setCustomId(`permission_always:${contextHash}`)
|
|
121
|
+
.setLabel('Accept Always')
|
|
122
|
+
.setStyle(ButtonStyle.Success)
|
|
123
|
+
|
|
124
|
+
const denyButton = new ButtonBuilder()
|
|
125
|
+
.setCustomId(`permission_reject:${contextHash}`)
|
|
126
|
+
.setLabel('Deny')
|
|
127
|
+
.setStyle(ButtonStyle.Secondary)
|
|
128
|
+
|
|
129
|
+
const actionRow = new ActionRowBuilder<ButtonBuilder>().addComponents(
|
|
130
|
+
acceptButton,
|
|
131
|
+
acceptAlwaysButton,
|
|
132
|
+
denyButton,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
const subtaskLine = subtaskLabel ? `**From:** \`${subtaskLabel}\`\n` : ''
|
|
136
|
+
const fullContent =
|
|
137
|
+
`⚠️ **Permission Required**\n` +
|
|
138
|
+
subtaskLine +
|
|
139
|
+
`**Type:** \`${permission.permission}\`\n` +
|
|
140
|
+
(patternStr ? `**Pattern:** \`${patternStr}\`` : '')
|
|
141
|
+
const permissionMessage = await thread.send({
|
|
142
|
+
content: fullContent.slice(0, 1900),
|
|
143
|
+
components: [actionRow],
|
|
144
|
+
flags: NOTIFY_MESSAGE_FLAGS,
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
logger.log(`Showed permission buttons for ${permission.id}`)
|
|
148
|
+
|
|
149
|
+
return { messageId: permissionMessage.id, contextHash }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Handle button click for permission.
|
|
154
|
+
*/
|
|
155
|
+
export async function handlePermissionButton(
|
|
156
|
+
interaction: ButtonInteraction,
|
|
157
|
+
): Promise<void> {
|
|
158
|
+
const customId = interaction.customId
|
|
159
|
+
|
|
160
|
+
// Extract action and hash from customId (e.g., "permission_once:abc123")
|
|
161
|
+
const [actionPart, contextHash] = customId.split(':')
|
|
162
|
+
if (!actionPart || !contextHash) {
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const response = actionPart.replace('permission_', '') as
|
|
167
|
+
| 'once'
|
|
168
|
+
| 'always'
|
|
169
|
+
| 'reject'
|
|
170
|
+
|
|
171
|
+
const context = pendingPermissionContexts.get(contextHash)
|
|
172
|
+
|
|
173
|
+
if (!context) {
|
|
174
|
+
await interaction.update({ components: [] })
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
await interaction.deferUpdate()
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
const permClient = getOpencodeClient(context.directory)
|
|
182
|
+
if (!permClient) {
|
|
183
|
+
throw new Error('OpenCode server not found for directory')
|
|
184
|
+
}
|
|
185
|
+
const requestIds =
|
|
186
|
+
context.requestIds.length > 0
|
|
187
|
+
? context.requestIds
|
|
188
|
+
: [context.permission.id]
|
|
189
|
+
await Promise.all(
|
|
190
|
+
requestIds.map((requestId) => {
|
|
191
|
+
return permClient.permission.reply({
|
|
192
|
+
requestID: requestId,
|
|
193
|
+
directory: context.permissionDirectory,
|
|
194
|
+
reply: response,
|
|
195
|
+
})
|
|
196
|
+
}),
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
pendingPermissionContexts.delete(contextHash)
|
|
200
|
+
|
|
201
|
+
// Update message: show result and remove dropdown
|
|
202
|
+
const resultText = (() => {
|
|
203
|
+
switch (response) {
|
|
204
|
+
case 'once':
|
|
205
|
+
return '✅ Permission **accepted**'
|
|
206
|
+
case 'always':
|
|
207
|
+
return '✅ Permission **accepted** (auto-approve similar requests)'
|
|
208
|
+
case 'reject':
|
|
209
|
+
return '❌ Permission **rejected**'
|
|
210
|
+
}
|
|
211
|
+
})()
|
|
212
|
+
|
|
213
|
+
const patternStr = compactPermissionPatterns(
|
|
214
|
+
context.permission.patterns,
|
|
215
|
+
).join(', ')
|
|
216
|
+
await interaction.editReply({
|
|
217
|
+
content:
|
|
218
|
+
`⚠️ **Permission Required**\n` +
|
|
219
|
+
`**Type:** \`${context.permission.permission}\`\n` +
|
|
220
|
+
(patternStr ? `**Pattern:** \`${patternStr}\`\n` : '') +
|
|
221
|
+
resultText,
|
|
222
|
+
components: [], // Remove the buttons
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
logger.log(
|
|
226
|
+
`Permission ${context.permission.id} ${response} (${requestIds.length} request(s))`,
|
|
227
|
+
)
|
|
228
|
+
} catch (error) {
|
|
229
|
+
logger.error('Error handling permission:', error)
|
|
230
|
+
await interaction.editReply({
|
|
231
|
+
content: `Failed to process permission: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
232
|
+
components: [],
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function addPermissionRequestToContext({
|
|
238
|
+
contextHash,
|
|
239
|
+
requestId,
|
|
240
|
+
}: {
|
|
241
|
+
contextHash: string
|
|
242
|
+
requestId: string
|
|
243
|
+
}): boolean {
|
|
244
|
+
const context = pendingPermissionContexts.get(contextHash)
|
|
245
|
+
if (!context) {
|
|
246
|
+
return false
|
|
247
|
+
}
|
|
248
|
+
if (context.requestIds.includes(requestId)) {
|
|
249
|
+
return false
|
|
250
|
+
}
|
|
251
|
+
context.requestIds = [...context.requestIds, requestId]
|
|
252
|
+
pendingPermissionContexts.set(contextHash, context)
|
|
253
|
+
return true
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Clean up a pending permission context (e.g., on auto-reject).
|
|
258
|
+
*/
|
|
259
|
+
export function cleanupPermissionContext(contextHash: string): void {
|
|
260
|
+
pendingPermissionContexts.delete(contextHash)
|
|
261
|
+
}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
// Queue commands - /queue, /queue-command, /clear-queue
|
|
2
|
+
|
|
3
|
+
import { ChannelType, MessageFlags, type ThreadChannel } from 'discord.js'
|
|
4
|
+
import type { AutocompleteContext, CommandContext } from './types.js'
|
|
5
|
+
import { getThreadSession } from '../database.js'
|
|
6
|
+
import {
|
|
7
|
+
resolveWorkingDirectory,
|
|
8
|
+
sendThreadMessage,
|
|
9
|
+
SILENT_MESSAGE_FLAGS,
|
|
10
|
+
} from '../discord-utils.js'
|
|
11
|
+
import {
|
|
12
|
+
handleOpencodeSession,
|
|
13
|
+
abortControllers,
|
|
14
|
+
addToQueue,
|
|
15
|
+
getQueueLength,
|
|
16
|
+
clearQueue,
|
|
17
|
+
queueOrSendMessage,
|
|
18
|
+
} from '../session-handler.js'
|
|
19
|
+
import { createLogger, LogPrefix } from '../logger.js'
|
|
20
|
+
import { notifyError } from '../sentry.js'
|
|
21
|
+
import { registeredUserCommands } from '../config.js'
|
|
22
|
+
|
|
23
|
+
const logger = createLogger(LogPrefix.QUEUE)
|
|
24
|
+
|
|
25
|
+
export async function handleQueueCommand({
|
|
26
|
+
command,
|
|
27
|
+
appId,
|
|
28
|
+
}: CommandContext): Promise<void> {
|
|
29
|
+
const message = command.options.getString('message', true)
|
|
30
|
+
const channel = command.channel
|
|
31
|
+
|
|
32
|
+
if (!channel) {
|
|
33
|
+
await command.reply({
|
|
34
|
+
content: 'This command can only be used in a channel',
|
|
35
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
36
|
+
})
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const isThread = [
|
|
41
|
+
ChannelType.PublicThread,
|
|
42
|
+
ChannelType.PrivateThread,
|
|
43
|
+
ChannelType.AnnouncementThread,
|
|
44
|
+
].includes(channel.type)
|
|
45
|
+
|
|
46
|
+
if (!isThread) {
|
|
47
|
+
await command.reply({
|
|
48
|
+
content:
|
|
49
|
+
'This command can only be used in a thread with an active session',
|
|
50
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
51
|
+
})
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const result = await queueOrSendMessage({
|
|
56
|
+
thread: channel as ThreadChannel,
|
|
57
|
+
prompt: message,
|
|
58
|
+
userId: command.user.id,
|
|
59
|
+
username: command.user.displayName,
|
|
60
|
+
appId,
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
if (result.action === 'no-session') {
|
|
64
|
+
await command.reply({
|
|
65
|
+
content:
|
|
66
|
+
'No active session in this thread. Send a message directly instead.',
|
|
67
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
68
|
+
})
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (result.action === 'no-directory') {
|
|
73
|
+
await command.reply({
|
|
74
|
+
content: 'Could not determine project directory',
|
|
75
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
76
|
+
})
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (result.action === 'sent') {
|
|
81
|
+
await command.reply({
|
|
82
|
+
content: `» **${command.user.displayName}:** ${message.slice(0, 100)}${message.length > 100 ? '...' : ''}`,
|
|
83
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
84
|
+
})
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
await command.reply({
|
|
89
|
+
content: `Message queued (position: ${result.position}). Will be sent after current response.`,
|
|
90
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function handleClearQueueCommand({
|
|
95
|
+
command,
|
|
96
|
+
}: CommandContext): Promise<void> {
|
|
97
|
+
const channel = command.channel
|
|
98
|
+
|
|
99
|
+
if (!channel) {
|
|
100
|
+
await command.reply({
|
|
101
|
+
content: 'This command can only be used in a channel',
|
|
102
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
103
|
+
})
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const isThread = [
|
|
108
|
+
ChannelType.PublicThread,
|
|
109
|
+
ChannelType.PrivateThread,
|
|
110
|
+
ChannelType.AnnouncementThread,
|
|
111
|
+
].includes(channel.type)
|
|
112
|
+
|
|
113
|
+
if (!isThread) {
|
|
114
|
+
await command.reply({
|
|
115
|
+
content: 'This command can only be used in a thread',
|
|
116
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
117
|
+
})
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const queueLength = getQueueLength(channel.id)
|
|
122
|
+
|
|
123
|
+
if (queueLength === 0) {
|
|
124
|
+
await command.reply({
|
|
125
|
+
content: 'No messages in queue',
|
|
126
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
127
|
+
})
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
clearQueue(channel.id)
|
|
132
|
+
|
|
133
|
+
await command.reply({
|
|
134
|
+
content: `🗑 Cleared ${queueLength} queued message${queueLength > 1 ? 's' : ''}`,
|
|
135
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
logger.log(
|
|
139
|
+
`[QUEUE] User ${command.user.displayName} cleared queue in thread ${channel.id}`,
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function handleQueueCommandCommand({
|
|
144
|
+
command,
|
|
145
|
+
appId,
|
|
146
|
+
}: CommandContext): Promise<void> {
|
|
147
|
+
const commandName = command.options.getString('command', true)
|
|
148
|
+
const args = command.options.getString('arguments') || ''
|
|
149
|
+
const channel = command.channel
|
|
150
|
+
|
|
151
|
+
if (!channel) {
|
|
152
|
+
await command.reply({
|
|
153
|
+
content: 'This command can only be used in a channel',
|
|
154
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
155
|
+
})
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const isThread = [
|
|
160
|
+
ChannelType.PublicThread,
|
|
161
|
+
ChannelType.PrivateThread,
|
|
162
|
+
ChannelType.AnnouncementThread,
|
|
163
|
+
].includes(channel.type)
|
|
164
|
+
|
|
165
|
+
if (!isThread) {
|
|
166
|
+
await command.reply({
|
|
167
|
+
content:
|
|
168
|
+
'This command can only be used in a thread with an active session',
|
|
169
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
170
|
+
})
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const sessionId = await getThreadSession(channel.id)
|
|
175
|
+
|
|
176
|
+
if (!sessionId) {
|
|
177
|
+
await command.reply({
|
|
178
|
+
content:
|
|
179
|
+
'No active session in this thread. Send a message directly instead.',
|
|
180
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
181
|
+
})
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Validate command exists in registered user commands
|
|
186
|
+
const isKnownCommand = registeredUserCommands.some((cmd) => {
|
|
187
|
+
return cmd.name === commandName
|
|
188
|
+
})
|
|
189
|
+
if (!isKnownCommand) {
|
|
190
|
+
await command.reply({
|
|
191
|
+
content: `Unknown command: /${commandName}. Use autocomplete to pick from available commands.`,
|
|
192
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
193
|
+
})
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const commandPayload = { name: commandName, arguments: args }
|
|
198
|
+
const displayText = `/${commandName}`
|
|
199
|
+
|
|
200
|
+
// Check if there's an active request running
|
|
201
|
+
const existingController = abortControllers.get(sessionId)
|
|
202
|
+
const hasActiveRequest = Boolean(
|
|
203
|
+
existingController && !existingController.signal.aborted,
|
|
204
|
+
)
|
|
205
|
+
if (existingController && existingController.signal.aborted) {
|
|
206
|
+
abortControllers.delete(sessionId)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (!hasActiveRequest) {
|
|
210
|
+
const resolved = await resolveWorkingDirectory({
|
|
211
|
+
channel: channel as ThreadChannel,
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
if (!resolved) {
|
|
215
|
+
await command.reply({
|
|
216
|
+
content: 'Could not determine project directory',
|
|
217
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
218
|
+
})
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
await command.reply({
|
|
223
|
+
content: `» **${command.user.displayName}:** ${displayText}`,
|
|
224
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
logger.log(
|
|
228
|
+
`[QUEUE] No active request, sending command immediately in thread ${channel.id}`,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
handleOpencodeSession({
|
|
232
|
+
prompt: '',
|
|
233
|
+
thread: channel as ThreadChannel,
|
|
234
|
+
projectDirectory: resolved.projectDirectory,
|
|
235
|
+
channelId: (channel as ThreadChannel).parentId || channel.id,
|
|
236
|
+
command: commandPayload,
|
|
237
|
+
appId,
|
|
238
|
+
}).catch(async (e) => {
|
|
239
|
+
logger.error(`[QUEUE] Failed to send command:`, e)
|
|
240
|
+
void notifyError(e, 'Queue: failed to send command')
|
|
241
|
+
const errorMsg = e instanceof Error ? e.message : String(e)
|
|
242
|
+
await sendThreadMessage(
|
|
243
|
+
channel as ThreadChannel,
|
|
244
|
+
`Failed: ${errorMsg.slice(0, 200)}`,
|
|
245
|
+
)
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
return
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Add to queue with command payload
|
|
252
|
+
const queuePosition = addToQueue({
|
|
253
|
+
threadId: channel.id,
|
|
254
|
+
message: {
|
|
255
|
+
prompt: '',
|
|
256
|
+
userId: command.user.id,
|
|
257
|
+
username: command.user.displayName,
|
|
258
|
+
queuedAt: Date.now(),
|
|
259
|
+
appId,
|
|
260
|
+
command: commandPayload,
|
|
261
|
+
},
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
await command.reply({
|
|
265
|
+
content: `Command queued (position: ${queuePosition}): ${displayText}`,
|
|
266
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
logger.log(
|
|
270
|
+
`[QUEUE] User ${command.user.displayName} queued command /${commandName} in thread ${channel.id}`,
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export async function handleQueueCommandAutocomplete({
|
|
275
|
+
interaction,
|
|
276
|
+
}: AutocompleteContext): Promise<void> {
|
|
277
|
+
const focused = interaction.options.getFocused(true)
|
|
278
|
+
|
|
279
|
+
if (focused.name !== 'command') {
|
|
280
|
+
await interaction.respond([])
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const query = focused.value.toLowerCase()
|
|
285
|
+
const choices = registeredUserCommands
|
|
286
|
+
.filter((cmd) => {
|
|
287
|
+
return cmd.name.toLowerCase().includes(query)
|
|
288
|
+
})
|
|
289
|
+
.slice(0, 25)
|
|
290
|
+
.map((cmd) => ({
|
|
291
|
+
name: `/${cmd.name} - ${cmd.description}`.slice(0, 100),
|
|
292
|
+
value: cmd.name.slice(0, 100),
|
|
293
|
+
}))
|
|
294
|
+
|
|
295
|
+
await interaction.respond(choices)
|
|
296
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// /remove-project command - Remove Discord channels for a project.
|
|
2
|
+
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import * as errore from 'errore'
|
|
5
|
+
import type { CommandContext, AutocompleteContext } from './types.js'
|
|
6
|
+
import {
|
|
7
|
+
findChannelsByDirectory,
|
|
8
|
+
deleteChannelDirectoriesByDirectory,
|
|
9
|
+
getAllTextChannelDirectories,
|
|
10
|
+
} from '../database.js'
|
|
11
|
+
import { createLogger, LogPrefix } from '../logger.js'
|
|
12
|
+
import { abbreviatePath } from '../utils.js'
|
|
13
|
+
|
|
14
|
+
const logger = createLogger(LogPrefix.REMOVE_PROJECT)
|
|
15
|
+
|
|
16
|
+
export async function handleRemoveProjectCommand({
|
|
17
|
+
command,
|
|
18
|
+
appId,
|
|
19
|
+
}: CommandContext): Promise<void> {
|
|
20
|
+
await command.deferReply({ ephemeral: false })
|
|
21
|
+
|
|
22
|
+
const directory = command.options.getString('project', true)
|
|
23
|
+
const guild = command.guild
|
|
24
|
+
|
|
25
|
+
if (!guild) {
|
|
26
|
+
await command.editReply('This command can only be used in a guild')
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
// Get channel IDs for this directory
|
|
32
|
+
const channels = await findChannelsByDirectory({ directory })
|
|
33
|
+
|
|
34
|
+
if (channels.length === 0) {
|
|
35
|
+
await command.editReply(
|
|
36
|
+
`No channels found for directory: \`${directory}\``,
|
|
37
|
+
)
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const deletedChannels: string[] = []
|
|
42
|
+
const failedChannels: string[] = []
|
|
43
|
+
|
|
44
|
+
for (const { channel_id, channel_type } of channels as Array<{
|
|
45
|
+
channel_id: string
|
|
46
|
+
channel_type: string
|
|
47
|
+
}>) {
|
|
48
|
+
const channel = await errore.tryAsync({
|
|
49
|
+
try: () => guild.channels.fetch(channel_id),
|
|
50
|
+
catch: (e) => e as Error,
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
if (channel instanceof Error) {
|
|
54
|
+
logger.error(`Failed to fetch channel ${channel_id}:`, channel)
|
|
55
|
+
failedChannels.push(`${channel_type}: ${channel_id}`)
|
|
56
|
+
continue
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (channel) {
|
|
60
|
+
try {
|
|
61
|
+
await channel.delete(`Removed by /remove-project command`)
|
|
62
|
+
deletedChannels.push(`${channel_type}: ${channel_id}`)
|
|
63
|
+
} catch (error) {
|
|
64
|
+
logger.error(`Failed to delete channel ${channel_id}:`, error)
|
|
65
|
+
failedChannels.push(`${channel_type}: ${channel_id}`)
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
deletedChannels.push(`${channel_type}: ${channel_id} (already deleted)`)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Remove from database
|
|
73
|
+
await deleteChannelDirectoriesByDirectory(directory)
|
|
74
|
+
|
|
75
|
+
const projectName = path.basename(directory)
|
|
76
|
+
let message = `Removed project **${projectName}**\n`
|
|
77
|
+
message += `Directory: \`${directory}\`\n\n`
|
|
78
|
+
|
|
79
|
+
if (deletedChannels.length > 0) {
|
|
80
|
+
message += `Deleted channels:\n${deletedChannels.map((c) => `- ${c}`).join('\n')}`
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (failedChannels.length > 0) {
|
|
84
|
+
message += `\n\nFailed to delete (may be in another server):\n${failedChannels.map((c) => `- ${c}`).join('\n')}`
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
await command.editReply(message)
|
|
88
|
+
logger.log(`Removed project ${projectName} at ${directory}`)
|
|
89
|
+
} catch (error) {
|
|
90
|
+
logger.error('[REMOVE-PROJECT] Error:', error)
|
|
91
|
+
await command.editReply(
|
|
92
|
+
`Failed to remove project: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function handleRemoveProjectAutocomplete({
|
|
98
|
+
interaction,
|
|
99
|
+
appId,
|
|
100
|
+
}: AutocompleteContext): Promise<void> {
|
|
101
|
+
const focusedValue = interaction.options.getFocused()
|
|
102
|
+
const guild = interaction.guild
|
|
103
|
+
|
|
104
|
+
if (!guild) {
|
|
105
|
+
await interaction.respond([])
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
// Get all directories with channels
|
|
111
|
+
const allChannels = (await findChannelsByDirectory({
|
|
112
|
+
channelType: 'text',
|
|
113
|
+
})) as Array<{
|
|
114
|
+
directory: string
|
|
115
|
+
channel_id: string
|
|
116
|
+
}>
|
|
117
|
+
|
|
118
|
+
// Filter to only channels that exist in this guild
|
|
119
|
+
const projectsInGuild: { directory: string; channelId: string }[] = []
|
|
120
|
+
|
|
121
|
+
for (const { directory, channel_id } of allChannels) {
|
|
122
|
+
const channel = await errore.tryAsync({
|
|
123
|
+
try: () => guild.channels.fetch(channel_id),
|
|
124
|
+
catch: (e) => e as Error,
|
|
125
|
+
})
|
|
126
|
+
if (channel instanceof Error) {
|
|
127
|
+
// Channel not in this guild, skip
|
|
128
|
+
continue
|
|
129
|
+
}
|
|
130
|
+
if (channel) {
|
|
131
|
+
projectsInGuild.push({ directory, channelId: channel_id })
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const projects = projectsInGuild
|
|
136
|
+
.filter(({ directory }) => {
|
|
137
|
+
const baseName = path.basename(directory)
|
|
138
|
+
const searchText = `${baseName} ${directory}`.toLowerCase()
|
|
139
|
+
return searchText.includes(focusedValue.toLowerCase())
|
|
140
|
+
})
|
|
141
|
+
.slice(0, 25)
|
|
142
|
+
.map(({ directory }) => {
|
|
143
|
+
const name = `${path.basename(directory)} (${abbreviatePath(directory)})`
|
|
144
|
+
return {
|
|
145
|
+
name: name.length > 100 ? name.slice(0, 99) + '...' : name,
|
|
146
|
+
value: directory,
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
await interaction.respond(projects)
|
|
151
|
+
} catch (error) {
|
|
152
|
+
logger.error('[AUTOCOMPLETE] Error fetching projects:', error)
|
|
153
|
+
await interaction.respond([])
|
|
154
|
+
}
|
|
155
|
+
}
|