terramend 0.2.0
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/LICENSE +661 -0
- package/README.md +145 -0
- package/dist/agents/claude.d.ts +73 -0
- package/dist/agents/claudePretoolGate.d.ts +99 -0
- package/dist/agents/gateServer.d.ts +7 -0
- package/dist/agents/index.d.ts +6 -0
- package/dist/agents/nativeFsDenies.d.ts +28 -0
- package/dist/agents/opencode.d.ts +231 -0
- package/dist/agents/opencodePlugin.d.ts +85 -0
- package/dist/agents/opencodeShared.d.ts +40 -0
- package/dist/agents/postRun.d.ts +132 -0
- package/dist/agents/reviewer.d.ts +38 -0
- package/dist/agents/sessionLabeler.d.ts +97 -0
- package/dist/agents/shared.d.ts +189 -0
- package/dist/agents/subagentModels.d.ts +19 -0
- package/dist/agents/subagentToolGates.d.ts +55 -0
- package/dist/cli.mjs +197426 -0
- package/dist/external.d.ts +227 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +196783 -0
- package/dist/internal/index.d.ts +18 -0
- package/dist/internal.js +1714 -0
- package/dist/lifecycle.d.ts +2 -0
- package/dist/main.d.ts +8 -0
- package/dist/mcp/arkConfig.d.ts +1 -0
- package/dist/mcp/checkSuite.d.ts +25 -0
- package/dist/mcp/checkout.d.ts +77 -0
- package/dist/mcp/comment.d.ts +119 -0
- package/dist/mcp/commitInfo.d.ts +9 -0
- package/dist/mcp/crosswalk.d.ts +105 -0
- package/dist/mcp/dependencies.d.ts +8 -0
- package/dist/mcp/geminiSanitizer.d.ts +28 -0
- package/dist/mcp/git.d.ts +46 -0
- package/dist/mcp/guardrails.d.ts +104 -0
- package/dist/mcp/issue.d.ts +18 -0
- package/dist/mcp/issueComments.d.ts +9 -0
- package/dist/mcp/issueEvents.d.ts +9 -0
- package/dist/mcp/issueInfo.d.ts +9 -0
- package/dist/mcp/labels.d.ts +12 -0
- package/dist/mcp/localContext.d.ts +19 -0
- package/dist/mcp/moduleExtraction.d.ts +71 -0
- package/dist/mcp/moduleTests.d.ts +104 -0
- package/dist/mcp/modules.d.ts +179 -0
- package/dist/mcp/output.d.ts +12 -0
- package/dist/mcp/pathSafety.d.ts +14 -0
- package/dist/mcp/policy.d.ts +48 -0
- package/dist/mcp/pr.d.ts +49 -0
- package/dist/mcp/prInfo.d.ts +9 -0
- package/dist/mcp/providerSchema.d.ts +50 -0
- package/dist/mcp/review.d.ts +199 -0
- package/dist/mcp/reviewComments.d.ts +178 -0
- package/dist/mcp/roots.d.ts +58 -0
- package/dist/mcp/scope.d.ts +15 -0
- package/dist/mcp/selectMode.d.ts +18 -0
- package/dist/mcp/server.d.ts +48 -0
- package/dist/mcp/shared.d.ts +47 -0
- package/dist/mcp/shell.d.ts +37 -0
- package/dist/mcp/staleFix.d.ts +51 -0
- package/dist/mcp/terraform/cost.d.ts +55 -0
- package/dist/mcp/terraform/currency.d.ts +94 -0
- package/dist/mcp/terraform/decisions.d.ts +178 -0
- package/dist/mcp/terraform/findings.d.ts +75 -0
- package/dist/mcp/terraform/plan.d.ts +157 -0
- package/dist/mcp/terraform/scanners.d.ts +131 -0
- package/dist/mcp/terraform/tools.d.ts +63 -0
- package/dist/mcp/terraform/types.d.ts +172 -0
- package/dist/mcp/terraform.d.ts +22 -0
- package/dist/mcp/terratest.d.ts +83 -0
- package/dist/mcp/upload.d.ts +6 -0
- package/dist/models.d.ts +171 -0
- package/dist/modes.d.ts +26 -0
- package/dist/prep/index.d.ts +7 -0
- package/dist/prep/installNodeDependencies.d.ts +2 -0
- package/dist/prep/installPythonDependencies.d.ts +2 -0
- package/dist/prep/types.d.ts +31 -0
- package/dist/reviewQuality.d.ts +64 -0
- package/dist/skills/terraform-best-practices/SKILL.md +369 -0
- package/dist/toolState.d.ts +135 -0
- package/dist/utils/activity.d.ts +40 -0
- package/dist/utils/agent.d.ts +20 -0
- package/dist/utils/agentHangReport.d.ts +38 -0
- package/dist/utils/apiFetch.d.ts +19 -0
- package/dist/utils/apiKeys.d.ts +41 -0
- package/dist/utils/apiUrl.d.ts +20 -0
- package/dist/utils/assets.d.ts +8 -0
- package/dist/utils/billingErrors.d.ts +85 -0
- package/dist/utils/body.d.ts +34 -0
- package/dist/utils/buildTerramendFooter.d.ts +25 -0
- package/dist/utils/byokFallback.d.ts +85 -0
- package/dist/utils/claudeSubscription.d.ts +30 -0
- package/dist/utils/cli.d.ts +10 -0
- package/dist/utils/codexHome.d.ts +29 -0
- package/dist/utils/codexOAuth.d.ts +60 -0
- package/dist/utils/diffCoverage.d.ts +63 -0
- package/dist/utils/errorReport.d.ts +17 -0
- package/dist/utils/exitHandler.d.ts +8 -0
- package/dist/utils/fixDoubleEscapedString.d.ts +1 -0
- package/dist/utils/gitAuth.d.ts +84 -0
- package/dist/utils/gitAuthServer.d.ts +24 -0
- package/dist/utils/github.d.ts +78 -0
- package/dist/utils/globals.d.ts +3 -0
- package/dist/utils/install.d.ts +60 -0
- package/dist/utils/instructions.d.ts +48 -0
- package/dist/utils/leapingComment.d.ts +11 -0
- package/dist/utils/learnings.d.ts +62 -0
- package/dist/utils/learningsTruncate.d.ts +25 -0
- package/dist/utils/lifecycle.d.ts +57 -0
- package/dist/utils/log.d.ts +111 -0
- package/dist/utils/normalizeEnv.d.ts +30 -0
- package/dist/utils/openCodeModels.d.ts +11 -0
- package/dist/utils/overrides.d.ts +40 -0
- package/dist/utils/packageManager.d.ts +49 -0
- package/dist/utils/patchWorkflowRunFields.d.ts +29 -0
- package/dist/utils/payload.d.ts +105 -0
- package/dist/utils/prSummary.d.ts +61 -0
- package/dist/utils/progressComment.d.ts +146 -0
- package/dist/utils/providerErrors.d.ts +31 -0
- package/dist/utils/rangeDiff.d.ts +51 -0
- package/dist/utils/remediationCommand.d.ts +55 -0
- package/dist/utils/retry.d.ts +13 -0
- package/dist/utils/reviewCleanup.d.ts +14 -0
- package/dist/utils/run.d.ts +9 -0
- package/dist/utils/runContext.d.ts +60 -0
- package/dist/utils/runContextData.d.ts +23 -0
- package/dist/utils/runErrorRenderer.d.ts +64 -0
- package/dist/utils/runLifecycle.d.ts +86 -0
- package/dist/utils/runStartupLog.d.ts +15 -0
- package/dist/utils/secrets.d.ts +22 -0
- package/dist/utils/setup.d.ts +90 -0
- package/dist/utils/shell.d.ts +32 -0
- package/dist/utils/skills.d.ts +10 -0
- package/dist/utils/subprocess.d.ts +80 -0
- package/dist/utils/terraformMcp.d.ts +42 -0
- package/dist/utils/time.d.ts +15 -0
- package/dist/utils/timer.d.ts +23 -0
- package/dist/utils/todoTracking.d.ts +16 -0
- package/dist/utils/token.d.ts +39 -0
- package/dist/utils/version.d.ts +2 -0
- package/dist/utils/versioning.d.ts +7 -0
- package/dist/utils/vertex.d.ts +16 -0
- package/dist/utils/workflow.d.ts +13 -0
- package/package.json +119 -0
- package/src/agents/claude.test.ts +1016 -0
- package/src/agents/claude.ts +1246 -0
- package/src/agents/claudePretoolGate.test.ts +28 -0
- package/src/agents/claudePretoolGate.ts +173 -0
- package/src/agents/gateServer.test.ts +204 -0
- package/src/agents/gateServer.ts +124 -0
- package/src/agents/index.ts +10 -0
- package/src/agents/nativeFsDenies.ts +82 -0
- package/src/agents/opencode.test.ts +1440 -0
- package/src/agents/opencode.ts +1312 -0
- package/src/agents/opencodePlugin.ts +222 -0
- package/src/agents/opencodeShared.test.ts +34 -0
- package/src/agents/opencodeShared.ts +121 -0
- package/src/agents/postRun.test.ts +549 -0
- package/src/agents/postRun.ts +535 -0
- package/src/agents/reviewer.ts +104 -0
- package/src/agents/sessionLabeler.test.ts +247 -0
- package/src/agents/sessionLabeler.ts +178 -0
- package/src/agents/shared.test.ts +76 -0
- package/src/agents/shared.ts +292 -0
- package/src/agents/subagentModels.test.ts +113 -0
- package/src/agents/subagentModels.ts +40 -0
- package/src/agents/subagentRegistration.test.ts +41 -0
- package/src/agents/subagentToolGates.ts +114 -0
- package/src/cli.test.ts +129 -0
- package/src/cli.ts +105 -0
- package/src/commands/gha.test.ts +192 -0
- package/src/commands/gha.ts +188 -0
- package/src/commands/mcp.ts +122 -0
- package/src/config.ts +1 -0
- package/src/entry.ts +7 -0
- package/src/entryPost.stdlibOnly.test.ts +109 -0
- package/src/entryPost.ts +99 -0
- package/src/external.test.ts +16 -0
- package/src/external.ts +302 -0
- package/src/index.ts +11 -0
- package/src/internal/index.ts +71 -0
- package/src/lifecycle.ts +2 -0
- package/src/main.test.ts +873 -0
- package/src/main.ts +712 -0
- package/src/mcp/__fixtures__/terramend-scratch-pr-49-review-3485940013.json +110 -0
- package/src/mcp/__fixtures__/terramend-scratch-pr-64-review-3531000326.json +14 -0
- package/src/mcp/__fixtures__/terramend-test-repo-pr-1.diff.json +67 -0
- package/src/mcp/__snapshots__/checkout.test.ts.snap +109 -0
- package/src/mcp/__snapshots__/reviewComments.test.ts.snap +71 -0
- package/src/mcp/arkConfig.ts +7 -0
- package/src/mcp/checkSuite.test.ts +245 -0
- package/src/mcp/checkSuite.ts +255 -0
- package/src/mcp/checkout.test.ts +752 -0
- package/src/mcp/checkout.ts +886 -0
- package/src/mcp/comment.test.ts +772 -0
- package/src/mcp/comment.ts +582 -0
- package/src/mcp/commitInfo.test.ts +127 -0
- package/src/mcp/commitInfo.ts +61 -0
- package/src/mcp/crosswalk.test.ts +106 -0
- package/src/mcp/crosswalk.ts +339 -0
- package/src/mcp/dependencies.test.ts +309 -0
- package/src/mcp/dependencies.ts +189 -0
- package/src/mcp/geminiSanitizer.test.ts +287 -0
- package/src/mcp/geminiSanitizer.ts +207 -0
- package/src/mcp/git.test.ts +1083 -0
- package/src/mcp/git.ts +890 -0
- package/src/mcp/guardrails.test.ts +705 -0
- package/src/mcp/guardrails.ts +465 -0
- package/src/mcp/issue.test.ts +113 -0
- package/src/mcp/issue.ts +73 -0
- package/src/mcp/issueComments.test.ts +69 -0
- package/src/mcp/issueComments.ts +48 -0
- package/src/mcp/issueEvents.test.ts +134 -0
- package/src/mcp/issueEvents.ts +100 -0
- package/src/mcp/issueInfo.test.ts +104 -0
- package/src/mcp/issueInfo.ts +72 -0
- package/src/mcp/labels.test.ts +52 -0
- package/src/mcp/labels.ts +34 -0
- package/src/mcp/localContext.ts +28 -0
- package/src/mcp/localServer.test.ts +75 -0
- package/src/mcp/localServer.ts +131 -0
- package/src/mcp/moduleExtraction.test.ts +261 -0
- package/src/mcp/moduleExtraction.ts +313 -0
- package/src/mcp/moduleTests.test.ts +269 -0
- package/src/mcp/moduleTests.ts +421 -0
- package/src/mcp/modules.test.ts +640 -0
- package/src/mcp/modules.ts +696 -0
- package/src/mcp/output.test.ts +96 -0
- package/src/mcp/output.ts +70 -0
- package/src/mcp/pathSafety.test.ts +44 -0
- package/src/mcp/pathSafety.ts +28 -0
- package/src/mcp/policy.test.ts +282 -0
- package/src/mcp/policy.ts +199 -0
- package/src/mcp/pr.test.ts +387 -0
- package/src/mcp/pr.ts +194 -0
- package/src/mcp/prInfo.test.ts +96 -0
- package/src/mcp/prInfo.ts +91 -0
- package/src/mcp/providerSchema.test.ts +85 -0
- package/src/mcp/providerSchema.ts +175 -0
- package/src/mcp/review.test.ts +936 -0
- package/src/mcp/review.ts +923 -0
- package/src/mcp/reviewComments.test.ts +549 -0
- package/src/mcp/reviewComments.ts +896 -0
- package/src/mcp/roots.test.ts +175 -0
- package/src/mcp/roots.ts +217 -0
- package/src/mcp/scope.test.ts +59 -0
- package/src/mcp/scope.ts +65 -0
- package/src/mcp/security.test.ts +720 -0
- package/src/mcp/selectMode.test.ts +210 -0
- package/src/mcp/selectMode.ts +181 -0
- package/src/mcp/server.test.ts +292 -0
- package/src/mcp/server.ts +403 -0
- package/src/mcp/shared.ts +100 -0
- package/src/mcp/shell.test.ts +520 -0
- package/src/mcp/shell.ts +505 -0
- package/src/mcp/staleFix.test.ts +237 -0
- package/src/mcp/staleFix.ts +277 -0
- package/src/mcp/terraform/cost.ts +163 -0
- package/src/mcp/terraform/currency.test.ts +338 -0
- package/src/mcp/terraform/currency.ts +336 -0
- package/src/mcp/terraform/decisions.ts +527 -0
- package/src/mcp/terraform/findings.ts +333 -0
- package/src/mcp/terraform/plan.ts +348 -0
- package/src/mcp/terraform/scanners.ts +809 -0
- package/src/mcp/terraform/tools.test.ts +1071 -0
- package/src/mcp/terraform/tools.ts +908 -0
- package/src/mcp/terraform/types.ts +305 -0
- package/src/mcp/terraform.test.ts +1957 -0
- package/src/mcp/terraform.ts +23 -0
- package/src/mcp/terratest.test.ts +105 -0
- package/src/mcp/terratest.ts +196 -0
- package/src/mcp/toolFiltering.test.ts +85 -0
- package/src/mcp/upload.test.ts +180 -0
- package/src/mcp/upload.ts +112 -0
- package/src/models.test.ts +300 -0
- package/src/models.ts +708 -0
- package/src/modes.test.ts +107 -0
- package/src/modes.ts +880 -0
- package/src/prep/index.ts +43 -0
- package/src/prep/installNodeDependencies.test.ts +298 -0
- package/src/prep/installNodeDependencies.ts +196 -0
- package/src/prep/installPythonDependencies.test.ts +268 -0
- package/src/prep/installPythonDependencies.ts +199 -0
- package/src/prep/types.ts +38 -0
- package/src/reviewQuality.test.ts +63 -0
- package/src/reviewQuality.ts +134 -0
- package/src/runCli.test.ts +214 -0
- package/src/runCli.ts +282 -0
- package/src/skills/terraform-best-practices/SKILL.md +369 -0
- package/src/toolState.test.ts +45 -0
- package/src/toolState.ts +252 -0
- package/src/utils/activity.test.ts +188 -0
- package/src/utils/activity.ts +210 -0
- package/src/utils/agent.test.ts +251 -0
- package/src/utils/agent.ts +139 -0
- package/src/utils/agentHangReport.test.ts +203 -0
- package/src/utils/agentHangReport.ts +170 -0
- package/src/utils/apiFetch.test.ts +115 -0
- package/src/utils/apiFetch.ts +62 -0
- package/src/utils/apiKeys.test.ts +344 -0
- package/src/utils/apiKeys.ts +206 -0
- package/src/utils/apiUrl.test.ts +30 -0
- package/src/utils/apiUrl.ts +59 -0
- package/src/utils/assets.test.ts +153 -0
- package/src/utils/assets.ts +107 -0
- package/src/utils/billingErrors.test.ts +121 -0
- package/src/utils/billingErrors.ts +189 -0
- package/src/utils/body.test.ts +217 -0
- package/src/utils/body.ts +168 -0
- package/src/utils/buildTerramendFooter.test.ts +38 -0
- package/src/utils/buildTerramendFooter.ts +82 -0
- package/src/utils/byokFallback.test.ts +205 -0
- package/src/utils/byokFallback.ts +128 -0
- package/src/utils/claudeSubscription.test.ts +179 -0
- package/src/utils/claudeSubscription.ts +93 -0
- package/src/utils/cli.ts +31 -0
- package/src/utils/codexHome.test.ts +190 -0
- package/src/utils/codexHome.ts +191 -0
- package/src/utils/codexOAuth.ts +147 -0
- package/src/utils/codexRefreshDetect.test.ts +85 -0
- package/src/utils/codexRefreshDetect.ts +35 -0
- package/src/utils/diffCoverage.test.ts +468 -0
- package/src/utils/diffCoverage.ts +404 -0
- package/src/utils/errorReport.test.ts +135 -0
- package/src/utils/errorReport.ts +83 -0
- package/src/utils/exitHandler.ts +35 -0
- package/src/utils/fixDoubleEscapedString.ts +9 -0
- package/src/utils/ghaCore.ts +13 -0
- package/src/utils/gitAuth.test.ts +322 -0
- package/src/utils/gitAuth.ts +263 -0
- package/src/utils/gitAuthServer.test.ts +260 -0
- package/src/utils/gitAuthServer.ts +182 -0
- package/src/utils/github.test.ts +615 -0
- package/src/utils/github.ts +538 -0
- package/src/utils/globals.ts +9 -0
- package/src/utils/humanEditCapture.test.ts +100 -0
- package/src/utils/humanEditCapture.ts +193 -0
- package/src/utils/install.test.ts +768 -0
- package/src/utils/install.ts +492 -0
- package/src/utils/instructions.test.ts +240 -0
- package/src/utils/instructions.ts +543 -0
- package/src/utils/leapingComment.test.ts +51 -0
- package/src/utils/leapingComment.ts +18 -0
- package/src/utils/learnings.test.ts +87 -0
- package/src/utils/learnings.ts +138 -0
- package/src/utils/learningsTocRender.test.ts +116 -0
- package/src/utils/learningsTruncate.test.ts +39 -0
- package/src/utils/learningsTruncate.ts +42 -0
- package/src/utils/lifecycle.test.ts +195 -0
- package/src/utils/lifecycle.ts +198 -0
- package/src/utils/log.test.ts +402 -0
- package/src/utils/log.ts +432 -0
- package/src/utils/normalizeEnv.test.ts +91 -0
- package/src/utils/normalizeEnv.ts +106 -0
- package/src/utils/openCodeModels.ts +82 -0
- package/src/utils/overrides.test.ts +89 -0
- package/src/utils/overrides.ts +98 -0
- package/src/utils/packageManager.test.ts +321 -0
- package/src/utils/packageManager.ts +257 -0
- package/src/utils/patchWorkflowRunFields.test.ts +92 -0
- package/src/utils/patchWorkflowRunFields.ts +150 -0
- package/src/utils/payload.test.ts +497 -0
- package/src/utils/payload.ts +371 -0
- package/src/utils/postApiFetch.ts +51 -0
- package/src/utils/prSummary.test.ts +224 -0
- package/src/utils/prSummary.ts +147 -0
- package/src/utils/progressComment.ts +261 -0
- package/src/utils/providerErrors.test.ts +315 -0
- package/src/utils/providerErrors.ts +172 -0
- package/src/utils/rangeDiff.test.ts +236 -0
- package/src/utils/rangeDiff.ts +182 -0
- package/src/utils/remediationCommand.test.ts +163 -0
- package/src/utils/remediationCommand.ts +119 -0
- package/src/utils/retry.test.ts +153 -0
- package/src/utils/retry.ts +58 -0
- package/src/utils/reviewCleanup.ts +106 -0
- package/src/utils/run.ts +99 -0
- package/src/utils/runContext.ts +145 -0
- package/src/utils/runContextData.ts +58 -0
- package/src/utils/runErrorRenderer.test.ts +95 -0
- package/src/utils/runErrorRenderer.ts +259 -0
- package/src/utils/runFixture.ts +76 -0
- package/src/utils/runLifecycle.ts +237 -0
- package/src/utils/runStartupLog.ts +60 -0
- package/src/utils/secrets.test.ts +103 -0
- package/src/utils/secrets.ts +177 -0
- package/src/utils/setup.test.ts +509 -0
- package/src/utils/setup.ts +352 -0
- package/src/utils/shell.ts +103 -0
- package/src/utils/skills.test.ts +46 -0
- package/src/utils/skills.ts +67 -0
- package/src/utils/subprocess.test.ts +170 -0
- package/src/utils/subprocess.ts +438 -0
- package/src/utils/terraformMcp.test.ts +63 -0
- package/src/utils/terraformMcp.ts +83 -0
- package/src/utils/time.test.ts +105 -0
- package/src/utils/time.ts +59 -0
- package/src/utils/timer.test.ts +91 -0
- package/src/utils/timer.ts +72 -0
- package/src/utils/todoTracking.test.ts +223 -0
- package/src/utils/todoTracking.ts +167 -0
- package/src/utils/token.test.ts +239 -0
- package/src/utils/token.ts +186 -0
- package/src/utils/version.ts +10 -0
- package/src/utils/versioning.test.ts +34 -0
- package/src/utils/versioning.ts +44 -0
- package/src/utils/vertex.ts +85 -0
- package/src/utils/workflow.ts +25 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { GetIssueCommentsTool } from "#app/mcp/issueComments";
|
|
3
|
+
import type { ToolContext } from "#app/mcp/server";
|
|
4
|
+
import { initToolState, type ToolState } from "#app/toolState";
|
|
5
|
+
|
|
6
|
+
type ToolResultShape = { content: [{ type: "text"; text: string }]; isError?: boolean };
|
|
7
|
+
|
|
8
|
+
async function runTool(t: { execute?: unknown }, params: unknown): Promise<ToolResultShape> {
|
|
9
|
+
const exec = t.execute as (p: unknown, c: unknown) => Promise<ToolResultShape>;
|
|
10
|
+
return exec(params, {});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function makeCtx(comments: Record<string, unknown>[]) {
|
|
14
|
+
const listComments = vi.fn();
|
|
15
|
+
const paginate = vi.fn(async (_route: unknown, _params: unknown) => comments);
|
|
16
|
+
const toolState: ToolState = initToolState({ progressComment: undefined });
|
|
17
|
+
const ctx = {
|
|
18
|
+
octokit: { paginate, rest: { issues: { listComments } } },
|
|
19
|
+
repo: { owner: "octo", name: "repo" },
|
|
20
|
+
toolState,
|
|
21
|
+
tmpdir: "/tmp",
|
|
22
|
+
githubInstallationToken: "tok",
|
|
23
|
+
} as unknown as ToolContext;
|
|
24
|
+
return { ctx, paginate, listComments, toolState };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("GetIssueCommentsTool", () => {
|
|
28
|
+
it("returns all comments with authors and records the issue number", async () => {
|
|
29
|
+
const { ctx, paginate, listComments, toolState } = makeCtx([
|
|
30
|
+
{ id: 1, body: "first comment", body_html: "<p>first comment</p>", user: { login: "alice" } },
|
|
31
|
+
{ id: 2, body: null, user: undefined },
|
|
32
|
+
]);
|
|
33
|
+
const result = await runTool(GetIssueCommentsTool(ctx), { issue_number: 7 });
|
|
34
|
+
|
|
35
|
+
expect(result.isError).toBeUndefined();
|
|
36
|
+
expect(toolState.issueNumber).toBe(7);
|
|
37
|
+
expect(paginate).toHaveBeenCalledWith(
|
|
38
|
+
listComments,
|
|
39
|
+
expect.objectContaining({
|
|
40
|
+
owner: "octo",
|
|
41
|
+
repo: "repo",
|
|
42
|
+
issue_number: 7,
|
|
43
|
+
headers: { accept: "application/vnd.github.full+json" },
|
|
44
|
+
}),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const text = result.content[0].text;
|
|
48
|
+
expect(text).toContain("count: 2");
|
|
49
|
+
expect(text).toContain("first comment");
|
|
50
|
+
expect(text).toContain("alice");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("handles an issue with no comments", async () => {
|
|
54
|
+
const { ctx } = makeCtx([]);
|
|
55
|
+
const result = await runTool(GetIssueCommentsTool(ctx), { issue_number: 7 });
|
|
56
|
+
|
|
57
|
+
expect(result.isError).toBeUndefined();
|
|
58
|
+
expect(result.content[0].text).toContain("count: 0");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("surfaces pagination failures as tool errors", async () => {
|
|
62
|
+
const { ctx, paginate } = makeCtx([]);
|
|
63
|
+
paginate.mockRejectedValueOnce(new Error("rate limited"));
|
|
64
|
+
const result = await runTool(GetIssueCommentsTool(ctx), { issue_number: 7 });
|
|
65
|
+
|
|
66
|
+
expect(result.isError).toBe(true);
|
|
67
|
+
expect(result.content[0].text).toContain("rate limited");
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type } from "arktype";
|
|
2
|
+
import type { ToolContext } from "#app/mcp/server";
|
|
3
|
+
import { execute, tool } from "#app/mcp/shared";
|
|
4
|
+
import { resolveBodyAssets } from "#app/utils/body";
|
|
5
|
+
|
|
6
|
+
export const GetIssueComments = type({
|
|
7
|
+
issue_number: type.number.describe("The issue number to get comments for"),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export function GetIssueCommentsTool(ctx: ToolContext) {
|
|
11
|
+
return tool({
|
|
12
|
+
name: "get_issue_comments",
|
|
13
|
+
description:
|
|
14
|
+
"Get all comments for a GitHub issue. Returns all comments including the issue body and all subsequent discussion comments. " +
|
|
15
|
+
"Example: `get_issue_comments({ issue_number: 1234 })`.",
|
|
16
|
+
parameters: GetIssueComments,
|
|
17
|
+
execute: execute(async ({ issue_number }) => {
|
|
18
|
+
// set issue context
|
|
19
|
+
ctx.toolState.issueNumber = issue_number;
|
|
20
|
+
|
|
21
|
+
const comments = await ctx.octokit.paginate(ctx.octokit.rest.issues.listComments, {
|
|
22
|
+
owner: ctx.repo.owner,
|
|
23
|
+
repo: ctx.repo.name,
|
|
24
|
+
issue_number,
|
|
25
|
+
headers: { accept: "application/vnd.github.full+json" },
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const processedComments = await Promise.all(
|
|
29
|
+
comments.map(async (comment) => ({
|
|
30
|
+
id: comment.id,
|
|
31
|
+
body: await resolveBodyAssets({
|
|
32
|
+
body: comment.body,
|
|
33
|
+
bodyHtml: comment.body_html,
|
|
34
|
+
tmpdir: ctx.tmpdir,
|
|
35
|
+
githubToken: ctx.githubInstallationToken,
|
|
36
|
+
}),
|
|
37
|
+
user: comment.user?.login,
|
|
38
|
+
})),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
issue_number,
|
|
43
|
+
comments: processedComments,
|
|
44
|
+
count: processedComments.length,
|
|
45
|
+
};
|
|
46
|
+
}),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { GetIssueEventsTool } from "#app/mcp/issueEvents";
|
|
3
|
+
import type { ToolContext } from "#app/mcp/server";
|
|
4
|
+
import type { ToolState } from "#app/toolState";
|
|
5
|
+
|
|
6
|
+
type ToolResultShape = { content: [{ type: "text"; text: string }]; isError?: boolean };
|
|
7
|
+
|
|
8
|
+
async function runTool(t: { execute?: unknown }, params: unknown): Promise<ToolResultShape> {
|
|
9
|
+
const exec = t.execute as (p: unknown, c: unknown) => Promise<ToolResultShape>;
|
|
10
|
+
return exec(params, {});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function makeCtx(events: unknown[]): {
|
|
14
|
+
ctx: ToolContext;
|
|
15
|
+
toolState: ToolState;
|
|
16
|
+
paginate: ReturnType<typeof vi.fn>;
|
|
17
|
+
} {
|
|
18
|
+
const toolState = {} as ToolState;
|
|
19
|
+
const listEventsForTimeline = { endpoint: "timeline" };
|
|
20
|
+
const paginate = vi.fn(async (endpoint: unknown) => {
|
|
21
|
+
expect(endpoint).toBe(listEventsForTimeline);
|
|
22
|
+
return events;
|
|
23
|
+
});
|
|
24
|
+
const ctx = {
|
|
25
|
+
octokit: { paginate, rest: { issues: { listEventsForTimeline } } },
|
|
26
|
+
repo: { owner: "octo", name: "repo" },
|
|
27
|
+
toolState,
|
|
28
|
+
} as unknown as ToolContext;
|
|
29
|
+
return { ctx, toolState, paginate };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
vi.clearAllMocks();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("GetIssueEventsTool", () => {
|
|
37
|
+
it("records the issue number on tool state and queries the timeline", async () => {
|
|
38
|
+
const { ctx, toolState, paginate } = makeCtx([]);
|
|
39
|
+
const result = await runTool(GetIssueEventsTool(ctx), { issue_number: 17 });
|
|
40
|
+
|
|
41
|
+
expect(result.isError).toBeUndefined();
|
|
42
|
+
expect(toolState.issueNumber).toBe(17);
|
|
43
|
+
expect(paginate).toHaveBeenCalledWith(
|
|
44
|
+
expect.anything(),
|
|
45
|
+
expect.objectContaining({ owner: "octo", repo: "repo", issue_number: 17 }),
|
|
46
|
+
);
|
|
47
|
+
expect(result.content[0].text).toContain("count: 0");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("filters out events that are not cross_referenced/referenced", async () => {
|
|
51
|
+
const { ctx } = makeCtx([
|
|
52
|
+
{ event: "labeled", id: 1 },
|
|
53
|
+
{ event: "assigned", id: 2 },
|
|
54
|
+
{ not_an_event: true },
|
|
55
|
+
{ event: 42 },
|
|
56
|
+
]);
|
|
57
|
+
const result = await runTool(GetIssueEventsTool(ctx), { issue_number: 17 });
|
|
58
|
+
|
|
59
|
+
expect(result.content[0].text).toContain("events: []");
|
|
60
|
+
expect(result.content[0].text).toContain("count: 0");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("extracts cross-referenced issues and pull requests with actor fallbacks", async () => {
|
|
64
|
+
const { ctx } = makeCtx([
|
|
65
|
+
{
|
|
66
|
+
event: "cross_referenced",
|
|
67
|
+
actor: { login: "alice" },
|
|
68
|
+
created_at: "2026-06-01T00:00:00Z",
|
|
69
|
+
source: {
|
|
70
|
+
type: "issue",
|
|
71
|
+
issue: { number: 12, title: "linked issue", html_url: "https://gh/i/12" },
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
event: "cross_referenced",
|
|
76
|
+
user: { login: "bob" },
|
|
77
|
+
source: {
|
|
78
|
+
type: "issue",
|
|
79
|
+
pull_request: { number: 34, title: "linked PR", html_url: "https://gh/pr/34" },
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
// cross_referenced without a source object — only the base fields survive
|
|
83
|
+
{ event: "cross_referenced", id: 3 },
|
|
84
|
+
]);
|
|
85
|
+
const result = await runTool(GetIssueEventsTool(ctx), { issue_number: 17 });
|
|
86
|
+
|
|
87
|
+
const text = result.content[0].text;
|
|
88
|
+
expect(text).toContain("count: 3");
|
|
89
|
+
expect(text).toContain("actor: alice");
|
|
90
|
+
expect(text).toContain("actor: bob");
|
|
91
|
+
expect(text).toContain("linked issue");
|
|
92
|
+
expect(text).toContain("https://gh/i/12");
|
|
93
|
+
expect(text).toContain("linked PR");
|
|
94
|
+
expect(text).toContain("https://gh/pr/34");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("extracts commit references from referenced events", async () => {
|
|
98
|
+
const { ctx } = makeCtx([
|
|
99
|
+
{
|
|
100
|
+
event: "referenced",
|
|
101
|
+
id: 99,
|
|
102
|
+
actor: { login: "carol" },
|
|
103
|
+
created_at: "2026-06-02T00:00:00Z",
|
|
104
|
+
commit_id: "abc123",
|
|
105
|
+
commit_url: "https://gh/commit/abc123",
|
|
106
|
+
},
|
|
107
|
+
]);
|
|
108
|
+
const result = await runTool(GetIssueEventsTool(ctx), { issue_number: 17 });
|
|
109
|
+
|
|
110
|
+
const text = result.content[0].text;
|
|
111
|
+
expect(text).toContain("count: 1");
|
|
112
|
+
expect(text).toContain("commit_id");
|
|
113
|
+
expect(text).toContain("abc123");
|
|
114
|
+
expect(text).toContain("99");
|
|
115
|
+
expect(text).toContain("carol");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("keeps a referenced event without commit fields (base fields only)", async () => {
|
|
119
|
+
const { ctx } = makeCtx([{ event: "referenced", id: 5 }]);
|
|
120
|
+
const result = await runTool(GetIssueEventsTool(ctx), { issue_number: 17 });
|
|
121
|
+
|
|
122
|
+
expect(result.content[0].text).toContain("count: 1");
|
|
123
|
+
expect(result.content[0].text).toContain("referenced");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("propagates pagination failures as tool errors", async () => {
|
|
127
|
+
const { ctx, paginate } = makeCtx([]);
|
|
128
|
+
paginate.mockRejectedValueOnce(new Error("rate limited"));
|
|
129
|
+
const result = await runTool(GetIssueEventsTool(ctx), { issue_number: 17 });
|
|
130
|
+
|
|
131
|
+
expect(result.isError).toBe(true);
|
|
132
|
+
expect(result.content[0].text).toContain("rate limited");
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { type } from "arktype";
|
|
2
|
+
import type { ToolContext } from "#app/mcp/server";
|
|
3
|
+
import { execute, tool } from "#app/mcp/shared";
|
|
4
|
+
|
|
5
|
+
export const GetIssueEvents = type({
|
|
6
|
+
issue_number: type.number.describe("The issue number to get events for"),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export function GetIssueEventsTool(ctx: ToolContext) {
|
|
10
|
+
return tool({
|
|
11
|
+
name: "get_issue_events",
|
|
12
|
+
description:
|
|
13
|
+
"Get timeline events for a GitHub issue that aren't reflected in the current state. Returns cross-references to other issues/PRs and commit references. Note: current labels, assignees, state, and milestone are already available via get_issue.",
|
|
14
|
+
parameters: GetIssueEvents,
|
|
15
|
+
execute: execute(async ({ issue_number }) => {
|
|
16
|
+
// set issue context
|
|
17
|
+
ctx.toolState.issueNumber = issue_number;
|
|
18
|
+
|
|
19
|
+
const events = await ctx.octokit.paginate(ctx.octokit.rest.issues.listEventsForTimeline, {
|
|
20
|
+
owner: ctx.repo.owner,
|
|
21
|
+
repo: ctx.repo.name,
|
|
22
|
+
issue_number,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Only include events not reflected in current issue state (get_issue already has labels, assignees, state, etc.)
|
|
26
|
+
// Keep only relationship/reference events that show connections to other issues/PRs/commits
|
|
27
|
+
const relevantEventTypes = new Set(["cross_referenced", "referenced"]);
|
|
28
|
+
|
|
29
|
+
const parsedEvents = events.flatMap((event) => {
|
|
30
|
+
// octokit's timeline-event union includes members with `event?:
|
|
31
|
+
// string`, so `"event" in event` does not narrow it to defined.
|
|
32
|
+
// require a string before the Set.has() check.
|
|
33
|
+
if (!("event" in event) || typeof event.event !== "string") return [];
|
|
34
|
+
if (!relevantEventTypes.has(event.event)) return [];
|
|
35
|
+
|
|
36
|
+
const baseEvent: Record<string, any> = {
|
|
37
|
+
event: event.event,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Common fields
|
|
41
|
+
if ("id" in event) {
|
|
42
|
+
baseEvent.id = event.id;
|
|
43
|
+
}
|
|
44
|
+
if ("actor" in event && event.actor) {
|
|
45
|
+
baseEvent.actor = event.actor.login;
|
|
46
|
+
} else if ("user" in event && event.user) {
|
|
47
|
+
baseEvent.actor = event.user.login;
|
|
48
|
+
}
|
|
49
|
+
if ("created_at" in event) {
|
|
50
|
+
baseEvent.created_at = event.created_at;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Event-specific data
|
|
54
|
+
if (event.event === "cross_referenced") {
|
|
55
|
+
if ("source" in event && event.source) {
|
|
56
|
+
const source = event.source as {
|
|
57
|
+
type?: string;
|
|
58
|
+
issue?: { number: number; title: string; html_url: string };
|
|
59
|
+
pull_request?: { number: number; title: string; html_url: string };
|
|
60
|
+
};
|
|
61
|
+
baseEvent.source = {
|
|
62
|
+
type: source.type,
|
|
63
|
+
issue: source.issue
|
|
64
|
+
? {
|
|
65
|
+
number: source.issue.number,
|
|
66
|
+
title: source.issue.title,
|
|
67
|
+
html_url: source.issue.html_url,
|
|
68
|
+
}
|
|
69
|
+
: null,
|
|
70
|
+
pull_request: source.pull_request
|
|
71
|
+
? {
|
|
72
|
+
number: source.pull_request.number,
|
|
73
|
+
title: source.pull_request.title,
|
|
74
|
+
html_url: source.pull_request.html_url,
|
|
75
|
+
}
|
|
76
|
+
: null,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (event.event === "referenced") {
|
|
82
|
+
if ("commit_id" in event) {
|
|
83
|
+
baseEvent.commit_id = event.commit_id;
|
|
84
|
+
}
|
|
85
|
+
if ("commit_url" in event) {
|
|
86
|
+
baseEvent.commit_url = event.commit_url;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return [baseEvent];
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
issue_number,
|
|
95
|
+
events: parsedEvents,
|
|
96
|
+
count: parsedEvents.length,
|
|
97
|
+
};
|
|
98
|
+
}),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { IssueInfoTool } from "#app/mcp/issueInfo";
|
|
3
|
+
import type { ToolContext } from "#app/mcp/server";
|
|
4
|
+
import { initToolState, type ToolState } from "#app/toolState";
|
|
5
|
+
|
|
6
|
+
type ToolResultShape = { content: [{ type: "text"; text: string }]; isError?: boolean };
|
|
7
|
+
|
|
8
|
+
async function runTool(t: { execute?: unknown }, params: unknown): Promise<ToolResultShape> {
|
|
9
|
+
const exec = t.execute as (p: unknown, c: unknown) => Promise<ToolResultShape>;
|
|
10
|
+
return exec(params, {});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const fullIssue = {
|
|
14
|
+
number: 5,
|
|
15
|
+
html_url: "https://gh/issues/5",
|
|
16
|
+
title: "Broken pipeline",
|
|
17
|
+
body: "plain body",
|
|
18
|
+
body_html: "<p>plain body</p>",
|
|
19
|
+
state: "open",
|
|
20
|
+
locked: false,
|
|
21
|
+
labels: ["bug", { name: "infra" }],
|
|
22
|
+
assignees: [{ login: "alice" }],
|
|
23
|
+
user: { login: "bob" },
|
|
24
|
+
created_at: "2026-01-01T00:00:00Z",
|
|
25
|
+
updated_at: "2026-01-02T00:00:00Z",
|
|
26
|
+
closed_at: null,
|
|
27
|
+
comments: 2,
|
|
28
|
+
milestone: { title: "v1" },
|
|
29
|
+
pull_request: {
|
|
30
|
+
url: "https://api/pulls/5",
|
|
31
|
+
html_url: "https://gh/pull/5",
|
|
32
|
+
diff_url: "https://gh/pull/5.diff",
|
|
33
|
+
patch_url: "https://gh/pull/5.patch",
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function makeCtx(issueData: Record<string, unknown>) {
|
|
38
|
+
const get = vi.fn(async (_p: unknown) => ({ data: issueData }));
|
|
39
|
+
const toolState: ToolState = initToolState({ progressComment: undefined });
|
|
40
|
+
const ctx = {
|
|
41
|
+
octokit: { rest: { issues: { get } } },
|
|
42
|
+
repo: { owner: "octo", name: "repo" },
|
|
43
|
+
toolState,
|
|
44
|
+
tmpdir: "/tmp",
|
|
45
|
+
githubInstallationToken: "tok",
|
|
46
|
+
} as unknown as ToolContext;
|
|
47
|
+
return { ctx, get, toolState };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe("IssueInfoTool", () => {
|
|
51
|
+
it("returns issue details and records the issue number in tool state", async () => {
|
|
52
|
+
const { ctx, get, toolState } = makeCtx(fullIssue);
|
|
53
|
+
const result = await runTool(IssueInfoTool(ctx), { issue_number: 5 });
|
|
54
|
+
|
|
55
|
+
expect(result.isError).toBeUndefined();
|
|
56
|
+
expect(get).toHaveBeenCalledWith(
|
|
57
|
+
expect.objectContaining({
|
|
58
|
+
owner: "octo",
|
|
59
|
+
repo: "repo",
|
|
60
|
+
issue_number: 5,
|
|
61
|
+
headers: { accept: "application/vnd.github.full+json" },
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
64
|
+
expect(toolState.issueNumber).toBe(5);
|
|
65
|
+
|
|
66
|
+
const text = result.content[0].text;
|
|
67
|
+
expect(text).toContain("Broken pipeline");
|
|
68
|
+
expect(text).toContain("plain body");
|
|
69
|
+
expect(text).toContain("infra");
|
|
70
|
+
expect(text).toContain("alice");
|
|
71
|
+
expect(text).toContain("v1");
|
|
72
|
+
expect(text).toContain("https://gh/pull/5.diff");
|
|
73
|
+
expect(text).toContain("get_issue_comments");
|
|
74
|
+
expect(text).toContain("get_issue_events");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("omits the comments hint and nulls pull_request for a bare issue", async () => {
|
|
78
|
+
const { ctx } = makeCtx({
|
|
79
|
+
...fullIssue,
|
|
80
|
+
comments: 0,
|
|
81
|
+
pull_request: undefined,
|
|
82
|
+
labels: undefined,
|
|
83
|
+
assignees: undefined,
|
|
84
|
+
user: null,
|
|
85
|
+
milestone: undefined,
|
|
86
|
+
});
|
|
87
|
+
const result = await runTool(IssueInfoTool(ctx), { issue_number: 5 });
|
|
88
|
+
|
|
89
|
+
expect(result.isError).toBeUndefined();
|
|
90
|
+
const text = result.content[0].text;
|
|
91
|
+
expect(text).not.toContain("get_issue_comments");
|
|
92
|
+
expect(text).toContain("get_issue_events");
|
|
93
|
+
expect(text).toContain("pull_request: null");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("surfaces API failures as tool errors", async () => {
|
|
97
|
+
const { ctx, get } = makeCtx(fullIssue);
|
|
98
|
+
get.mockRejectedValueOnce(new Error("API down"));
|
|
99
|
+
const result = await runTool(IssueInfoTool(ctx), { issue_number: 5 });
|
|
100
|
+
|
|
101
|
+
expect(result.isError).toBe(true);
|
|
102
|
+
expect(result.content[0].text).toContain("API down");
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { type } from "arktype";
|
|
2
|
+
import type { ToolContext } from "#app/mcp/server";
|
|
3
|
+
import { execute, tool } from "#app/mcp/shared";
|
|
4
|
+
import { resolveBodyAssets } from "#app/utils/body";
|
|
5
|
+
|
|
6
|
+
export const IssueInfo = type({
|
|
7
|
+
issue_number: type.number.describe("The issue number to fetch"),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export function IssueInfoTool(ctx: ToolContext) {
|
|
11
|
+
return tool({
|
|
12
|
+
name: "get_issue",
|
|
13
|
+
description:
|
|
14
|
+
"Retrieve GitHub issue information by issue number. " +
|
|
15
|
+
"Example: `get_issue({ issue_number: 1234 })`.",
|
|
16
|
+
parameters: IssueInfo,
|
|
17
|
+
execute: execute(async ({ issue_number }) => {
|
|
18
|
+
const issue = await ctx.octokit.rest.issues.get({
|
|
19
|
+
owner: ctx.repo.owner,
|
|
20
|
+
repo: ctx.repo.name,
|
|
21
|
+
issue_number,
|
|
22
|
+
headers: { accept: "application/vnd.github.full+json" },
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const data = issue.data;
|
|
26
|
+
|
|
27
|
+
const body = await resolveBodyAssets({
|
|
28
|
+
body: data.body,
|
|
29
|
+
bodyHtml: data.body_html,
|
|
30
|
+
tmpdir: ctx.tmpdir,
|
|
31
|
+
githubToken: ctx.githubInstallationToken,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// set issue context
|
|
35
|
+
ctx.toolState.issueNumber = issue_number;
|
|
36
|
+
|
|
37
|
+
const hints: string[] = [];
|
|
38
|
+
if (data.comments > 0) {
|
|
39
|
+
hints.push("use get_issue_comments to retrieve all comments for this issue");
|
|
40
|
+
}
|
|
41
|
+
hints.push(
|
|
42
|
+
"use get_issue_events to retrieve cross-references and commit references (relationships not reflected in current state)",
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
number: data.number,
|
|
47
|
+
url: data.html_url,
|
|
48
|
+
title: data.title,
|
|
49
|
+
body: body,
|
|
50
|
+
state: data.state,
|
|
51
|
+
locked: data.locked,
|
|
52
|
+
labels: data.labels?.map((label) => (typeof label === "string" ? label : label.name)),
|
|
53
|
+
assignees: data.assignees?.map((assignee) => assignee.login),
|
|
54
|
+
user: data.user?.login,
|
|
55
|
+
created_at: data.created_at,
|
|
56
|
+
updated_at: data.updated_at,
|
|
57
|
+
closed_at: data.closed_at,
|
|
58
|
+
comments: data.comments,
|
|
59
|
+
milestone: data.milestone?.title,
|
|
60
|
+
pull_request: data.pull_request
|
|
61
|
+
? {
|
|
62
|
+
url: data.pull_request.url,
|
|
63
|
+
html_url: data.pull_request.html_url,
|
|
64
|
+
diff_url: data.pull_request.diff_url,
|
|
65
|
+
patch_url: data.pull_request.patch_url,
|
|
66
|
+
}
|
|
67
|
+
: null,
|
|
68
|
+
hints,
|
|
69
|
+
};
|
|
70
|
+
}),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { AddLabelsTool } from "#app/mcp/labels";
|
|
3
|
+
import type { ToolContext } from "#app/mcp/server";
|
|
4
|
+
|
|
5
|
+
type ToolResultShape = { content: [{ type: "text"; text: string }]; isError?: boolean };
|
|
6
|
+
|
|
7
|
+
async function runTool(t: { execute?: unknown }, params: unknown): Promise<ToolResultShape> {
|
|
8
|
+
const exec = t.execute as (p: unknown, c: unknown) => Promise<ToolResultShape>;
|
|
9
|
+
return exec(params, {});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function makeCtx() {
|
|
13
|
+
const addLabels = vi.fn(async (_p: unknown) => ({
|
|
14
|
+
data: [{ name: "bug" }, { name: "infra" }],
|
|
15
|
+
}));
|
|
16
|
+
const ctx = {
|
|
17
|
+
octokit: { rest: { issues: { addLabels } } },
|
|
18
|
+
repo: { owner: "octo", name: "repo" },
|
|
19
|
+
} as unknown as ToolContext;
|
|
20
|
+
return { ctx, addLabels };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("AddLabelsTool", () => {
|
|
24
|
+
it("adds labels and returns the resulting label set", async () => {
|
|
25
|
+
const { ctx, addLabels } = makeCtx();
|
|
26
|
+
const result = await runTool(AddLabelsTool(ctx), {
|
|
27
|
+
issue_number: 12,
|
|
28
|
+
labels: ["bug", "infra"],
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
expect(result.isError).toBeUndefined();
|
|
32
|
+
expect(addLabels).toHaveBeenCalledWith({
|
|
33
|
+
owner: "octo",
|
|
34
|
+
repo: "repo",
|
|
35
|
+
issue_number: 12,
|
|
36
|
+
labels: ["bug", "infra"],
|
|
37
|
+
});
|
|
38
|
+
const text = result.content[0].text;
|
|
39
|
+
expect(text).toContain("success: true");
|
|
40
|
+
expect(text).toContain("bug");
|
|
41
|
+
expect(text).toContain("infra");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("surfaces API failures as tool errors", async () => {
|
|
45
|
+
const { ctx, addLabels } = makeCtx();
|
|
46
|
+
addLabels.mockRejectedValueOnce(new Error("Label does not exist"));
|
|
47
|
+
const result = await runTool(AddLabelsTool(ctx), { issue_number: 12, labels: ["nope"] });
|
|
48
|
+
|
|
49
|
+
expect(result.isError).toBe(true);
|
|
50
|
+
expect(result.content[0].text).toContain("Label does not exist");
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type } from "arktype";
|
|
2
|
+
import { assertTargetInScope } from "#app/mcp/scope";
|
|
3
|
+
import type { ToolContext } from "#app/mcp/server";
|
|
4
|
+
import { execute, tool } from "#app/mcp/shared";
|
|
5
|
+
import { log } from "#app/utils/cli";
|
|
6
|
+
|
|
7
|
+
export const AddLabelsParams = type({
|
|
8
|
+
issue_number: type.number.describe("the issue or PR number to add labels to"),
|
|
9
|
+
labels: type.string.array().atLeastLength(1).describe("array of label names to add"),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export function AddLabelsTool(ctx: ToolContext) {
|
|
13
|
+
return tool({
|
|
14
|
+
name: "add_labels",
|
|
15
|
+
description:
|
|
16
|
+
"Add labels to a GitHub issue or pull request. Only use labels that already exist in the repository.",
|
|
17
|
+
parameters: AddLabelsParams,
|
|
18
|
+
execute: execute(async ({ issue_number, labels }) => {
|
|
19
|
+
assertTargetInScope(ctx, issue_number, "add labels to");
|
|
20
|
+
const result = await ctx.octokit.rest.issues.addLabels({
|
|
21
|
+
owner: ctx.repo.owner,
|
|
22
|
+
repo: ctx.repo.name,
|
|
23
|
+
issue_number,
|
|
24
|
+
labels,
|
|
25
|
+
});
|
|
26
|
+
log.info(`» added labels [${labels.join(", ")}] to issue #${issue_number}`);
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
success: true,
|
|
30
|
+
labels: result.data.map((label) => label.name),
|
|
31
|
+
};
|
|
32
|
+
}),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ToolState } from "#app/toolState";
|
|
2
|
+
import type { ResolvedPayload } from "#app/utils/payload";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The cwd-scoped, GitHub-free subset of `ToolContext` that the read-only
|
|
6
|
+
* Terraform tools depend on. Two providers exist:
|
|
7
|
+
*
|
|
8
|
+
* - the GitHub Action run: the full `ToolContext` (structurally assignable —
|
|
9
|
+
* it carries these fields plus the GitHub/auth surface), and
|
|
10
|
+
* - `terramend mcp` (the local stdio MCP server): exactly this shape, built
|
|
11
|
+
* from CLI flags — no octokit, no tokens, no event payload.
|
|
12
|
+
*
|
|
13
|
+
* Keep this interface to fields a LOCAL run can genuinely provide. A tool that
|
|
14
|
+
* needs more (octokit, push, PR state) belongs on `ToolContext`, not here.
|
|
15
|
+
*/
|
|
16
|
+
export interface LocalToolContext {
|
|
17
|
+
payload: Pick<
|
|
18
|
+
ResolvedPayload,
|
|
19
|
+
| "cwd"
|
|
20
|
+
| "scanScope"
|
|
21
|
+
| "severityThreshold"
|
|
22
|
+
| "autonomyThreshold"
|
|
23
|
+
| "costIncreaseBlockUsd"
|
|
24
|
+
| "moduleCatalogue"
|
|
25
|
+
>;
|
|
26
|
+
toolState: ToolState;
|
|
27
|
+
tmpdir: string;
|
|
28
|
+
}
|