@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,490 @@
|
|
|
1
|
+
// /login command - Authenticate with AI providers (OAuth or API key).
|
|
2
|
+
// Supports GitHub Copilot (device flow), OpenAI Codex (device flow), and API keys.
|
|
3
|
+
import { ChatInputCommandInteraction, StringSelectMenuInteraction, StringSelectMenuBuilder, ActionRowBuilder, ModalBuilder, TextInputBuilder, TextInputStyle, ModalSubmitInteraction, ChannelType, MessageFlags, } from 'discord.js';
|
|
4
|
+
import crypto from 'node:crypto';
|
|
5
|
+
import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
6
|
+
import { resolveTextChannel, getKimakiMetadata } from '../discord-utils.js';
|
|
7
|
+
import { createLogger, LogPrefix } from '../logger.js';
|
|
8
|
+
const loginLogger = createLogger(LogPrefix.LOGIN);
|
|
9
|
+
// Store context by hash to avoid customId length limits (Discord max: 100 chars)
|
|
10
|
+
const pendingLoginContexts = new Map();
|
|
11
|
+
/**
|
|
12
|
+
* Handle the /login slash command.
|
|
13
|
+
* Shows a select menu with available providers.
|
|
14
|
+
*/
|
|
15
|
+
export async function handleLoginCommand({ interaction, appId, }) {
|
|
16
|
+
loginLogger.log('[LOGIN] handleLoginCommand called');
|
|
17
|
+
// Defer reply immediately to avoid 3-second timeout
|
|
18
|
+
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
|
19
|
+
loginLogger.log('[LOGIN] Deferred reply');
|
|
20
|
+
const channel = interaction.channel;
|
|
21
|
+
if (!channel) {
|
|
22
|
+
await interaction.editReply({
|
|
23
|
+
content: 'This command can only be used in a channel',
|
|
24
|
+
});
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
// Determine if we're in a thread or text channel
|
|
28
|
+
const isThread = [
|
|
29
|
+
ChannelType.PublicThread,
|
|
30
|
+
ChannelType.PrivateThread,
|
|
31
|
+
ChannelType.AnnouncementThread,
|
|
32
|
+
].includes(channel.type);
|
|
33
|
+
let projectDirectory;
|
|
34
|
+
let channelAppId;
|
|
35
|
+
let targetChannelId;
|
|
36
|
+
if (isThread) {
|
|
37
|
+
const thread = channel;
|
|
38
|
+
const textChannel = await resolveTextChannel(thread);
|
|
39
|
+
const metadata = await getKimakiMetadata(textChannel);
|
|
40
|
+
projectDirectory = metadata.projectDirectory;
|
|
41
|
+
channelAppId = metadata.channelAppId;
|
|
42
|
+
targetChannelId = textChannel?.id || channel.id;
|
|
43
|
+
}
|
|
44
|
+
else if (channel.type === ChannelType.GuildText) {
|
|
45
|
+
const textChannel = channel;
|
|
46
|
+
const metadata = await getKimakiMetadata(textChannel);
|
|
47
|
+
projectDirectory = metadata.projectDirectory;
|
|
48
|
+
channelAppId = metadata.channelAppId;
|
|
49
|
+
targetChannelId = channel.id;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
await interaction.editReply({
|
|
53
|
+
content: 'This command can only be used in text channels or threads',
|
|
54
|
+
});
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (channelAppId && channelAppId !== appId) {
|
|
58
|
+
await interaction.editReply({
|
|
59
|
+
content: 'This channel is not configured for this bot',
|
|
60
|
+
});
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (!projectDirectory) {
|
|
64
|
+
await interaction.editReply({
|
|
65
|
+
content: 'This channel is not configured with a project directory',
|
|
66
|
+
});
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
71
|
+
if (getClient instanceof Error) {
|
|
72
|
+
await interaction.editReply({ content: getClient.message });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const providersResponse = await getClient().provider.list({
|
|
76
|
+
directory: projectDirectory,
|
|
77
|
+
});
|
|
78
|
+
if (!providersResponse.data) {
|
|
79
|
+
await interaction.editReply({
|
|
80
|
+
content: 'Failed to fetch providers',
|
|
81
|
+
});
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const { all: allProviders, connected } = providersResponse.data;
|
|
85
|
+
if (allProviders.length === 0) {
|
|
86
|
+
await interaction.editReply({
|
|
87
|
+
content: 'No providers available.',
|
|
88
|
+
});
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// Store context with a short hash key to avoid customId length limits
|
|
92
|
+
const context = {
|
|
93
|
+
dir: projectDirectory,
|
|
94
|
+
channelId: targetChannelId,
|
|
95
|
+
};
|
|
96
|
+
const contextHash = crypto.randomBytes(8).toString('hex');
|
|
97
|
+
pendingLoginContexts.set(contextHash, context);
|
|
98
|
+
const options = [...allProviders]
|
|
99
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
100
|
+
.slice(0, 25)
|
|
101
|
+
.map((provider) => {
|
|
102
|
+
const isConnected = connected.includes(provider.id);
|
|
103
|
+
return {
|
|
104
|
+
label: `${provider.name}${isConnected ? ' ✓' : ''}`.slice(0, 100),
|
|
105
|
+
value: provider.id,
|
|
106
|
+
description: isConnected
|
|
107
|
+
? 'Connected - select to re-authenticate'
|
|
108
|
+
: 'Not connected',
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
const selectMenu = new StringSelectMenuBuilder()
|
|
112
|
+
.setCustomId(`login_provider:${contextHash}`)
|
|
113
|
+
.setPlaceholder('Select a provider to authenticate')
|
|
114
|
+
.addOptions(options);
|
|
115
|
+
const actionRow = new ActionRowBuilder().addComponents(selectMenu);
|
|
116
|
+
await interaction.editReply({
|
|
117
|
+
content: '**Authenticate with Provider**\nSelect a provider:',
|
|
118
|
+
components: [actionRow],
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
loginLogger.error('Error loading providers:', error);
|
|
123
|
+
await interaction.editReply({
|
|
124
|
+
content: `Failed to load providers: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Handle the provider select menu interaction.
|
|
130
|
+
* Shows a second select menu with auth methods for the chosen provider.
|
|
131
|
+
*/
|
|
132
|
+
export async function handleLoginProviderSelectMenu(interaction) {
|
|
133
|
+
const customId = interaction.customId;
|
|
134
|
+
if (!customId.startsWith('login_provider:')) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const contextHash = customId.replace('login_provider:', '');
|
|
138
|
+
const context = pendingLoginContexts.get(contextHash);
|
|
139
|
+
if (!context) {
|
|
140
|
+
await interaction.deferUpdate();
|
|
141
|
+
await interaction.editReply({
|
|
142
|
+
content: 'Selection expired. Please run /login again.',
|
|
143
|
+
components: [],
|
|
144
|
+
});
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const selectedProviderId = interaction.values[0];
|
|
148
|
+
if (!selectedProviderId) {
|
|
149
|
+
await interaction.deferUpdate();
|
|
150
|
+
await interaction.editReply({
|
|
151
|
+
content: 'No provider selected',
|
|
152
|
+
components: [],
|
|
153
|
+
});
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
const getClient = await initializeOpencodeForDirectory(context.dir);
|
|
158
|
+
if (getClient instanceof Error) {
|
|
159
|
+
await interaction.deferUpdate();
|
|
160
|
+
await interaction.editReply({
|
|
161
|
+
content: getClient.message,
|
|
162
|
+
components: [],
|
|
163
|
+
});
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
// Get provider info for display
|
|
167
|
+
const providersResponse = await getClient().provider.list({
|
|
168
|
+
directory: context.dir,
|
|
169
|
+
});
|
|
170
|
+
const provider = providersResponse.data?.all.find((p) => p.id === selectedProviderId);
|
|
171
|
+
const providerName = provider?.name || selectedProviderId;
|
|
172
|
+
// Get auth methods for all providers
|
|
173
|
+
const authMethodsResponse = await getClient().provider.auth({
|
|
174
|
+
directory: context.dir,
|
|
175
|
+
});
|
|
176
|
+
if (!authMethodsResponse.data) {
|
|
177
|
+
await interaction.deferUpdate();
|
|
178
|
+
await interaction.editReply({
|
|
179
|
+
content: 'Failed to fetch authentication methods',
|
|
180
|
+
components: [],
|
|
181
|
+
});
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
// Get methods for this specific provider, default to API key if none defined
|
|
185
|
+
const methods = authMethodsResponse.data[selectedProviderId] || [{ type: 'api', label: 'API Key' }];
|
|
186
|
+
if (methods.length === 0) {
|
|
187
|
+
await interaction.deferUpdate();
|
|
188
|
+
await interaction.editReply({
|
|
189
|
+
content: `No authentication methods available for ${providerName}`,
|
|
190
|
+
components: [],
|
|
191
|
+
});
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
// Update context with provider info
|
|
195
|
+
context.providerId = selectedProviderId;
|
|
196
|
+
context.providerName = providerName;
|
|
197
|
+
pendingLoginContexts.set(contextHash, context);
|
|
198
|
+
// If only one method and it's API, show modal directly (no defer)
|
|
199
|
+
if (methods.length === 1 && methods[0].type === 'api') {
|
|
200
|
+
const method = methods[0];
|
|
201
|
+
context.methodIndex = 0;
|
|
202
|
+
context.methodType = method.type;
|
|
203
|
+
context.methodLabel = method.label;
|
|
204
|
+
pendingLoginContexts.set(contextHash, context);
|
|
205
|
+
await showApiKeyModal(interaction, contextHash, providerName);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
// For OAuth or multiple methods, defer and continue
|
|
209
|
+
await interaction.deferUpdate();
|
|
210
|
+
// If only one method and it's OAuth, start flow directly
|
|
211
|
+
if (methods.length === 1) {
|
|
212
|
+
const method = methods[0];
|
|
213
|
+
context.methodIndex = 0;
|
|
214
|
+
context.methodType = method.type;
|
|
215
|
+
context.methodLabel = method.label;
|
|
216
|
+
pendingLoginContexts.set(contextHash, context);
|
|
217
|
+
await startOAuthFlow(interaction, context, contextHash);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
// Multiple methods - show selection menu
|
|
221
|
+
const options = methods.slice(0, 25).map((method, index) => ({
|
|
222
|
+
label: method.label.slice(0, 100),
|
|
223
|
+
value: String(index),
|
|
224
|
+
description: method.type === 'oauth'
|
|
225
|
+
? 'OAuth authentication'
|
|
226
|
+
: 'Enter API key manually',
|
|
227
|
+
}));
|
|
228
|
+
const selectMenu = new StringSelectMenuBuilder()
|
|
229
|
+
.setCustomId(`login_method:${contextHash}`)
|
|
230
|
+
.setPlaceholder('Select authentication method')
|
|
231
|
+
.addOptions(options);
|
|
232
|
+
const actionRow = new ActionRowBuilder().addComponents(selectMenu);
|
|
233
|
+
await interaction.editReply({
|
|
234
|
+
content: `**Authenticate with ${providerName}**\nSelect authentication method:`,
|
|
235
|
+
components: [actionRow],
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
loginLogger.error('Error loading auth methods:', error);
|
|
240
|
+
if (!interaction.deferred && !interaction.replied) {
|
|
241
|
+
await interaction.deferUpdate();
|
|
242
|
+
}
|
|
243
|
+
await interaction.editReply({
|
|
244
|
+
content: `Failed to load auth methods: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
245
|
+
components: [],
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Handle the auth method select menu interaction.
|
|
251
|
+
* Starts OAuth flow or shows API key modal.
|
|
252
|
+
*/
|
|
253
|
+
export async function handleLoginMethodSelectMenu(interaction) {
|
|
254
|
+
const customId = interaction.customId;
|
|
255
|
+
if (!customId.startsWith('login_method:')) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const contextHash = customId.replace('login_method:', '');
|
|
259
|
+
const context = pendingLoginContexts.get(contextHash);
|
|
260
|
+
if (!context || !context.providerId || !context.providerName) {
|
|
261
|
+
await interaction.deferUpdate();
|
|
262
|
+
await interaction.editReply({
|
|
263
|
+
content: 'Selection expired. Please run /login again.',
|
|
264
|
+
components: [],
|
|
265
|
+
});
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const selectedMethodIndex = parseInt(interaction.values[0] || '0', 10);
|
|
269
|
+
try {
|
|
270
|
+
const getClient = await initializeOpencodeForDirectory(context.dir);
|
|
271
|
+
if (getClient instanceof Error) {
|
|
272
|
+
await interaction.deferUpdate();
|
|
273
|
+
await interaction.editReply({
|
|
274
|
+
content: getClient.message,
|
|
275
|
+
components: [],
|
|
276
|
+
});
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
// Get auth methods again to get the selected one
|
|
280
|
+
const authMethodsResponse = await getClient().provider.auth({
|
|
281
|
+
directory: context.dir,
|
|
282
|
+
});
|
|
283
|
+
const methods = authMethodsResponse.data?.[context.providerId] || [{ type: 'api', label: 'API Key' }];
|
|
284
|
+
const selectedMethod = methods[selectedMethodIndex];
|
|
285
|
+
if (!selectedMethod) {
|
|
286
|
+
await interaction.deferUpdate();
|
|
287
|
+
await interaction.editReply({
|
|
288
|
+
content: 'Invalid method selected',
|
|
289
|
+
components: [],
|
|
290
|
+
});
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
// Update context
|
|
294
|
+
context.methodIndex = selectedMethodIndex;
|
|
295
|
+
context.methodType = selectedMethod.type;
|
|
296
|
+
context.methodLabel = selectedMethod.label;
|
|
297
|
+
pendingLoginContexts.set(contextHash, context);
|
|
298
|
+
if (selectedMethod.type === 'api') {
|
|
299
|
+
// Show API key modal (don't defer for modals)
|
|
300
|
+
await showApiKeyModal(interaction, contextHash, context.providerName);
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
// Start OAuth flow
|
|
304
|
+
await interaction.deferUpdate();
|
|
305
|
+
await startOAuthFlow(interaction, context, contextHash);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
catch (error) {
|
|
309
|
+
loginLogger.error('Error processing auth method:', error);
|
|
310
|
+
try {
|
|
311
|
+
if (!interaction.deferred && !interaction.replied) {
|
|
312
|
+
await interaction.deferUpdate();
|
|
313
|
+
}
|
|
314
|
+
await interaction.editReply({
|
|
315
|
+
content: `Failed to process auth method: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
316
|
+
components: [],
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
// Ignore follow-up errors
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Show API key input modal.
|
|
326
|
+
*/
|
|
327
|
+
async function showApiKeyModal(interaction, contextHash, providerName) {
|
|
328
|
+
const modal = new ModalBuilder()
|
|
329
|
+
.setCustomId(`login_apikey:${contextHash}`)
|
|
330
|
+
.setTitle(`${providerName} API Key`.slice(0, 45));
|
|
331
|
+
const apiKeyInput = new TextInputBuilder()
|
|
332
|
+
.setCustomId('apikey')
|
|
333
|
+
.setLabel('API Key')
|
|
334
|
+
.setPlaceholder('sk-...')
|
|
335
|
+
.setStyle(TextInputStyle.Short)
|
|
336
|
+
.setRequired(true);
|
|
337
|
+
const actionRow = new ActionRowBuilder().addComponents(apiKeyInput);
|
|
338
|
+
modal.addComponents(actionRow);
|
|
339
|
+
await interaction.showModal(modal);
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Start OAuth authorization flow.
|
|
343
|
+
*/
|
|
344
|
+
async function startOAuthFlow(interaction, context, contextHash) {
|
|
345
|
+
if (!context.providerId || context.methodIndex === undefined) {
|
|
346
|
+
await interaction.editReply({
|
|
347
|
+
content: 'Invalid context for OAuth flow',
|
|
348
|
+
components: [],
|
|
349
|
+
});
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
try {
|
|
353
|
+
const getClient = await initializeOpencodeForDirectory(context.dir);
|
|
354
|
+
if (getClient instanceof Error) {
|
|
355
|
+
await interaction.editReply({
|
|
356
|
+
content: getClient.message,
|
|
357
|
+
components: [],
|
|
358
|
+
});
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
await interaction.editReply({
|
|
362
|
+
content: `**Authenticating with ${context.providerName}**\nStarting authorization...`,
|
|
363
|
+
components: [],
|
|
364
|
+
});
|
|
365
|
+
// Start OAuth authorization
|
|
366
|
+
const authorizeResponse = await getClient().provider.oauth.authorize({
|
|
367
|
+
providerID: context.providerId,
|
|
368
|
+
method: context.methodIndex,
|
|
369
|
+
directory: context.dir,
|
|
370
|
+
});
|
|
371
|
+
if (!authorizeResponse.data) {
|
|
372
|
+
const errorData = authorizeResponse.error;
|
|
373
|
+
await interaction.editReply({
|
|
374
|
+
content: `Failed to start authorization: ${errorData?.data?.message || 'Unknown error'}`,
|
|
375
|
+
components: [],
|
|
376
|
+
});
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
const { url, method, instructions } = authorizeResponse.data;
|
|
380
|
+
// Show authorization URL and instructions
|
|
381
|
+
let message = `**Authenticating with ${context.providerName}**\n\n`;
|
|
382
|
+
message += `Open this URL to authorize:\n${url}\n\n`;
|
|
383
|
+
if (instructions) {
|
|
384
|
+
// Extract code from instructions like "Enter code: ABC-123"
|
|
385
|
+
const codeMatch = instructions.match(/code[:\s]+([A-Z0-9-]+)/i);
|
|
386
|
+
if (codeMatch) {
|
|
387
|
+
message += `**Code:** \`${codeMatch[1]}\`\n\n`;
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
message += `${instructions}\n\n`;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (method === 'auto') {
|
|
394
|
+
message += '_Waiting for authorization to complete..._';
|
|
395
|
+
}
|
|
396
|
+
await interaction.editReply({
|
|
397
|
+
content: message,
|
|
398
|
+
components: [],
|
|
399
|
+
});
|
|
400
|
+
if (method === 'auto') {
|
|
401
|
+
// Poll for completion (device flow)
|
|
402
|
+
const callbackResponse = await getClient().provider.oauth.callback({
|
|
403
|
+
providerID: context.providerId,
|
|
404
|
+
method: context.methodIndex,
|
|
405
|
+
directory: context.dir,
|
|
406
|
+
});
|
|
407
|
+
if (callbackResponse.error) {
|
|
408
|
+
const errorData = callbackResponse.error;
|
|
409
|
+
await interaction.editReply({
|
|
410
|
+
content: `**Authentication Failed**\n${errorData?.data?.message || 'Authorization was not completed'}`,
|
|
411
|
+
components: [],
|
|
412
|
+
});
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
// Dispose to refresh provider state so new credentials are recognized
|
|
416
|
+
await getClient().instance.dispose({ directory: context.dir });
|
|
417
|
+
await interaction.editReply({
|
|
418
|
+
content: `✅ **Successfully authenticated with ${context.providerName}!**\n\nYou can now use models from this provider.`,
|
|
419
|
+
components: [],
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
// For 'code' method, we would need to prompt for code input
|
|
423
|
+
// But Discord modals can't be shown after deferUpdate, so we'd need a different flow
|
|
424
|
+
// For now, most providers use 'auto' (device flow) which works well for Discord
|
|
425
|
+
// Clean up context
|
|
426
|
+
pendingLoginContexts.delete(contextHash);
|
|
427
|
+
}
|
|
428
|
+
catch (error) {
|
|
429
|
+
loginLogger.error('OAuth flow error:', error);
|
|
430
|
+
await interaction.editReply({
|
|
431
|
+
content: `**Authentication Failed**\n${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
432
|
+
components: [],
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Handle API key modal submission.
|
|
438
|
+
*/
|
|
439
|
+
export async function handleApiKeyModalSubmit(interaction) {
|
|
440
|
+
const customId = interaction.customId;
|
|
441
|
+
if (!customId.startsWith('login_apikey:')) {
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
|
445
|
+
const contextHash = customId.replace('login_apikey:', '');
|
|
446
|
+
const context = pendingLoginContexts.get(contextHash);
|
|
447
|
+
if (!context || !context.providerId || !context.providerName) {
|
|
448
|
+
await interaction.editReply({
|
|
449
|
+
content: 'Session expired. Please run /login again.',
|
|
450
|
+
});
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
const apiKey = interaction.fields.getTextInputValue('apikey');
|
|
454
|
+
if (!apiKey?.trim()) {
|
|
455
|
+
await interaction.editReply({
|
|
456
|
+
content: 'API key is required.',
|
|
457
|
+
});
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
try {
|
|
461
|
+
const getClient = await initializeOpencodeForDirectory(context.dir);
|
|
462
|
+
if (getClient instanceof Error) {
|
|
463
|
+
await interaction.editReply({
|
|
464
|
+
content: getClient.message,
|
|
465
|
+
});
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
// Set the API key
|
|
469
|
+
await getClient().auth.set({
|
|
470
|
+
providerID: context.providerId,
|
|
471
|
+
auth: {
|
|
472
|
+
type: 'api',
|
|
473
|
+
key: apiKey.trim(),
|
|
474
|
+
},
|
|
475
|
+
});
|
|
476
|
+
// Dispose to refresh provider state so new credentials are recognized
|
|
477
|
+
await getClient().instance.dispose({ directory: context.dir });
|
|
478
|
+
await interaction.editReply({
|
|
479
|
+
content: `✅ **Successfully authenticated with ${context.providerName}!**\n\nYou can now use models from this provider.`,
|
|
480
|
+
});
|
|
481
|
+
// Clean up context
|
|
482
|
+
pendingLoginContexts.delete(contextHash);
|
|
483
|
+
}
|
|
484
|
+
catch (error) {
|
|
485
|
+
loginLogger.error('API key save error:', error);
|
|
486
|
+
await interaction.editReply({
|
|
487
|
+
content: `**Failed to save API key**\n${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// /toggle-mention-mode command.
|
|
2
|
+
// Toggles mention-only mode for a channel.
|
|
3
|
+
// When enabled, bot only responds to messages that @mention it.
|
|
4
|
+
// Messages in threads are not affected - they always work without mentions.
|
|
5
|
+
import { ChatInputCommandInteraction, MessageFlags, ChannelType, } from 'discord.js';
|
|
6
|
+
import { getChannelMentionMode, setChannelMentionMode } from '../database.js';
|
|
7
|
+
import { getKimakiMetadata } from '../discord-utils.js';
|
|
8
|
+
import { createLogger, LogPrefix } from '../logger.js';
|
|
9
|
+
const mentionModeLogger = createLogger(LogPrefix.CLI);
|
|
10
|
+
/**
|
|
11
|
+
* Handle the /toggle-mention-mode slash command.
|
|
12
|
+
* Toggles whether the bot only responds when @mentioned in this channel.
|
|
13
|
+
*/
|
|
14
|
+
export async function handleToggleMentionModeCommand({ command, appId, }) {
|
|
15
|
+
mentionModeLogger.log('[TOGGLE_MENTION_MODE] Command called');
|
|
16
|
+
const channel = command.channel;
|
|
17
|
+
if (!channel || channel.type !== ChannelType.GuildText) {
|
|
18
|
+
await command.reply({
|
|
19
|
+
content: 'This command can only be used in text channels (not threads).',
|
|
20
|
+
flags: MessageFlags.Ephemeral,
|
|
21
|
+
});
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const textChannel = channel;
|
|
25
|
+
const metadata = await getKimakiMetadata(textChannel);
|
|
26
|
+
if (metadata.channelAppId && metadata.channelAppId !== appId) {
|
|
27
|
+
await command.reply({
|
|
28
|
+
content: 'This channel is configured for a different bot.',
|
|
29
|
+
flags: MessageFlags.Ephemeral,
|
|
30
|
+
});
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (!metadata.projectDirectory) {
|
|
34
|
+
await command.reply({
|
|
35
|
+
content: 'This channel is not configured with a project directory.\nUse `/add-project` to set up this channel.',
|
|
36
|
+
flags: MessageFlags.Ephemeral,
|
|
37
|
+
});
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const wasEnabled = await getChannelMentionMode(textChannel.id);
|
|
41
|
+
const nextEnabled = !wasEnabled;
|
|
42
|
+
await setChannelMentionMode(textChannel.id, nextEnabled);
|
|
43
|
+
const nextLabel = nextEnabled ? 'enabled' : 'disabled';
|
|
44
|
+
mentionModeLogger.log(`[TOGGLE_MENTION_MODE] ${nextLabel.toUpperCase()} for channel ${textChannel.id}`);
|
|
45
|
+
await command.reply({
|
|
46
|
+
content: nextEnabled
|
|
47
|
+
? `Mention mode **enabled** for this channel.\nThe bot will only start new sessions when @mentioned.\nMessages in existing threads are not affected.`
|
|
48
|
+
: `Mention mode **disabled** for this channel.\nThe bot will respond to all messages in **#${textChannel.name}**.`,
|
|
49
|
+
flags: MessageFlags.Ephemeral,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// /merge-worktree command - Merge worktree commits into default branch.
|
|
2
|
+
// Uses worktrunk-style pipeline: squash -> rebase -> local push.
|
|
3
|
+
// On rebase conflicts, asks the AI model in the thread to resolve them.
|
|
4
|
+
import {} from 'discord.js';
|
|
5
|
+
import { getThreadWorktree, getThreadSession } from '../database.js';
|
|
6
|
+
import { createLogger, LogPrefix } from '../logger.js';
|
|
7
|
+
import { notifyError } from '../sentry.js';
|
|
8
|
+
import { mergeWorktree } from '../worktree-utils.js';
|
|
9
|
+
import { sendThreadMessage, resolveWorkingDirectory } from '../discord-utils.js';
|
|
10
|
+
import { handleOpencodeSession, abortControllers, addToQueue, } from '../session-handler.js';
|
|
11
|
+
import { RebaseConflictError, DirtyWorktreeError } from '../errors.js';
|
|
12
|
+
const logger = createLogger(LogPrefix.WORKTREE);
|
|
13
|
+
/** Worktree thread title prefix - indicates unmerged worktree */
|
|
14
|
+
export const WORKTREE_PREFIX = '⬦ ';
|
|
15
|
+
async function removeWorktreePrefixFromTitle(thread) {
|
|
16
|
+
if (!thread.name.startsWith(WORKTREE_PREFIX)) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const newName = thread.name.slice(WORKTREE_PREFIX.length);
|
|
20
|
+
const timeoutMs = 5000;
|
|
21
|
+
await Promise.race([
|
|
22
|
+
thread.setName(newName).catch((e) => {
|
|
23
|
+
logger.warn(`Failed to update thread title: ${e instanceof Error ? e.message : String(e)}`);
|
|
24
|
+
}),
|
|
25
|
+
new Promise((resolve) => {
|
|
26
|
+
setTimeout(() => {
|
|
27
|
+
logger.warn(`Thread title update timed out after ${timeoutMs}ms`);
|
|
28
|
+
resolve();
|
|
29
|
+
}, timeoutMs);
|
|
30
|
+
}),
|
|
31
|
+
]);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Send a prompt to the AI model in the thread.
|
|
35
|
+
* If a session is actively streaming, queues it. Otherwise sends directly.
|
|
36
|
+
*/
|
|
37
|
+
async function sendPromptToModel({ prompt, thread, projectDirectory, command, appId, }) {
|
|
38
|
+
const sessionId = await getThreadSession(thread.id);
|
|
39
|
+
const existingController = sessionId ? abortControllers.get(sessionId) : null;
|
|
40
|
+
const hasActiveRequest = Boolean(existingController && !existingController.signal.aborted);
|
|
41
|
+
if (hasActiveRequest) {
|
|
42
|
+
addToQueue({
|
|
43
|
+
threadId: thread.id,
|
|
44
|
+
message: {
|
|
45
|
+
prompt,
|
|
46
|
+
userId: command.user.id,
|
|
47
|
+
username: command.user.displayName,
|
|
48
|
+
queuedAt: Date.now(),
|
|
49
|
+
appId,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
logger.log(`[merge] Queued prompt (session active)`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const resolved = await resolveWorkingDirectory({ channel: thread });
|
|
56
|
+
handleOpencodeSession({
|
|
57
|
+
prompt,
|
|
58
|
+
thread,
|
|
59
|
+
projectDirectory: resolved?.projectDirectory || projectDirectory,
|
|
60
|
+
channelId: thread.parentId || thread.id,
|
|
61
|
+
username: command.user.displayName,
|
|
62
|
+
userId: command.user.id,
|
|
63
|
+
appId,
|
|
64
|
+
}).catch((e) => {
|
|
65
|
+
logger.error(`[merge] Failed to send prompt to model:`, e);
|
|
66
|
+
void notifyError(e, 'Merge-worktree prompt send failed');
|
|
67
|
+
sendThreadMessage(thread, `Failed to send prompt: ${(e instanceof Error ? e.message : String(e)).slice(0, 1900)}`).catch(() => { });
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
export async function handleMergeWorktreeCommand({ command, appId, }) {
|
|
71
|
+
await command.deferReply({ ephemeral: false });
|
|
72
|
+
const channel = command.channel;
|
|
73
|
+
if (!channel || !channel.isThread()) {
|
|
74
|
+
await command.editReply('This command can only be used in a thread');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const thread = channel;
|
|
78
|
+
const worktreeInfo = await getThreadWorktree(thread.id);
|
|
79
|
+
if (!worktreeInfo) {
|
|
80
|
+
await command.editReply('This thread is not associated with a worktree');
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (worktreeInfo.status !== 'ready' || !worktreeInfo.worktree_directory) {
|
|
84
|
+
await command.editReply(`Worktree is not ready (status: ${worktreeInfo.status})${worktreeInfo.error_message ? `: ${worktreeInfo.error_message}` : ''}`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const result = await mergeWorktree({
|
|
88
|
+
worktreeDir: worktreeInfo.worktree_directory,
|
|
89
|
+
mainRepoDir: worktreeInfo.project_directory,
|
|
90
|
+
worktreeName: worktreeInfo.worktree_name,
|
|
91
|
+
onProgress: (msg) => {
|
|
92
|
+
logger.log(`[merge] ${msg}`);
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
if (result instanceof Error) {
|
|
96
|
+
if (result instanceof DirtyWorktreeError) {
|
|
97
|
+
await command.editReply('Merge failed: uncommitted changes in the worktree. Commit changes first, then run `/merge-worktree` again.');
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (result instanceof RebaseConflictError) {
|
|
101
|
+
await command.editReply('Rebase conflict detected. Asking the model to resolve...');
|
|
102
|
+
await sendPromptToModel({
|
|
103
|
+
prompt: [
|
|
104
|
+
'A rebase conflict occurred while merging this worktree into the default branch.',
|
|
105
|
+
'Please resolve the rebase conflicts:',
|
|
106
|
+
'1. Check `git status` to see which files have conflicts',
|
|
107
|
+
'2. Edit the conflicted files to resolve the merge markers',
|
|
108
|
+
'3. Stage resolved files with `git add`',
|
|
109
|
+
'4. Continue the rebase with `git rebase --continue`',
|
|
110
|
+
'5. After the rebase completes successfully, tell me so I can run `/merge-worktree` again',
|
|
111
|
+
].join('\n'),
|
|
112
|
+
thread,
|
|
113
|
+
projectDirectory: worktreeInfo.project_directory,
|
|
114
|
+
command,
|
|
115
|
+
appId,
|
|
116
|
+
});
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
await command.editReply(`Merge failed: ${result.message}`);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
void removeWorktreePrefixFromTitle(thread);
|
|
123
|
+
await command.editReply(`Merged \`${result.branchName}\` into \`${result.defaultBranch}\` @ ${result.shortSha} (${result.commitCount} commit${result.commitCount === 1 ? '' : 's'})\nWorktree now at detached HEAD.`);
|
|
124
|
+
}
|