diff --git a/services/ask-ai-bot/.env.example b/services/ask-ai-bot/.env.example index bd6490f05f09..6a56465928f7 100644 --- a/services/ask-ai-bot/.env.example +++ b/services/ask-ai-bot/.env.example @@ -14,5 +14,8 @@ AI_MODEL=claude-sonnet-4-6 # Path to documentation directory (default: ./docs in Docker, for local dev use ../../documentation/docs) DOCS_PATH=../../documentation/docs +# GitHub personal access token (optional, for higher API rate limits when reading issues) +GITHUB_TOKEN=ghp_1234 + # Path to codebase root (default: ../.. relative to this service, /app/codebase in Docker) CODEBASE_PATH=../.. diff --git a/services/ask-ai-bot/bun.lock b/services/ask-ai-bot/bun.lock index 1ad1463c1ac6..aa4e4e80209c 100644 --- a/services/ask-ai-bot/bun.lock +++ b/services/ask-ai-bot/bun.lock @@ -6,6 +6,7 @@ "name": "discraft-bot", "dependencies": { "@ai-sdk/anthropic": "^4.0.10", + "@octokit/rest": "^22.0.1", "ai": "^7.0.18", "consola": "^3.4.2", "dedent": "^1.7.2", @@ -102,6 +103,30 @@ "@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="], + "@octokit/auth-token": ["@octokit/auth-token@6.0.0", "", {}, "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="], + + "@octokit/core": ["@octokit/core@7.0.6", "", { "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.3", "@octokit/request": "^10.0.6", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "before-after-hook": "^4.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q=="], + + "@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], + + "@octokit/graphql": ["@octokit/graphql@9.0.3", "", { "dependencies": { "@octokit/request": "^10.0.6", "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA=="], + + "@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], + + "@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@14.0.0", "", { "dependencies": { "@octokit/types": "^16.0.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw=="], + + "@octokit/plugin-request-log": ["@octokit/plugin-request-log@6.0.0", "", { "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q=="], + + "@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@17.0.0", "", { "dependencies": { "@octokit/types": "^16.0.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw=="], + + "@octokit/request": ["@octokit/request@10.0.11", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "content-type": "^2.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-+s7HUxjfFqOMS9VlIwDffq0MikjSAK0gSpG73W+meAvVAvX4MBrHYTK5Bj3Uot55qFT4gzUtfzE4mGWY4Br8/Q=="], + + "@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="], + + "@octokit/rest": ["@octokit/rest@22.0.1", "", { "dependencies": { "@octokit/core": "^7.0.6", "@octokit/plugin-paginate-rest": "^14.0.0", "@octokit/plugin-request-log": "^6.0.0", "@octokit/plugin-rest-endpoint-methods": "^17.0.0" } }, "sha512-Jzbhzl3CEexhnivb1iQ0KJ7s5vvjMWcmRtq5aUsKmKDrRW6z3r84ngmiFKFvpZjpiU/9/S6ITPFRpn5s/3uQJw=="], + + "@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], + "@sapphire/async-queue": ["@sapphire/async-queue@1.5.5", "", {}, "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg=="], "@sapphire/shapeshift": ["@sapphire/shapeshift@4.0.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "lodash": "^4.17.21" } }, "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg=="], @@ -162,12 +187,16 @@ "ai": ["ai@7.0.18", "", { "dependencies": { "@ai-sdk/gateway": "4.0.14", "@ai-sdk/provider": "4.0.2", "@ai-sdk/provider-utils": "5.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-/DsiRfbLPtoHf9C47Hy+LK+HkjNowX1DQo3id4zWegfU6z1WjVDRAHYJAiX7SXhGp0mWX4tcenUhfIFessDWnw=="], + "before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="], + "chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], "commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="], "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], + "content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + "dedent": ["dedent@1.7.2", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA=="], "discord-api-types": ["discord-api-types@0.38.49", "", {}, "sha512-XnqcWmnFZFAE8ZM8SHAw9DIV8D3Or00rMQ8iQLotrEA2PmXhl+ykaf6L6q4l474hrSUH1JaYcv+iOMRWp2p6Tg=="], @@ -196,6 +225,8 @@ "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], + "json-with-bigint": ["json-with-bigint@3.5.10", "", {}, "sha512-Vcx+JVNEBts/xfcoCS69sKrOhOk/3TVlvlT+XzUOefVKnnrbYSCKpDCm10pohsJFtsJVYnwa/cXRZ4eElzaM6w=="], + "jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], @@ -242,6 +273,8 @@ "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "universal-user-agent": ["universal-user-agent@7.0.3", "", {}, "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A=="], + "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], diff --git a/services/ask-ai-bot/package.json b/services/ask-ai-bot/package.json index 96af25ce6233..3faf9a33101d 100644 --- a/services/ask-ai-bot/package.json +++ b/services/ask-ai-bot/package.json @@ -1,7 +1,7 @@ { "name": "goose-ask-ai-bot", "private": true, - "version": "1.0.3", + "version": "1.0.4", "description": "Bot created with discraft", "module": "index.ts", "type": "module", @@ -16,6 +16,7 @@ }, "dependencies": { "@ai-sdk/anthropic": "^4.0.10", + "@octokit/rest": "^22.0.1", "ai": "^7.0.18", "consola": "^3.4.2", "dedent": "^1.7.2", diff --git a/services/ask-ai-bot/utils/ai/index.ts b/services/ask-ai-bot/utils/ai/index.ts index ab5c458f3745..79de16693432 100644 --- a/services/ask-ai-bot/utils/ai/index.ts +++ b/services/ask-ai-bot/utils/ai/index.ts @@ -84,6 +84,10 @@ export async function answerQuestion({ ); } else if (event.toolName === "list_codebase_files") { await statusMessage.edit("Exploring project structure..."); + } else if (event.toolName === "search_github") { + await statusMessage.edit("Searching GitHub..."); + } else if (event.toolName === "get_github_issue_or_pr") { + await statusMessage.edit("Reading GitHub issues and PRs..."); } } catch (error) { logger.verbose("Failed to update status message:", error); @@ -123,6 +127,10 @@ export async function answerQuestion({ } } else if (event.toolName === "list_codebase_files") { tracker.recordListDir(); + } else if (event.toolName === "search_github") { + tracker.recordGitHubSearch(); + } else if (event.toolName === "get_github_issue_or_pr") { + tracker.recordGitHubRead(); } } } diff --git a/services/ask-ai-bot/utils/ai/system-prompt.ts b/services/ask-ai-bot/utils/ai/system-prompt.ts index 35eeffafd440..82bcbebd7d80 100644 --- a/services/ask-ai-bot/utils/ai/system-prompt.ts +++ b/services/ask-ai-bot/utils/ai/system-prompt.ts @@ -1,6 +1,6 @@ import dedent from "dedent"; -export const MAX_STEPS = 25; +export const MAX_STEPS = 35; export function buildSystemPrompt(serverContext?: string): string { let prompt = dedent`You are a helpful assistant in the goose Discord server. @@ -22,7 +22,13 @@ When answering questions about how goose works internally, its architecture, imp 3. Use \`view_codebase\` to read the actual source code files 4. Cite the source file in your response (using its GitHub URL) -You can combine documentation and codebase tools in a single response when needed. For example, if a user asks how a feature works, you might search the docs for usage instructions AND search the codebase for the implementation. +## GitHub tools +When answering questions about specific issues, bug reports, feature requests, or the development history of goose: +1. Use \`search_github\` to find relevant issues and PRs - you can use GitHub qualifiers (e.g., \`label:bug\`, \`is:pr\`, \`author:username\`) and sort by recency (\`sort: "updated"\`) or other criteria +2. Use \`get_github_issue_or_pr\` to read the full description and comments of a specific issue or PR +3. Cite the issue URL in your response + +You can combine documentation, codebase, and GitHub tools in a single response when needed. For example, if a user asks how a feature works, you might search the docs for usage instructions, search the codebase for the implementation, and read related GitHub issues or PRs. Be thorough! When providing links, wrap the URL in angle brackets (e.g., \`\` or \`[Example]()\`) to prevent excessive link previews. Do not use backtick characters around the URL.`; diff --git a/services/ask-ai-bot/utils/ai/tool-tracker.ts b/services/ask-ai-bot/utils/ai/tool-tracker.ts index 1fd6c9426d70..35ca2bc7be91 100644 --- a/services/ask-ai-bot/utils/ai/tool-tracker.ts +++ b/services/ask-ai-bot/utils/ai/tool-tracker.ts @@ -6,6 +6,8 @@ export class ToolTracker { private codeSearchResults: number = 0; private viewedCodePaths: Set = new Set(); private listedDirs: number = 0; + private gitHubSearchCalls: number = 0; + private gitHubReadCalls: number = 0; recordSearchCall(results: string[]): void { this.docSearchCalls++; @@ -31,6 +33,14 @@ export class ToolTracker { this.listedDirs++; } + recordGitHubSearch(): void { + this.gitHubSearchCalls++; + } + + recordGitHubRead(): void { + this.gitHubReadCalls++; + } + getSummary(): string { const parts: string[] = []; @@ -63,6 +73,21 @@ export class ToolTracker { parts.push(`viewed ${fileCount} source ${filesText}`); } + if (this.listedDirs > 0) { + const timesText = this.listedDirs === 1 ? "time" : "times"; + parts.push(`listed directories ${this.listedDirs} ${timesText}`); + } + + if (this.gitHubSearchCalls > 0) { + const timesText = this.gitHubSearchCalls === 1 ? "time" : "times"; + parts.push(`searched GitHub ${this.gitHubSearchCalls} ${timesText}`); + } + + if (this.gitHubReadCalls > 0) { + const itemText = this.gitHubReadCalls === 1 ? "item" : "items"; + parts.push(`read ${this.gitHubReadCalls} GitHub ${itemText}`); + } + if (parts.length === 0) return ""; const firstPart = parts[0].charAt(0).toUpperCase() + parts[0].slice(1); diff --git a/services/ask-ai-bot/utils/ai/tools/github.ts b/services/ask-ai-bot/utils/ai/tools/github.ts new file mode 100644 index 000000000000..329d32809df0 --- /dev/null +++ b/services/ask-ai-bot/utils/ai/tools/github.ts @@ -0,0 +1,132 @@ +import { Octokit } from "@octokit/rest"; + +const REPO_OWNER = "aaif-goose"; +const REPO_NAME = "goose"; + +let octokit: Octokit | null = null; + +function getOctokit(): Octokit { + if (!octokit) { + octokit = new Octokit({ + auth: process.env.GITHUB_TOKEN, + }); + } + return octokit; +} + +export interface GitHubItem { + number: number; + title: string; + state: string; + isMerged: boolean; + author: string; + createdAt: string; + updatedAt: string; + labels: string[]; + body: string; + comments: number; + url: string; +} + +export interface GitHubComment { + author: string; + createdAt: string; + body: string; +} + +export async function searchGitHub( + query: string, + options: { + sort?: "created" | "updated" | "comments"; + order?: "asc" | "desc"; + state?: "open" | "closed" | "all"; + limit?: number; + } = {}, +): Promise { + const { sort, order = "desc", state = "all", limit = 10 } = options; + const api = getOctokit(); + + const sanitized = query.replace(/\b(?:repo|org|user):\S+/gi, "").trim(); + const q = `repo:${REPO_OWNER}/${REPO_NAME} ${sanitized}${state !== "all" ? ` state:${state}` : ""}`; + + const response = await api.rest.search.issuesAndPullRequests({ + q, + ...(sort ? { sort, order } : {}), + per_page: limit, + }); + + return response.data.items.map((item: any) => ({ + number: item.number, + title: item.title, + state: item.state, + isMerged: !!item.pull_request?.merged_at, + author: item.user?.login ?? "unknown", + createdAt: item.created_at, + updatedAt: item.updated_at, + labels: item.labels.map((l: any) => + typeof l === "string" ? l : (l.name ?? ""), + ), + body: item.body ?? "", + comments: item.comments, + url: item.html_url, + })); +} + +export async function getGitHubItem(number: number): Promise { + const api = getOctokit(); + + const response = await api.rest.issues.get({ + owner: REPO_OWNER, + repo: REPO_NAME, + issue_number: number, + }); + + const item: any = response.data; + return { + number: item.number, + title: item.title, + state: item.state, + isMerged: !!item.pull_request?.merged_at, + author: item.user?.login ?? "unknown", + createdAt: item.created_at, + updatedAt: item.updated_at, + labels: item.labels.map((l: any) => + typeof l === "string" ? l : (l.name ?? ""), + ), + body: item.body ?? "", + comments: item.comments, + url: item.html_url, + }; +} + +export async function getGitHubItemComments( + number: number, + limit: number = 30, +): Promise { + const api = getOctokit(); + const perPage = Math.min(limit, 100); + const comments: GitHubComment[] = []; + + for (let page = 1; comments.length < limit; page++) { + const response = await api.rest.issues.listComments({ + owner: REPO_OWNER, + repo: REPO_NAME, + issue_number: number, + per_page: perPage, + page, + }); + + if (response.data.length === 0) break; + + for (const comment of response.data) { + comments.push({ + author: comment.user?.login ?? "unknown", + createdAt: comment.created_at, + body: comment.body ?? "", + }); + if (comments.length >= limit) break; + } + } + + return comments; +} diff --git a/services/ask-ai-bot/utils/ai/tools/index.ts b/services/ask-ai-bot/utils/ai/tools/index.ts index 79b46e24d0fd..491cce0fd8fd 100644 --- a/services/ask-ai-bot/utils/ai/tools/index.ts +++ b/services/ask-ai-bot/utils/ai/tools/index.ts @@ -5,6 +5,11 @@ import { listCodebaseFiles, searchCodebase } from "./codebase-search"; import { viewCodebaseFiles } from "./codebase-viewer"; import { searchDocs } from "./docs-search"; import { viewDocs } from "./docs-viewer"; +import { getGitHubItem, getGitHubItemComments, searchGitHub } from "./github"; + +function truncateBody(body: string, maxLen: number = 500): string { + return body.length > maxLen ? body.slice(0, maxLen) + "..." : body; +} export const aiTools = { search_docs: tool({ @@ -173,4 +178,138 @@ export const aiTools = { } }, }), + search_github: tool({ + description: + "Search GitHub issues and pull requests in the aaif-goose/goose repository. Use this to find bugs, feature requests, or discussions. Results can be sorted by recency, relevance, or comment count.", + inputSchema: z.object({ + query: z + .string() + .describe( + "Search query (supports GitHub qualifiers like 'label:bug', 'is:issue', 'is:pr', 'author:username')", + ), + sort: z + .enum(["created", "updated", "comments"]) + .optional() + .describe( + "Sort by created date, last updated, or comment count. Omit for relevance-based sorting (default).", + ), + order: z + .enum(["asc", "desc"]) + .optional() + .describe("Sort order (default: desc)"), + state: z + .enum(["open", "closed", "all"]) + .optional() + .describe("Filter by issue state (default: all)"), + limit: z + .number() + .optional() + .describe("Maximum number of results (default 10)"), + }), + execute: async ({ query, sort, order, state, limit }) => { + try { + const results = await searchGitHub(query, { + sort, + order, + state, + limit, + }); + + if (results.length === 0) { + return "Nothing found on GitHub matching your query. Try different keywords."; + } + + return results + .map((r) => { + const status = + r.state === "closed" + ? r.isMerged + ? "merged" + : "closed" + : r.state; + return ( + `**#${r.number}** (${status}) - ${r.title}\n` + + `Author: ${r.author} | Created: ${r.createdAt.slice(0, 10)} | Comments: ${r.comments}\n` + + `Labels: ${r.labels.join(", ") || "none"}\n` + + `${r.body ? truncateBody(r.body) + "\n" : ""}` + + `URL: <${r.url}>` + ); + }) + .join("\n\n"); + } catch (error) { + const errorMsg = + error instanceof Error ? error.message : "Unknown error"; + logger.error(`Error searching GitHub: ${errorMsg}`); + return `Error searching GitHub: ${errorMsg}`; + } + }, + }), + get_github_issue_or_pr: tool({ + description: + "Get detailed information about a specific GitHub issue or pull request in the aaif-goose/goose repository, including its description and comments.", + inputSchema: z.object({ + issueNumber: z.number().describe("The issue or pull request number"), + includeComments: z + .boolean() + .optional() + .describe( + "Whether to include comments in the response (default: true)", + ), + commentLimit: z + .number() + .optional() + .describe("Maximum number of comments to fetch (default: 30)"), + }), + execute: async ({ + issueNumber, + includeComments = true, + commentLimit = 30, + }) => { + try { + const item = await getGitHubItem(issueNumber); + + const status = + item.state === "closed" + ? item.isMerged + ? "merged" + : "closed" + : item.state; + let result = + `## #${item.number}: ${item.title}\n` + + `**State:** ${status} | **Author:** ${item.author}\n` + + `**Created:** ${item.createdAt} | **Updated:** ${item.updatedAt}\n` + + `**Labels:** ${item.labels.join(", ") || "none"}\n` + + `**URL:** <${item.url}>\n\n`; + + if (item.body) { + result += `**Description:**\n${item.body.slice(0, 4000)}\n`; + } + + if (includeComments && item.comments > 0) { + const comments = await getGitHubItemComments( + issueNumber, + commentLimit, + ); + result += `\n**Comments (${comments.length}):**\n`; + result += comments + .slice(0, commentLimit) + .map( + (c) => + `\n**${c.author}** (${c.createdAt.slice(0, 10)}):\n${c.body.slice(0, 1500)}`, + ) + .join("\n---"); + if (item.comments > comments.length) { + result += `\n\n... and ${item.comments - comments.length} more comments. Use get_github_issue_or_pr with includeComments=true and a larger commentLimit to fetch more.`; + } + } + + return result; + } catch (error) { + const errorMsg = + error instanceof Error ? error.message : "Unknown error"; + logger.error(`Error getting GitHub item: ${errorMsg}`); + return `Error getting item: ${errorMsg}`; + } + }, + }), };