feat: allow askai bot to read issues and PRs (#10398)
Signed-off-by: BestCodes <106822363+The-Best-Codes@users.noreply.github.com>
This commit is contained in:
@@ -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=../..
|
||||
|
||||
@@ -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=="],
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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., \`<https://example.com>\` or \`[Example](<https://example.com>)\`) to prevent excessive link previews. Do not use backtick characters around the URL.`;
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ export class ToolTracker {
|
||||
private codeSearchResults: number = 0;
|
||||
private viewedCodePaths: Set<string> = 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);
|
||||
|
||||
@@ -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<GitHubItem[]> {
|
||||
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<GitHubItem> {
|
||||
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<GitHubComment[]> {
|
||||
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;
|
||||
}
|
||||
@@ -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}`;
|
||||
}
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user