boomtick-cli 0.2.1__py3-none-any.whl

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.
Files changed (103) hide show
  1. boomtick_cli-0.2.1.dist-info/METADATA +20 -0
  2. boomtick_cli-0.2.1.dist-info/RECORD +103 -0
  3. boomtick_cli-0.2.1.dist-info/WHEEL +5 -0
  4. boomtick_cli-0.2.1.dist-info/entry_points.txt +3 -0
  5. boomtick_cli-0.2.1.dist-info/top_level.txt +1 -0
  6. dev_tools/__init__.py +1 -0
  7. dev_tools/cli-schema.json +2028 -0
  8. dev_tools/cli.py +1542 -0
  9. dev_tools/config.py +235 -0
  10. dev_tools/constants.py +14 -0
  11. dev_tools/daemon.py +146 -0
  12. dev_tools/dist/config.js +108 -0
  13. dev_tools/dist/index.js +10 -0
  14. dev_tools/dist/lib/error_utils.js +8 -0
  15. dev_tools/dist/lib/git.js +31 -0
  16. dev_tools/dist/lib/result.js +21 -0
  17. dev_tools/dist/lib/shell.js +91 -0
  18. dev_tools/dist/lib/shell.test.js +10 -0
  19. dev_tools/dist/lib/td-cli.js +27 -0
  20. dev_tools/dist/lib/test-utils.js +20 -0
  21. dev_tools/dist/mcp/definitions.js +242 -0
  22. dev_tools/dist/mcp/server.js +317 -0
  23. dev_tools/dist/mcp/tools.js +18 -0
  24. dev_tools/dist/tools/contract.js +2069 -0
  25. dev_tools/dist/tools/ddgs.search.js +50 -0
  26. dev_tools/dist/tools/ddgs.search.test.js +67 -0
  27. dev_tools/dist/tools/ddgs_search.py +23 -0
  28. dev_tools/dist/tools/github.checkout_branch.js +20 -0
  29. dev_tools/dist/tools/github.comment_triage_summary.js +25 -0
  30. dev_tools/dist/tools/github.create_issue.js +28 -0
  31. dev_tools/dist/tools/github.create_issue.test.js +54 -0
  32. dev_tools/dist/tools/github.create_pull_request.js +44 -0
  33. dev_tools/dist/tools/github.get_merge_conflict_files.js +26 -0
  34. dev_tools/dist/tools/github.get_pr.js +18 -0
  35. dev_tools/dist/tools/github.get_pr_diff.js +24 -0
  36. dev_tools/dist/tools/github.issue_comment.js +24 -0
  37. dev_tools/dist/tools/github.issue_update.js +29 -0
  38. dev_tools/dist/tools/github.issue_view.js +28 -0
  39. dev_tools/dist/tools/github.open_replacement_pr.js +41 -0
  40. dev_tools/dist/tools/github.search_open_prs.js +31 -0
  41. dev_tools/dist/tools/github.search_open_prs.test.js +51 -0
  42. dev_tools/dist/tools/jules/cancel-session.js +20 -0
  43. dev_tools/dist/tools/jules/create-session.js +20 -0
  44. dev_tools/dist/tools/jules/create-session.test.js +99 -0
  45. dev_tools/dist/tools/jules/get-messages.js +19 -0
  46. dev_tools/dist/tools/jules/get-messages.test.js +45 -0
  47. dev_tools/dist/tools/jules/get-pr.js +14 -0
  48. dev_tools/dist/tools/jules/get-pr.test.js +37 -0
  49. dev_tools/dist/tools/jules/get-session.js +10 -0
  50. dev_tools/dist/tools/jules/list-sessions.js +43 -0
  51. dev_tools/dist/tools/jules/send-message.js +22 -0
  52. dev_tools/dist/tools/jules/send-message.test.js +56 -0
  53. dev_tools/dist/tools/jules/shared.js +26 -0
  54. dev_tools/dist/tools/jules/trigger-feedback.js +16 -0
  55. dev_tools/dist/tools/jules/trigger-feedback.test.js +42 -0
  56. dev_tools/dist/tools/repo.commit_patch.js +33 -0
  57. dev_tools/dist/tools/repo.create_branch.js +21 -0
  58. dev_tools/dist/tools/repo.create_branch.test.js +42 -0
  59. dev_tools/dist/tools/repo.create_repair_branch.js +38 -0
  60. dev_tools/dist/tools/repo.get_changed_files.js +21 -0
  61. dev_tools/dist/tools/repo.get_command_schema.js +27 -0
  62. dev_tools/dist/tools/repo.get_package_scripts.js +35 -0
  63. dev_tools/dist/tools/repo.get_package_scripts.test.js +39 -0
  64. dev_tools/dist/tools/repo.get_route_map.js +37 -0
  65. dev_tools/dist/tools/repo.logs.js +16 -0
  66. dev_tools/dist/tools/repo.read_agent_context.js +20 -0
  67. dev_tools/dist/tools/repo.read_ci_logs.js +18 -0
  68. dev_tools/dist/tools/repo.run_lighthouse.js +37 -0
  69. dev_tools/dist/tools/repo.run_playwright.js +29 -0
  70. dev_tools/dist/tools/repo.run_tests.js +43 -0
  71. dev_tools/dist/tools/types.js +1 -0
  72. dev_tools/get_ai_context.py +63 -0
  73. dev_tools/handlers/__init__.py +1 -0
  74. dev_tools/handlers/command_handler.py +69 -0
  75. dev_tools/mcp_server.py +36 -0
  76. dev_tools/models.py +354 -0
  77. dev_tools/orchestrator.py +2841 -0
  78. dev_tools/pr_overlap.py +139 -0
  79. dev_tools/resources/__init__.py +1 -0
  80. dev_tools/resources/build-repo-context.py +182 -0
  81. dev_tools/resources/prompt_constants.json +5 -0
  82. dev_tools/resources/review_template.md +41 -0
  83. dev_tools/resources/ux-audit.config.json +15 -0
  84. dev_tools/resources/visual_guidelines.json +3 -0
  85. dev_tools/review_read_pass.py +232 -0
  86. dev_tools/schema_gen.py +55 -0
  87. dev_tools/schema_utils.py +125 -0
  88. dev_tools/scope_check.py +85 -0
  89. dev_tools/services/__init__.py +1 -0
  90. dev_tools/services/ai_service.py +816 -0
  91. dev_tools/services/dependency_graph.py +123 -0
  92. dev_tools/services/github.py +783 -0
  93. dev_tools/services/jules.py +181 -0
  94. dev_tools/services/repair_service.py +199 -0
  95. dev_tools/services/vector_store.py +82 -0
  96. dev_tools/services/vision_service.py +91 -0
  97. dev_tools/td_cli.py +28 -0
  98. dev_tools/utils/__init__.py +1035 -0
  99. dev_tools/utils/git.py +68 -0
  100. dev_tools/ux_report.py +213 -0
  101. dev_tools/verify_infra.py +91 -0
  102. dev_tools/verify_versions.py +191 -0
  103. dev_tools/version_utils.py +175 -0
@@ -0,0 +1,50 @@
1
+ import { z } from "zod";
2
+ import { runCommand } from "../lib/shell.js";
3
+ export const DdgsSearchInputSchema = z.object({
4
+ query: z.string(),
5
+ maxResults: z.number().optional().default(5),
6
+ });
7
+ import path from "path";
8
+ import { fileURLToPath } from "url";
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+ export async function ddgsSearchHandler(args) {
12
+ const scriptPath = path.join(__dirname, "ddgs_search.py");
13
+ // Sanitize query by removing control characters, newlines, and null bytes
14
+ // eslint-disable-next-line no-control-regex
15
+ const safeQuery = args.query.replace(/[\x00-\x1F\x7F-\x9F]/g, " ").trim();
16
+ if (!safeQuery) {
17
+ throw new Error("Query cannot be empty after sanitization.");
18
+ }
19
+ const result = await runCommand("python3", [scriptPath, safeQuery, (args.maxResults ?? 5).toString()]);
20
+ // duckduckgo_search logs annoying deprecation warnings to stderr we want to ignore
21
+ // if it really failed, exitCode will be non-zero and we'll have stdout/stderr
22
+ if (result.exitCode !== 0) {
23
+ let parsedError = result.stderr || result.stdout;
24
+ const lines = parsedError.split("\n");
25
+ for (const line of lines) {
26
+ try {
27
+ const json = JSON.parse(line);
28
+ if (json.error) {
29
+ parsedError = json.error;
30
+ break;
31
+ }
32
+ }
33
+ catch {
34
+ // continue
35
+ }
36
+ }
37
+ throw new Error(`Failed to search ddgs: ${parsedError}`);
38
+ }
39
+ try {
40
+ const data = JSON.parse(result.stdout);
41
+ if (!Array.isArray(data)) {
42
+ throw new Error("Expected an array of search results");
43
+ }
44
+ return { results: data };
45
+ }
46
+ catch (e) {
47
+ const message = e instanceof Error ? e.message : String(e);
48
+ throw new Error(`Failed to parse ddgs search results. Error: ${message}. Output was: ${result.stdout}`);
49
+ }
50
+ }
@@ -0,0 +1,67 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { ddgsSearchHandler } from "./ddgs.search.js";
3
+ import { runCommand } from "../lib/shell.js";
4
+ vi.mock("../lib/shell.js");
5
+ describe("ddgsSearchHandler", () => {
6
+ beforeEach(() => {
7
+ vi.resetAllMocks();
8
+ });
9
+ it("should parse arguments, sanitize query, and return search results", async () => {
10
+ const mockOutput = JSON.stringify([
11
+ { title: "Result 1", href: "https://example.com/1", body: "Description 1" },
12
+ { title: "Result 2", href: "https://example.com/2", body: "Description 2" },
13
+ ]);
14
+ vi.mocked(runCommand).mockResolvedValue({
15
+ stdout: mockOutput,
16
+ stderr: "",
17
+ exitCode: 0,
18
+ durationMs: 100,
19
+ command: "mock python3",
20
+ });
21
+ const result = await ddgsSearchHandler({ query: "test\nquery\x00", maxResults: 2 });
22
+ expect(runCommand).toHaveBeenCalledTimes(1);
23
+ expect(runCommand).toHaveBeenCalledWith("python3", [expect.stringContaining("ddgs_search.py"), "test query", "2"]);
24
+ expect(result.results).toHaveLength(2);
25
+ expect(result.results[0].title).toBe("Result 1");
26
+ });
27
+ it("should handle raw error when python script exits with non-zero code and non-JSON output", async () => {
28
+ vi.mocked(runCommand).mockResolvedValue({
29
+ stdout: "",
30
+ stderr: "Python script failed",
31
+ exitCode: 1,
32
+ durationMs: 100,
33
+ command: "mock python3",
34
+ });
35
+ await expect(ddgsSearchHandler({ query: "test query", maxResults: 2 })).rejects.toThrow("Failed to search ddgs: Python script failed");
36
+ });
37
+ it("should handle parsed JSON error when python script exits with non-zero code", async () => {
38
+ vi.mocked(runCommand).mockResolvedValue({
39
+ stdout: "",
40
+ stderr: JSON.stringify({ error: "Structured JSON error" }),
41
+ exitCode: 1,
42
+ durationMs: 100,
43
+ command: "mock python3",
44
+ });
45
+ await expect(ddgsSearchHandler({ query: "test query", maxResults: 2 })).rejects.toThrow("Failed to search ddgs: Structured JSON error");
46
+ });
47
+ it("should handle error when python output is not an array", async () => {
48
+ vi.mocked(runCommand).mockResolvedValue({
49
+ stdout: JSON.stringify({ error: "Something went wrong" }),
50
+ stderr: "",
51
+ exitCode: 0,
52
+ durationMs: 100,
53
+ command: "mock python3",
54
+ });
55
+ await expect(ddgsSearchHandler({ query: "test query", maxResults: 2 })).rejects.toThrow("Expected an array of search results");
56
+ });
57
+ it("should handle error when python output cannot be parsed", async () => {
58
+ vi.mocked(runCommand).mockResolvedValue({
59
+ stdout: "invalid json",
60
+ stderr: "",
61
+ exitCode: 0,
62
+ durationMs: 100,
63
+ command: "mock python3",
64
+ });
65
+ await expect(ddgsSearchHandler({ query: "test query", maxResults: 2 })).rejects.toThrow("Failed to parse ddgs search results");
66
+ });
67
+ });
@@ -0,0 +1,23 @@
1
+ # pylint: disable=line-too-long
2
+ """DuckDuckGo Search tool."""
3
+ import json
4
+ import sys
5
+
6
+ try:
7
+ from duckduckgo_search import DDGS
8
+ except ImportError:
9
+ print(json.dumps({"error": "duckduckgo_search package not found. Please install it using 'pip install duckduckgo-search' (preferably in a virtual environment)."}), file=sys.stderr)
10
+ sys.exit(1)
11
+
12
+ if len(sys.argv) != 3:
13
+ print(json.dumps({"error": f"Invalid argument count. Expected 2 (query, max_results), got {len(sys.argv) - 1}."}), file=sys.stderr)
14
+ sys.exit(1)
15
+
16
+ try:
17
+ query = sys.argv[1]
18
+ max_results = int(sys.argv[2])
19
+ results = DDGS().text(query, max_results=max_results)
20
+ print(json.dumps(list(results)))
21
+ except Exception as e:
22
+ print(json.dumps({"error": str(e)}), file=sys.stderr)
23
+ sys.exit(1)
@@ -0,0 +1,20 @@
1
+ import { z } from "zod";
2
+ import { runCommand } from "../lib/shell.js";
3
+ export const CheckoutBranchInputSchema = z.object({
4
+ branch: z.string(),
5
+ worktreePath: z.string().optional(),
6
+ });
7
+ export async function checkoutBranchHandler(args) {
8
+ const result = await runCommand("td-cli", ["gh", "checkout", args.branch], { cwd: args.worktreePath });
9
+ if (result.exitCode !== 0) {
10
+ throw new Error(`Failed to checkout branch ${args.branch}: ${result.stderr}`);
11
+ }
12
+ const output = JSON.parse(result.stdout);
13
+ if (output.status === "error") {
14
+ throw new Error(`Failed to checkout branch: ${output.message}`);
15
+ }
16
+ return {
17
+ success: true,
18
+ branch: args.branch
19
+ };
20
+ }
@@ -0,0 +1,25 @@
1
+ import { z } from "zod";
2
+ import { runCommand } from "../lib/shell.js";
3
+ export const CommentTriageSummaryInputSchema = z.object({
4
+ prNumber: z.number(),
5
+ body: z.string(),
6
+ });
7
+ export async function commentTriageSummaryHandler(args) {
8
+ const result = await runCommand("td-cli", [
9
+ "gh",
10
+ "post-comment",
11
+ "--pr", args.prNumber.toString(),
12
+ "--body", args.body
13
+ ]);
14
+ if (result.exitCode !== 0) {
15
+ throw new Error(`Failed to comment on PR: ${result.stderr}`);
16
+ }
17
+ const output = JSON.parse(result.stdout);
18
+ if (output.status === "error") {
19
+ throw new Error(`Failed to comment on PR: ${output.message}`);
20
+ }
21
+ return {
22
+ success: true,
23
+ commentUrl: output.html_url || output.url
24
+ };
25
+ }
@@ -0,0 +1,28 @@
1
+ import { runCommand } from "../lib/shell.js";
2
+ import { sanitizeError } from "../lib/error_utils.js";
3
+ import { CreateIssueInputSchema, CreateIssueResponseSchema } from "./contract.js";
4
+ export { CreateIssueInputSchema };
5
+ export async function createIssueHandler(args) {
6
+ const params = CreateIssueInputSchema.parse(args);
7
+ const cmdArgs = ["gh", "create-issue", "--title", params.title];
8
+ if (params.body)
9
+ cmdArgs.push("--body", params.body);
10
+ if (params.file)
11
+ cmdArgs.push("--file", params.file);
12
+ const result = await runCommand("td-cli", cmdArgs);
13
+ if (result.exitCode !== 0) {
14
+ throw new Error(`Failed to create issue: ${sanitizeError(result.stderr)}`);
15
+ }
16
+ let output;
17
+ try {
18
+ output = JSON.parse(result.stdout);
19
+ }
20
+ catch (e) {
21
+ throw new Error(`Failed to parse CLI output: ${result.stdout}`);
22
+ }
23
+ const parsedOutput = CreateIssueResponseSchema.parse(output);
24
+ if (parsedOutput.status === "error") {
25
+ throw new Error(`Failed to create issue: ${parsedOutput.message}`);
26
+ }
27
+ return { status: "success", issue: parsedOutput.issue };
28
+ }
@@ -0,0 +1,54 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { createIssueHandler } from "./github.create_issue.js";
3
+ import * as shell from "../lib/shell.js";
4
+ vi.mock("../lib/shell.js", () => ({
5
+ runCommand: vi.fn()
6
+ }));
7
+ describe("github.create_issue", () => {
8
+ it("should create an issue successfully", async () => {
9
+ const mockResponse = {
10
+ status: "success",
11
+ issue: {
12
+ number: 123,
13
+ title: "Test Issue",
14
+ html_url: "https://github.com/owner/repo/issues/123",
15
+ state: "open"
16
+ }
17
+ };
18
+ vi.mocked(shell.runCommand).mockResolvedValue({
19
+ stdout: JSON.stringify(mockResponse),
20
+ stderr: "",
21
+ exitCode: 0,
22
+ durationMs: 10,
23
+ command: "td-cli gh create-issue --title 'Test Issue' --body 'Test Body'"
24
+ });
25
+ const result = await createIssueHandler({ title: "Test Issue", body: "Test Body", file: null });
26
+ expect(result.status).toBe("success");
27
+ expect(result.issue?.number).toBe(123);
28
+ expect(result.issue?.title).toBe("Test Issue");
29
+ });
30
+ it("should throw error on command failure", async () => {
31
+ vi.mocked(shell.runCommand).mockResolvedValue({
32
+ stdout: "",
33
+ stderr: "Auth failed",
34
+ exitCode: 1,
35
+ durationMs: 10,
36
+ command: "td-cli gh create-issue"
37
+ });
38
+ await expect(createIssueHandler({ title: "Test Issue", body: "Test Body", file: null })).rejects.toThrow("Failed to create issue: Auth failed");
39
+ });
40
+ it("should handle error status from CLI output", async () => {
41
+ const mockResponse = {
42
+ status: "error",
43
+ message: "Repo not found"
44
+ };
45
+ vi.mocked(shell.runCommand).mockResolvedValue({
46
+ stdout: JSON.stringify(mockResponse),
47
+ stderr: "",
48
+ exitCode: 0,
49
+ durationMs: 10,
50
+ command: "td-cli gh create-issue"
51
+ });
52
+ await expect(createIssueHandler({ title: "Test Issue", body: "Test Body", file: null })).rejects.toThrow("Failed to create issue: Repo not found");
53
+ });
54
+ });
@@ -0,0 +1,44 @@
1
+ import { z } from "zod";
2
+ import { runCommand } from "../lib/shell.js";
3
+ export const CreatePullRequestInputSchema = z.object({
4
+ title: z.string(),
5
+ body: z.string(),
6
+ head: z.string(),
7
+ base: z.string().optional().default("main"),
8
+ draft: z.boolean().optional().default(false),
9
+ });
10
+ export async function createPullRequestHandler(args) {
11
+ const tdArgs = [
12
+ "gh",
13
+ "create-pr",
14
+ "--title", args.title,
15
+ "--body", args.body,
16
+ "--head", args.head,
17
+ "--base", args.base
18
+ ];
19
+ if (args.draft) {
20
+ tdArgs.push("--draft");
21
+ }
22
+ const result = await runCommand("td-cli", tdArgs);
23
+ if (result.exitCode !== 0) {
24
+ // Attempt to parse stdout as JSON to see if there's a structured error message
25
+ try {
26
+ const errorOutput = JSON.parse(result.stdout);
27
+ if (errorOutput.status === "error") {
28
+ throw new Error(`Failed to create pull request: ${errorOutput.message}`);
29
+ }
30
+ }
31
+ catch (e) {
32
+ // If parsing fails, fall back to stderr or a generic message
33
+ }
34
+ throw new Error(`Failed to create pull request: ${result.stderr || result.stdout || "Unknown error"}`);
35
+ }
36
+ const output = JSON.parse(result.stdout);
37
+ if (output.status === "error") {
38
+ throw new Error(`Failed to create pull request: ${output.message}`);
39
+ }
40
+ return {
41
+ success: true,
42
+ url: output.pr.html_url
43
+ };
44
+ }
@@ -0,0 +1,26 @@
1
+ import { z } from "zod";
2
+ import { runCommand } from "../lib/shell.js";
3
+ export const GetMergeConflictFilesInputSchema = z.object({
4
+ prNumber: z.number(),
5
+ baseBranch: z.string().optional().default("main"),
6
+ });
7
+ export async function getMergeConflictFilesHandler(args) {
8
+ const result = await runCommand("td-cli", [
9
+ "gh", "merge-conflicts", args.prNumber.toString(),
10
+ "--base", args.baseBranch || "main"
11
+ ]);
12
+ if (result.exitCode !== 0) {
13
+ throw new Error(`Failed to get merge conflicts: ${result.stderr}`);
14
+ }
15
+ const output = JSON.parse(result.stdout);
16
+ if (output.status === "error") {
17
+ throw new Error(`Failed to get merge conflicts: ${output.message}`);
18
+ }
19
+ return {
20
+ prNumber: output.prNumber,
21
+ baseBranch: output.baseBranch,
22
+ headRef: output.headRef,
23
+ conflictFiles: output.conflictFiles,
24
+ commandLog: output.commandLog
25
+ };
26
+ }
@@ -0,0 +1,18 @@
1
+ import { z } from "zod";
2
+ import { runCommand } from "../lib/shell.js";
3
+ import { sanitizeError } from "../lib/error_utils.js";
4
+ export const GetPrInputSchema = z.object({
5
+ prNumber: z.number().describe("The number of the PR to view."),
6
+ });
7
+ export async function getPrHandler(args) {
8
+ const params = GetPrInputSchema.parse(args);
9
+ const result = await runCommand("td-cli", ["gh", "view", params.prNumber.toString()]);
10
+ if (result.exitCode !== 0) {
11
+ throw new Error(`Failed to get PR details: ${sanitizeError(result.stderr)}`);
12
+ }
13
+ const output = JSON.parse(result.stdout);
14
+ if (output.status === "error") {
15
+ throw new Error(`Failed to get PR details: ${output.message}`);
16
+ }
17
+ return { pr: output.pr };
18
+ }
@@ -0,0 +1,24 @@
1
+ import { z } from "zod";
2
+ import { runCommand } from "../lib/shell.js";
3
+ export const GetPrDiffInputSchema = z.object({
4
+ prNumber: z.number(),
5
+ });
6
+ export async function getPrDiffHandler(args) {
7
+ const result = await runCommand("td-cli", [
8
+ "gh", "pr-diff", args.prNumber.toString()
9
+ ]);
10
+ if (result.exitCode !== 0) {
11
+ throw new Error(`Failed to get PR diff: ${result.stderr}`);
12
+ }
13
+ const output = JSON.parse(result.stdout);
14
+ if (output.status === "error") {
15
+ throw new Error(`Failed to get PR diff: ${output.message}`);
16
+ }
17
+ // Every TypeScript tool file must be a pure routing shim
18
+ return {
19
+ prNumber: output.prNumber,
20
+ files: output.files,
21
+ diffText: output.diffText,
22
+ truncated: output.truncated
23
+ };
24
+ }
@@ -0,0 +1,24 @@
1
+ import { z } from "zod";
2
+ import { runCommand } from "../lib/shell.js";
3
+ import { sanitizeError } from "../lib/error_utils.js";
4
+ export const IssueCommentInputSchema = z.object({
5
+ issueNumber: z.number().describe("The number of the issue to comment on."),
6
+ body: z.string().min(1, "Comment body cannot be empty").describe("The content of the comment."),
7
+ });
8
+ const IssueCommentOutputSchema = z.object({
9
+ status: z.string(),
10
+ comment: z.any().optional(),
11
+ message: z.string().optional(),
12
+ });
13
+ export async function issueCommentHandler(args) {
14
+ const params = IssueCommentInputSchema.parse(args);
15
+ const result = await runCommand("td-cli", ["gh", "issue-comment", params.issueNumber.toString(), "--body", params.body]);
16
+ if (result.exitCode !== 0) {
17
+ throw new Error(`Failed to post comment: ${sanitizeError(result.stderr)}`);
18
+ }
19
+ const output = IssueCommentOutputSchema.parse(JSON.parse(result.stdout));
20
+ if (output.status === "error") {
21
+ throw new Error(`Failed to post comment: ${output.message}`);
22
+ }
23
+ return { status: "success", comment: output.comment };
24
+ }
@@ -0,0 +1,29 @@
1
+ import { runCommand } from "../lib/shell.js";
2
+ import { sanitizeError } from "../lib/error_utils.js";
3
+ import { IssueUpdateInputSchema, IssueUpdateResponseSchema } from "./contract.js";
4
+ export { IssueUpdateInputSchema };
5
+ export async function issueUpdateHandler(args) {
6
+ const params = IssueUpdateInputSchema.parse(args);
7
+ const cmdArgs = ["gh", "issue-update", params.issueNumber.toString()];
8
+ if (params.body)
9
+ cmdArgs.push("--body", params.body);
10
+ if (params.file)
11
+ cmdArgs.push("--file", params.file);
12
+ if (params.labels && params.labels.length > 0)
13
+ cmdArgs.push("--labels", params.labels.join(","));
14
+ if (params.addLabels && params.addLabels.length > 0)
15
+ cmdArgs.push("--add-labels", params.addLabels.join(","));
16
+ if (params.removeLabels && params.removeLabels.length > 0)
17
+ cmdArgs.push("--remove-labels", params.removeLabels.join(","));
18
+ if (params.state)
19
+ cmdArgs.push("--state", params.state);
20
+ const result = await runCommand("td-cli", cmdArgs);
21
+ if (result.exitCode !== 0) {
22
+ throw new Error(`Failed to update issue: ${sanitizeError(result.stderr)}`);
23
+ }
24
+ const output = IssueUpdateResponseSchema.parse(JSON.parse(result.stdout));
25
+ if (output.status === "error") {
26
+ throw new Error(`Failed to update issue: ${output.message}`);
27
+ }
28
+ return { status: "success", issue: output.issue };
29
+ }
@@ -0,0 +1,28 @@
1
+ import { z } from "zod";
2
+ import { runCommand } from "../lib/shell.js";
3
+ import { sanitizeError } from "../lib/error_utils.js";
4
+ export const IssueViewInputSchema = z.object({
5
+ issueNumber: z.number().describe("The number of the issue to view."),
6
+ });
7
+ const IssueViewOutputSchema = z.object({
8
+ status: z.string(),
9
+ issue: z.object({
10
+ number: z.number(),
11
+ title: z.string(),
12
+ body: z.string().nullable().optional(),
13
+ state: z.string(),
14
+ }).optional(),
15
+ message: z.string().optional(),
16
+ });
17
+ export async function issueViewHandler(args) {
18
+ const params = IssueViewInputSchema.parse(args);
19
+ const result = await runCommand("td-cli", ["gh", "issue-view", params.issueNumber.toString()]);
20
+ if (result.exitCode !== 0) {
21
+ throw new Error(`Failed to view issue: ${sanitizeError(result.stderr)}`);
22
+ }
23
+ const output = IssueViewOutputSchema.parse(JSON.parse(result.stdout));
24
+ if (output.status === "error") {
25
+ throw new Error(`Failed to view issue: ${output.message}`);
26
+ }
27
+ return { issue: output.issue };
28
+ }
@@ -0,0 +1,41 @@
1
+ import { z } from "zod";
2
+ import { runCommand } from "../lib/shell.js";
3
+ export const OpenReplacementPrInputSchema = z.object({
4
+ originalPrNumber: z.number(),
5
+ repairBranch: z.string(),
6
+ baseBranch: z.string(),
7
+ title: z.string(),
8
+ body: z.string(),
9
+ draft: z.boolean().optional().default(true),
10
+ worktreePath: z.string().optional(),
11
+ pushMode: z.boolean().optional().default(false),
12
+ });
13
+ export async function openReplacementPrHandler(args) {
14
+ if (!args.pushMode) {
15
+ throw new Error("pushMode must be true to push and open a replacement PR.");
16
+ }
17
+ // Push the branch
18
+ const pushResult = await runCommand("git", ["push", "origin", args.repairBranch], { cwd: args.worktreePath });
19
+ if (pushResult.exitCode !== 0) {
20
+ throw new Error(`Failed to push branch: ${pushResult.stderr}`);
21
+ }
22
+ const prArgs = [
23
+ "pr",
24
+ "create",
25
+ "--base", args.baseBranch,
26
+ "--head", args.repairBranch,
27
+ "--title", args.title,
28
+ "--body", args.body
29
+ ];
30
+ if (args.draft) {
31
+ prArgs.push("--draft");
32
+ }
33
+ const result = await runCommand("gh", prArgs, { cwd: args.worktreePath });
34
+ if (result.exitCode !== 0) {
35
+ throw new Error(`Failed to open replacement PR: ${result.stderr}`);
36
+ }
37
+ return {
38
+ success: true,
39
+ url: result.stdout.trim()
40
+ };
41
+ }
@@ -0,0 +1,31 @@
1
+ import { z } from "zod";
2
+ import { runCommand } from "../lib/shell.js";
3
+ export const SearchOpenPrsInputSchema = z.object({
4
+ state: z.enum(["open", "closed", "all"]).optional().default("open"),
5
+ includeDrafts: z.boolean().optional().default(true),
6
+ limit: z.number().min(1).max(100).optional().default(100),
7
+ labels: z.array(z.string()).optional(),
8
+ });
9
+ export async function searchOpenPrsHandler(args) {
10
+ const params = SearchOpenPrsInputSchema.parse(args);
11
+ const tdArgs = [
12
+ "gh", "search-prs",
13
+ "--state", params.state,
14
+ "--limit", params.limit.toString(),
15
+ ];
16
+ if (!params.includeDrafts) {
17
+ tdArgs.push("--no-include-drafts");
18
+ }
19
+ if (params.labels && params.labels.length > 0) {
20
+ tdArgs.push("--labels", params.labels.join(","));
21
+ }
22
+ const result = await runCommand("td-cli", tdArgs);
23
+ if (result.exitCode !== 0) {
24
+ throw new Error(`Failed to list PRs: ${result.stderr}`);
25
+ }
26
+ const output = JSON.parse(result.stdout);
27
+ if (output.status === "error") {
28
+ throw new Error(`Failed to list PRs: ${output.message}`);
29
+ }
30
+ return { prs: output.prs };
31
+ }
@@ -0,0 +1,51 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { searchOpenPrsHandler } from "./github.search_open_prs.js";
3
+ import * as shell from "../lib/shell.js";
4
+ vi.mock("../lib/shell.js", () => ({
5
+ runCommand: vi.fn()
6
+ }));
7
+ describe("github.search_open_prs", () => {
8
+ it("should return normalized PR objects", async () => {
9
+ const mockResponse = {
10
+ status: "success",
11
+ prs: [
12
+ { number: 1, title: "Test PR", author: { login: "user" }, isDraft: false }
13
+ ]
14
+ };
15
+ vi.mocked(shell.runCommand).mockResolvedValue({
16
+ stdout: JSON.stringify(mockResponse),
17
+ stderr: "",
18
+ exitCode: 0,
19
+ durationMs: 10,
20
+ command: "gh pr list"
21
+ });
22
+ const result = await searchOpenPrsHandler({ state: "open", limit: 1, includeDrafts: true });
23
+ expect(result.prs).toHaveLength(1);
24
+ expect(result.prs[0].number).toBe(1);
25
+ });
26
+ it("should handle empty PR list", async () => {
27
+ const mockResponse = {
28
+ status: "success",
29
+ prs: []
30
+ };
31
+ vi.mocked(shell.runCommand).mockResolvedValue({
32
+ stdout: JSON.stringify(mockResponse),
33
+ stderr: "",
34
+ exitCode: 0,
35
+ durationMs: 10,
36
+ command: "gh pr list"
37
+ });
38
+ const result = await searchOpenPrsHandler({ state: "open", limit: 10, includeDrafts: true });
39
+ expect(result.prs).toHaveLength(0);
40
+ });
41
+ it("should throw error on command failure", async () => {
42
+ vi.mocked(shell.runCommand).mockResolvedValue({
43
+ stdout: "",
44
+ stderr: "Auth failed",
45
+ exitCode: 1,
46
+ durationMs: 10,
47
+ command: "gh pr list"
48
+ });
49
+ await expect(searchOpenPrsHandler({ state: "open", limit: 10, includeDrafts: true })).rejects.toThrow("Failed to list PRs: Auth failed");
50
+ });
51
+ });
@@ -0,0 +1,20 @@
1
+ import { z } from "zod";
2
+ import { runCommand } from "../../lib/shell.js";
3
+ export const CancelJulesSessionInputSchema = z.object({
4
+ sessionId: z.string(),
5
+ });
6
+ export async function cancelJulesSessionHandler(input) {
7
+ const result = await runCommand("td-cli", ["agent", "cancel", input.sessionId]);
8
+ if (result.exitCode !== 0) {
9
+ throw new Error(`Failed to cancel session: ${result.stderr}`);
10
+ }
11
+ const output = JSON.parse(result.stdout);
12
+ if (output.status === "error") {
13
+ throw new Error(`Failed to cancel session: ${output.message}`);
14
+ }
15
+ return {
16
+ id: input.sessionId.replace("sessions/", ""),
17
+ status: "FAILED",
18
+ createdAt: new Date(),
19
+ };
20
+ }
@@ -0,0 +1,20 @@
1
+ import { z } from "zod";
2
+ import { runTDCli } from "../../lib/td-cli.js";
3
+ import { parseJulesSession } from "./shared.js";
4
+ export const CreateJulesSessionInputSchema = z.object({
5
+ task: z.string(),
6
+ branch: z.string().optional(),
7
+ pr: z.number().optional(),
8
+ });
9
+ export async function createJulesSessionHandler(input) {
10
+ let branch = input.branch;
11
+ if (!branch && input.pr) {
12
+ const prData = await runTDCli(["gh", "view", input.pr.toString()]);
13
+ branch = prData.pr.headRefName;
14
+ }
15
+ if (!branch) {
16
+ branch = process.env.DEFAULT_BASE_BRANCH || "main";
17
+ }
18
+ const output = await runTDCli(["agent", "dispatch", branch, input.task]);
19
+ return parseJulesSession(output.session || output);
20
+ }