From d4e201dfa67b6731bfe4fd7e09281709b5d15e04 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 12:10:39 +0000 Subject: [PATCH 1/2] feat(bot): add PR signature to Cloud Agent prompts in bot SDK Port the PR signature feature from the old slack-bot/discord-bot implementations into the new bot SDK. When the Cloud Agent creates a PR/MR, the description now includes a 'Built for [User](link) by [Kilo for Platform]' attribution line. --- src/lib/bot/pr-signature.ts | 108 ++++++++++++++++++ src/lib/bot/run.ts | 16 ++- .../bot/tools/spawn-cloud-agent-session.ts | 19 ++- 3 files changed, 136 insertions(+), 7 deletions(-) create mode 100644 src/lib/bot/pr-signature.ts diff --git a/src/lib/bot/pr-signature.ts b/src/lib/bot/pr-signature.ts new file mode 100644 index 000000000..356168f53 --- /dev/null +++ b/src/lib/bot/pr-signature.ts @@ -0,0 +1,108 @@ +import { getAccessTokenFromInstallation } from '@/lib/integrations/slack-service'; +import { getSlackMessagePermalink } from '@/lib/slack-bot/slack-utils'; +import { WebClient } from '@slack/web-api'; +import type { SlackEvent } from '@chat-adapter/slack'; +import type { PlatformIntegration } from '@kilocode/db'; +import type { Thread, Message } from 'chat'; + +type RequesterInfo = { + displayName: string; + messageLink?: string; + platform: string; +}; + +const PLATFORM_LINKS: Record = { + slack: { label: 'Kilo for Slack', url: 'https://kilo.ai/features/slack-integration' }, + discord: { label: 'Kilo for Discord', url: 'https://kilo.ai' }, +}; + +const DEFAULT_PLATFORM_LINK = { label: 'Kilo', url: 'https://kilo.ai' }; + +/** + * Build the PR signature instruction to append to the Cloud Agent prompt. + * Instructs the agent to include a "Built for …" line at the end of any + * PR/MR description it creates. + */ +export function buildPrSignature(requesterInfo: RequesterInfo): string { + const requesterPart = requesterInfo.messageLink + ? `[${requesterInfo.displayName}](${requesterInfo.messageLink})` + : requesterInfo.displayName; + + const { label, url } = + PLATFORM_LINKS[requesterInfo.platform] ?? DEFAULT_PLATFORM_LINK; + + return ` + +--- +**PR Signature to include in the PR description:** +When you create a pull request or merge request, include the following signature at the end of the PR/MR description: + +Built for ${requesterPart} by [${label}](${url})`; +} + +/** + * Gather requester info (display name + message link) for the PR signature. + * Platform-specific: uses the Slack API for permalinks, constructs Discord + * links from IDs, and degrades gracefully for unknown platforms. + */ +export async function getRequesterInfo( + thread: Thread, + message: Message, + platformIntegration: PlatformIntegration +): Promise { + const platform = thread.id.split(':')[0]; + const displayName = message.author.fullName || message.author.userName || message.author.userId; + + switch (platform) { + case 'slack': + return getSlackRequesterInfo(message, platformIntegration, displayName); + case 'discord': + return getDiscordRequesterInfo(message, displayName); + default: + return { displayName, platform }; + } +} + +async function getSlackRequesterInfo( + message: Message, + platformIntegration: PlatformIntegration, + displayName: string +): Promise { + const accessToken = getAccessTokenFromInstallation(platformIntegration); + if (!accessToken) { + return { displayName, platform: 'slack' }; + } + + const raw = (message as Message).raw; + const channelId = + typeof raw === 'object' && raw !== null && 'channel' in raw + ? (raw as { channel?: string }).channel + : undefined; + const messageTs = message.id; // chat SDK uses Slack ts as the message ID + + if (!channelId || !messageTs) { + return { displayName, platform: 'slack' }; + } + + const slackClient = new WebClient(accessToken); + const permalink = await getSlackMessagePermalink(slackClient, channelId, messageTs); + + return { displayName, messageLink: permalink, platform: 'slack' }; +} + +function getDiscordRequesterInfo( + message: Message, + displayName: string +): RequesterInfo { + const raw = message.raw as { guild_id?: string; channel_id?: string } | null; + const guildId = raw?.guild_id; + const channelId = raw?.channel_id; + const messageId = message.id; + + const messageLink = + guildId && channelId && messageId + ? `https://discord.com/channels/${guildId}/${channelId}/${messageId}` + : undefined; + + return { displayName, messageLink, platform: 'discord' }; +} diff --git a/src/lib/bot/run.ts b/src/lib/bot/run.ts index d45854af5..5d571cf8b 100644 --- a/src/lib/bot/run.ts +++ b/src/lib/bot/run.ts @@ -9,6 +9,7 @@ import { getConversationContext, formatConversationContextForPrompt, } from '@/lib/bot/conversation-context'; +import { buildPrSignature, getRequesterInfo } from '@/lib/bot/pr-signature'; import { updateBotRequest } from '@/lib/bot/request-logging'; import spawnCloudAgentSession, { spawnCloudAgentInputSchema, @@ -133,6 +134,18 @@ export async function processMessage({ const modelSlug = (platformIntegration.metadata as { model_slug?: string }).model_slug ?? DEFAULT_BOT_MODEL; const owner = ownerFromIntegration(platformIntegration); + const chatPlatform = thread.id.split(':')[0]; + + // Build PR signature from requester info (display name + message permalink) + let prSignature: string | undefined; + try { + const requesterInfo = await getRequesterInfo(thread, message, platformIntegration); + if (requesterInfo) { + prSignature = buildPrSignature(requesterInfo); + } + } catch (error) { + console.warn('[KiloBot] Failed to build PR signature, continuing without it:', error); + } const startedAt = Date.now(); const collectedSteps: BotRequestStep[] = []; @@ -172,7 +185,8 @@ After the tool returns, if mode was "code", check the result for a PR/MR URL and if (botRequestId) { updateBotRequest(botRequestId, { cloudAgentSessionId: kiloSessionId }); } - } + }, + { prSignature, chatPlatform } ), }), }, diff --git a/src/lib/bot/tools/spawn-cloud-agent-session.ts b/src/lib/bot/tools/spawn-cloud-agent-session.ts index b6033162e..c08056f62 100644 --- a/src/lib/bot/tools/spawn-cloud-agent-session.ts +++ b/src/lib/bot/tools/spawn-cloud-agent-session.ts @@ -66,22 +66,24 @@ export default async function spawnCloudAgentSession( platformIntegration: PlatformIntegration, authToken: string, ticketUserId: string, - onSessionReady?: RunSessionInput['onSessionReady'] + onSessionReady?: RunSessionInput['onSessionReady'], + options?: { prSignature?: string; chatPlatform?: string } ): Promise { - console.log('[SlackBot] spawnCloudAgentSession called with args:', JSON.stringify(args, null, 2)); + console.log('[KiloBot] spawnCloudAgentSession called with args:', JSON.stringify(args, null, 2)); // Build platform-specific prepareInput and initiateInput const kilocodeOrganizationId = platformIntegration.owned_by_organization_id || undefined; let prepareInput: PrepareSessionInput; let initiateInput: { githubToken?: string; kilocodeOrganizationId?: string }; const mode: AgentMode = args.mode; + const chatPlatform = options?.chatPlatform ?? 'slack'; if (!args.githubRepo && !args.gitlabProject) { return { response: 'Error: You must specify either a githubRepo or a gitlabProject.' }; } const isGitLab = !!args.gitlabProject; - const prompt = + let prompt = mode === 'code' ? args.prompt + (isGitLab @@ -89,6 +91,11 @@ export default async function spawnCloudAgentSession( : '\n\nOpen a pull request with your changes and return the PR URL.') : args.prompt; + // Append PR/MR signature to the prompt if available + if (options?.prSignature) { + prompt += options.prSignature; + } + if (args.gitlabProject) { // GitLab path: get token + instance URL, build clone URL, use gitUrl/gitToken const gitlabToken = @@ -112,7 +119,7 @@ export default async function spawnCloudAgentSession( const isSelfHosted = !/^https?:\/\/(www\.)?gitlab\.com(\/|$)/i.test(instanceUrl); console.log( - '[SlackBot] GitLab session - project:', + '[KiloBot] GitLab session - project:', args.gitlabProject, 'instance:', isSelfHosted ? 'self-hosted' : 'gitlab.com' @@ -126,7 +133,7 @@ export default async function spawnCloudAgentSession( gitToken: gitlabToken, platform: 'gitlab', kilocodeOrganizationId, - createdOnPlatform: 'slack', + createdOnPlatform: chatPlatform, }; initiateInput = { kilocodeOrganizationId }; } else { @@ -150,7 +157,7 @@ export default async function spawnCloudAgentSession( model, githubToken, kilocodeOrganizationId, - createdOnPlatform: 'slack', + createdOnPlatform: chatPlatform, }; initiateInput = { githubToken, kilocodeOrganizationId }; } From 9f7ca22d1940362f9b9f1b313c98a7130f3c95b6 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 14:06:39 +0000 Subject: [PATCH 2/2] style(bot): fix formatting in pr-signature.ts --- src/lib/bot/pr-signature.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/lib/bot/pr-signature.ts b/src/lib/bot/pr-signature.ts index 356168f53..c80699889 100644 --- a/src/lib/bot/pr-signature.ts +++ b/src/lib/bot/pr-signature.ts @@ -28,8 +28,7 @@ export function buildPrSignature(requesterInfo: RequesterInfo): string { ? `[${requesterInfo.displayName}](${requesterInfo.messageLink})` : requesterInfo.displayName; - const { label, url } = - PLATFORM_LINKS[requesterInfo.platform] ?? DEFAULT_PLATFORM_LINK; + const { label, url } = PLATFORM_LINKS[requesterInfo.platform] ?? DEFAULT_PLATFORM_LINK; return ` @@ -90,10 +89,7 @@ async function getSlackRequesterInfo( return { displayName, messageLink: permalink, platform: 'slack' }; } -function getDiscordRequesterInfo( - message: Message, - displayName: string -): RequesterInfo { +function getDiscordRequesterInfo(message: Message, displayName: string): RequesterInfo { const raw = message.raw as { guild_id?: string; channel_id?: string } | null; const guildId = raw?.guild_id; const channelId = raw?.channel_id;