@arii/boomtick-mcp 0.6.2 → 0.7.1
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/.mcp/schemas/boomtick.health.json +22 -3
- package/.mcp/schemas/boomtick_health.json +22 -3
- package/.mcp/schemas/github.checkout_branch.json +16 -3
- package/.mcp/schemas/github.comment_triage_summary.json +16 -3
- package/.mcp/schemas/github.create_pull_request.json +16 -3
- package/.mcp/schemas/github.get_merge_conflict_files.json +55 -5
- package/.mcp/schemas/github.get_pr.json +12 -3
- package/.mcp/schemas/github.get_pr_commits.json +12 -3
- package/.mcp/schemas/github.get_pr_reviews.json +24 -3
- package/.mcp/schemas/github.issue_comment.json +19 -3
- package/.mcp/schemas/github.issue_view.json +17 -3
- package/.mcp/schemas/github.open_replacement_pr.json +16 -3
- package/.mcp/schemas/github_checkout_branch.json +16 -3
- package/.mcp/schemas/github_comment_triage_summary.json +16 -3
- package/.mcp/schemas/github_create_pull_request.json +16 -3
- package/.mcp/schemas/github_get_merge_conflict_files.json +55 -5
- package/.mcp/schemas/github_get_pr.json +12 -3
- package/.mcp/schemas/github_get_pr_commits.json +12 -3
- package/.mcp/schemas/github_get_pr_reviews.json +24 -3
- package/.mcp/schemas/github_issue_comment.json +19 -3
- package/.mcp/schemas/github_issue_view.json +17 -3
- package/.mcp/schemas/github_open_replacement_pr.json +16 -3
- package/.mcp/schemas/jules.cancel_session.json +38 -3
- package/.mcp/schemas/jules.create_session.json +52 -3
- package/.mcp/schemas/jules.get_messages.json +19 -3
- package/.mcp/schemas/jules.get_pr.json +23 -3
- package/.mcp/schemas/jules.get_session.json +38 -3
- package/.mcp/schemas/jules.list_sessions.json +53 -3
- package/.mcp/schemas/jules.send_message.json +31 -3
- package/.mcp/schemas/jules.trigger_feedback.json +28 -3
- package/.mcp/schemas/jules_cancel_session.json +38 -3
- package/.mcp/schemas/jules_create_session.json +52 -3
- package/.mcp/schemas/jules_get_messages.json +19 -3
- package/.mcp/schemas/jules_get_pr.json +23 -3
- package/.mcp/schemas/jules_get_session.json +38 -3
- package/.mcp/schemas/jules_list_sessions.json +53 -3
- package/.mcp/schemas/jules_send_message.json +31 -3
- package/.mcp/schemas/jules_trigger_feedback.json +28 -3
- package/.mcp/schemas/repo.check_jules_status.json +93 -3
- package/.mcp/schemas/repo.commit_patch.json +23 -3
- package/.mcp/schemas/repo.create_branch.json +91 -3
- package/.mcp/schemas/repo.create_repair_branch.json +94 -3
- package/.mcp/schemas/repo.get_changed_files.json +50 -3
- package/.mcp/schemas/repo.get_command_schema.json +29 -3
- package/.mcp/schemas/repo.get_package_scripts.json +11 -3
- package/.mcp/schemas/repo.get_workflow_runs.json +12 -3
- package/.mcp/schemas/repo.logs.json +14 -3
- package/.mcp/schemas/repo.multi_exec.json +89 -0
- package/.mcp/schemas/repo.read_agent_context.json +117 -5
- package/.mcp/schemas/repo.read_ci_logs.json +46 -3
- package/.mcp/schemas/repo_check_jules_status.json +93 -3
- package/.mcp/schemas/repo_commit_patch.json +23 -3
- package/.mcp/schemas/repo_create_branch.json +91 -3
- package/.mcp/schemas/repo_create_repair_branch.json +94 -3
- package/.mcp/schemas/repo_get_changed_files.json +50 -3
- package/.mcp/schemas/repo_get_command_schema.json +29 -3
- package/.mcp/schemas/repo_get_package_scripts.json +11 -3
- package/.mcp/schemas/repo_get_workflow_runs.json +12 -3
- package/.mcp/schemas/repo_logs.json +14 -3
- package/.mcp/schemas/repo_multi_exec.json +89 -0
- package/.mcp/schemas/repo_read_agent_context.json +117 -5
- package/.mcp/schemas/repo_read_ci_logs.json +46 -3
- package/CHANGELOG.md +22 -0
- package/README.md +7 -5
- package/dist/config/dynamic.js +3 -2
- package/dist/config/repo.js +5 -5
- package/dist/config.test.js +10 -3
- package/dist/lib/git.js +78 -6
- package/dist/lib/git.test.js +34 -1
- package/dist/mcp/definitions.js +49 -36
- package/dist/mcp/http.js +15 -0
- package/dist/mcp/http.test.js +8 -0
- package/dist/mcp/server.js +34 -0
- package/dist/mcp/server.test.js +31 -0
- package/dist/tools/contract.js +1264 -92
- package/dist/tools/github.get_merge_conflict_files.js +10 -2
- package/dist/tools/jules/create-session.js +12 -2
- package/dist/tools/jules/create-session.test.js +7 -0
- package/dist/tools/repo.create_branch.js +18 -4
- package/dist/tools/repo.create_branch.test.js +41 -5
- package/dist/tools/repo.create_repair_branch.js +26 -9
- package/dist/tools/repo.get_changed_files.js +10 -2
- package/dist/tools/repo.multi_exec.js +21 -0
- package/dist/tools/repo.multi_exec.test.js +13 -0
- package/dist/tools/repo.read_agent_context.js +65 -3
- package/dist/tools/repo.read_agent_context.test.js +30 -1
- package/package.json +1 -1
- package/src/config/dynamic.ts +3 -2
- package/src/config/repo.ts +6 -6
- package/src/config.test.ts +11 -3
- package/src/lib/git.test.ts +45 -1
- package/src/lib/git.ts +94 -6
- package/src/mcp/definitions.ts +77 -36
- package/src/mcp/http.test.ts +10 -0
- package/src/mcp/http.ts +20 -0
- package/src/mcp/server.test.ts +38 -0
- package/src/mcp/server.ts +36 -0
- package/src/tools/contract.ts +1355 -67
- package/src/tools/github.get_merge_conflict_files.ts +11 -2
- package/src/tools/jules/create-session.test.ts +9 -0
- package/src/tools/jules/create-session.ts +13 -2
- package/src/tools/repo.create_branch.test.ts +73 -5
- package/src/tools/repo.create_branch.ts +30 -4
- package/src/tools/repo.create_repair_branch.ts +31 -9
- package/src/tools/repo.get_changed_files.ts +10 -2
- package/src/tools/repo.multi_exec.test.ts +15 -0
- package/src/tools/repo.multi_exec.ts +23 -0
- package/src/tools/repo.read_agent_context.test.ts +35 -1
- package/src/tools/repo.read_agent_context.ts +68 -3
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { getMergeConflictFiles } from "../lib/git.js";
|
|
2
|
+
import { getMergeConflictFiles, resolveRepoPath } from "../lib/git.js";
|
|
3
3
|
export const GetMergeConflictFilesInputSchema = z.object({
|
|
4
4
|
prNumber: z.number().optional(),
|
|
5
5
|
baseBranch: z.string().optional().default("main"),
|
|
6
6
|
headRef: z.string().optional().default("HEAD"),
|
|
7
|
+
repo: z.string().optional().describe("Target GitHub repository override."),
|
|
8
|
+
path: z.string().optional().describe("Local filesystem path to target repository root."),
|
|
9
|
+
repoPath: z.string().optional().describe("Alias for path to target repository root."),
|
|
7
10
|
});
|
|
8
11
|
export async function getMergeConflictFilesHandler(args) {
|
|
9
12
|
const baseBranch = args.baseBranch || "main";
|
|
10
13
|
const headRef = args.headRef || "HEAD";
|
|
11
|
-
const
|
|
14
|
+
const targetCwd = resolveRepoPath({
|
|
15
|
+
path: args.path,
|
|
16
|
+
repoPath: args.repoPath,
|
|
17
|
+
repo: args.repo,
|
|
18
|
+
});
|
|
19
|
+
const conflictFiles = await getMergeConflictFiles(baseBranch, headRef, targetCwd);
|
|
12
20
|
const hasConflicts = conflictFiles.length > 0;
|
|
13
21
|
return {
|
|
14
22
|
hasConflicts,
|
|
@@ -4,16 +4,26 @@ export const CreateJulesSessionInputSchema = z.object({
|
|
|
4
4
|
task: z.string(),
|
|
5
5
|
branch: z.string().optional(),
|
|
6
6
|
pr: z.number().optional(),
|
|
7
|
+
repo: z.string().optional(),
|
|
7
8
|
});
|
|
8
9
|
export async function createJulesSessionHandler(input) {
|
|
9
10
|
let branch = input.branch;
|
|
10
11
|
if (!branch && input.pr) {
|
|
11
|
-
const
|
|
12
|
+
const ghArgs = ["gh", "view", input.pr.toString()];
|
|
13
|
+
if (input.repo) {
|
|
14
|
+
ghArgs.push("--repo", input.repo);
|
|
15
|
+
}
|
|
16
|
+
const prData = await runTDCli(ghArgs);
|
|
12
17
|
branch = prData.pr.headRefName;
|
|
13
18
|
}
|
|
14
19
|
if (!branch) {
|
|
15
20
|
branch = process.env.DEFAULT_BASE_BRANCH || "main";
|
|
16
21
|
}
|
|
17
|
-
const
|
|
22
|
+
const dispatchArgs = ["agent", "dispatch"];
|
|
23
|
+
if (input.repo) {
|
|
24
|
+
dispatchArgs.push("--repo", input.repo);
|
|
25
|
+
}
|
|
26
|
+
dispatchArgs.push(branch, input.task);
|
|
27
|
+
const output = await runTDCli(dispatchArgs);
|
|
18
28
|
return parseJulesSession(output.session || output);
|
|
19
29
|
}
|
|
@@ -41,6 +41,13 @@ describe("createJulesSessionHandler", () => {
|
|
|
41
41
|
"agent", "dispatch", "feature-x", "do work"
|
|
42
42
|
]);
|
|
43
43
|
});
|
|
44
|
+
it("should create a session with repo parameter", async () => {
|
|
45
|
+
const result = await createJulesSessionHandler({ task: "do work", branch: "main", repo: "arii/portfolio" });
|
|
46
|
+
expect(result.id).toBe("1234567890");
|
|
47
|
+
expect(shell.runCommand).toHaveBeenCalledWith("td", [
|
|
48
|
+
"agent", "dispatch", "--repo", "arii/portfolio", "main", "do work"
|
|
49
|
+
]);
|
|
50
|
+
});
|
|
44
51
|
it("should create a session with PR number", async () => {
|
|
45
52
|
vi.mocked(shell.runCommand).mockImplementation(async (cmd, args) => {
|
|
46
53
|
if (args[1] === "view") {
|
|
@@ -1,21 +1,35 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { runCommand } from "../lib/shell.js";
|
|
3
|
+
import { resolveRepoPath } from "../lib/git.js";
|
|
3
4
|
export const CreateBranchInputSchema = z.object({
|
|
4
5
|
branchName: z.string(),
|
|
5
6
|
baseBranch: z.string().optional().default("main"),
|
|
7
|
+
repo: z.string().optional().describe("Target repository override (e.g. org/repo or repo)."),
|
|
8
|
+
path: z.string().optional().describe("Local filesystem path to target repository root."),
|
|
9
|
+
repoPath: z.string().optional().describe("Alias for path to target repository root."),
|
|
6
10
|
});
|
|
7
11
|
export async function createBranchHandler(args) {
|
|
8
12
|
const params = CreateBranchInputSchema.parse(args);
|
|
13
|
+
const targetCwd = resolveRepoPath({
|
|
14
|
+
path: params.path,
|
|
15
|
+
repoPath: params.repoPath,
|
|
16
|
+
repo: params.repo,
|
|
17
|
+
});
|
|
9
18
|
// Ensure latest base branch
|
|
10
|
-
await runCommand("git", ["fetch", "origin", params.baseBranch]);
|
|
19
|
+
await runCommand("git", ["fetch", "origin", params.baseBranch], { cwd: targetCwd });
|
|
11
20
|
// Create branch from origin base branch
|
|
12
|
-
const result = await runCommand("git", ["checkout", "-b", params.branchName, `origin/${params.baseBranch}`]);
|
|
21
|
+
const result = await runCommand("git", ["checkout", "-b", params.branchName, `origin/${params.baseBranch}`], { cwd: targetCwd });
|
|
13
22
|
if (result.exitCode !== 0) {
|
|
14
23
|
throw new Error(`Failed to create branch ${params.branchName}: ${result.stderr}`);
|
|
15
24
|
}
|
|
16
|
-
|
|
25
|
+
const response = {
|
|
17
26
|
success: true,
|
|
18
27
|
branchName: params.branchName,
|
|
19
|
-
baseBranch: params.baseBranch
|
|
28
|
+
baseBranch: params.baseBranch,
|
|
29
|
+
path: targetCwd,
|
|
20
30
|
};
|
|
31
|
+
if (params.repo) {
|
|
32
|
+
response.repo = params.repo;
|
|
33
|
+
}
|
|
34
|
+
return response;
|
|
21
35
|
}
|
|
@@ -15,13 +15,14 @@ describe("repo.create_branch", () => {
|
|
|
15
15
|
};
|
|
16
16
|
shell.runCommand.mockResolvedValue({ exitCode: 0, stdout: "", stderr: "" });
|
|
17
17
|
const result = await createBranchHandler(args);
|
|
18
|
-
expect(shell.runCommand).toHaveBeenCalledWith("git", ["fetch", "origin", "main"]);
|
|
19
|
-
expect(shell.runCommand).toHaveBeenCalledWith("git", ["checkout", "-b", "new-feature", "origin/main"]);
|
|
20
|
-
expect(result).
|
|
18
|
+
expect(shell.runCommand).toHaveBeenCalledWith("git", ["fetch", "origin", "main"], { cwd: expect.any(String) });
|
|
19
|
+
expect(shell.runCommand).toHaveBeenCalledWith("git", ["checkout", "-b", "new-feature", "origin/main"], { cwd: expect.any(String) });
|
|
20
|
+
expect(result).toMatchObject({
|
|
21
21
|
success: true,
|
|
22
22
|
branchName: "new-feature",
|
|
23
23
|
baseBranch: "main",
|
|
24
24
|
});
|
|
25
|
+
expect(result.path).toBeDefined();
|
|
25
26
|
});
|
|
26
27
|
it("should use default base branch 'main' if not provided", async () => {
|
|
27
28
|
const args = {
|
|
@@ -29,8 +30,43 @@ describe("repo.create_branch", () => {
|
|
|
29
30
|
};
|
|
30
31
|
shell.runCommand.mockResolvedValue({ exitCode: 0, stdout: "", stderr: "" });
|
|
31
32
|
const result = await createBranchHandler(args);
|
|
32
|
-
expect(shell.runCommand).toHaveBeenCalledWith("git", ["fetch", "origin", "main"]);
|
|
33
|
-
expect(shell.runCommand).toHaveBeenCalledWith("git", ["checkout", "-b", "another-feature", "origin/main"]);
|
|
33
|
+
expect(shell.runCommand).toHaveBeenCalledWith("git", ["fetch", "origin", "main"], { cwd: expect.any(String) });
|
|
34
|
+
expect(shell.runCommand).toHaveBeenCalledWith("git", ["checkout", "-b", "another-feature", "origin/main"], { cwd: expect.any(String) });
|
|
35
|
+
expect(result.success).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
it("should create branch in specified target workspace path", async () => {
|
|
38
|
+
const args = {
|
|
39
|
+
branchName: "custom-path-feature",
|
|
40
|
+
baseBranch: "main",
|
|
41
|
+
path: "/home/ari/tech-dancer",
|
|
42
|
+
};
|
|
43
|
+
shell.runCommand.mockResolvedValue({ exitCode: 0, stdout: "", stderr: "" });
|
|
44
|
+
const result = await createBranchHandler(args);
|
|
45
|
+
expect(shell.runCommand).toHaveBeenCalledWith("git", ["fetch", "origin", "main"], { cwd: "/home/ari/tech-dancer" });
|
|
46
|
+
expect(shell.runCommand).toHaveBeenCalledWith("git", ["checkout", "-b", "custom-path-feature", "origin/main"], { cwd: "/home/ari/tech-dancer" });
|
|
47
|
+
expect(result).toEqual({
|
|
48
|
+
success: true,
|
|
49
|
+
branchName: "custom-path-feature",
|
|
50
|
+
baseBranch: "main",
|
|
51
|
+
path: "/home/ari/tech-dancer",
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
it("should include repo field when repo override is specified", async () => {
|
|
55
|
+
const args = {
|
|
56
|
+
branchName: "repo-feature",
|
|
57
|
+
baseBranch: "main",
|
|
58
|
+
repo: "arii/tech-dancer",
|
|
59
|
+
path: "/home/ari/tech-dancer",
|
|
60
|
+
};
|
|
61
|
+
shell.runCommand.mockResolvedValue({ exitCode: 0, stdout: "", stderr: "" });
|
|
62
|
+
const result = await createBranchHandler(args);
|
|
63
|
+
expect(result).toEqual({
|
|
64
|
+
success: true,
|
|
65
|
+
branchName: "repo-feature",
|
|
66
|
+
baseBranch: "main",
|
|
67
|
+
path: "/home/ari/tech-dancer",
|
|
68
|
+
repo: "arii/tech-dancer",
|
|
69
|
+
});
|
|
34
70
|
});
|
|
35
71
|
it("should throw error if git command fails", async () => {
|
|
36
72
|
const args = {
|
|
@@ -1,38 +1,55 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { runCommand } from "../lib/shell.js";
|
|
3
|
-
import { createWorktree } from "../lib/git.js";
|
|
3
|
+
import { createWorktree, resolveRepoPath } from "../lib/git.js";
|
|
4
4
|
export const CreateRepairBranchInputSchema = z.object({
|
|
5
5
|
prNumber: z.number(),
|
|
6
6
|
repairBranchName: z.string().optional(),
|
|
7
7
|
writeMode: z.boolean().optional().default(false),
|
|
8
|
+
repo: z.string().optional().describe("Target repository override (e.g. org/repo or repo)."),
|
|
9
|
+
path: z.string().optional().describe("Local filesystem path to target repository root."),
|
|
10
|
+
repoPath: z.string().optional().describe("Alias for path to target repository root."),
|
|
8
11
|
});
|
|
9
12
|
export async function createRepairBranchHandler(args) {
|
|
10
13
|
if (!args.writeMode) {
|
|
11
14
|
throw new Error("writeMode must be true to create a repair branch.");
|
|
12
15
|
}
|
|
13
|
-
const
|
|
16
|
+
const targetCwd = resolveRepoPath({
|
|
17
|
+
path: args.path,
|
|
18
|
+
repoPath: args.repoPath,
|
|
19
|
+
repo: args.repo,
|
|
20
|
+
});
|
|
21
|
+
const ghArgs = [
|
|
14
22
|
"pr",
|
|
15
23
|
"view",
|
|
16
24
|
args.prNumber.toString(),
|
|
17
25
|
"--json", "headRefName,baseRefName,title"
|
|
18
|
-
]
|
|
26
|
+
];
|
|
27
|
+
if (args.repo) {
|
|
28
|
+
ghArgs.push("--repo", args.repo);
|
|
29
|
+
}
|
|
30
|
+
const prResult = await runCommand("gh", ghArgs, { cwd: targetCwd });
|
|
19
31
|
if (prResult.exitCode !== 0) {
|
|
20
32
|
throw new Error(`Failed to get PR info: ${prResult.stderr}`);
|
|
21
33
|
}
|
|
22
34
|
const { headRefName, baseRefName, title } = JSON.parse(prResult.stdout);
|
|
23
35
|
const sanitizedTitle = title.toLowerCase().replace(/[^a-z0-9]/g, "-").substring(0, 30);
|
|
24
36
|
const repairBranch = args.repairBranchName || `agent/repair-pr-${args.prNumber}-${sanitizedTitle}`;
|
|
25
|
-
// Ensure latest
|
|
26
|
-
await runCommand("git", ["fetch", "origin", headRefName]);
|
|
27
|
-
await runCommand("git", ["fetch", "origin", baseRefName]);
|
|
28
|
-
const worktreePath = await createWorktree(`origin/${headRefName}`, args.prNumber);
|
|
37
|
+
// Ensure latest in target repo
|
|
38
|
+
await runCommand("git", ["fetch", "origin", headRefName], { cwd: targetCwd });
|
|
39
|
+
await runCommand("git", ["fetch", "origin", baseRefName], { cwd: targetCwd });
|
|
40
|
+
const worktreePath = await createWorktree(`origin/${headRefName}`, args.prNumber, targetCwd);
|
|
29
41
|
// Create the actual repair branch in the worktree
|
|
30
42
|
await runCommand("git", ["checkout", "-b", repairBranch], { cwd: worktreePath });
|
|
31
|
-
|
|
43
|
+
const response = {
|
|
32
44
|
prNumber: args.prNumber,
|
|
33
45
|
originalBranch: headRefName,
|
|
34
46
|
repairBranch,
|
|
35
47
|
baseBranch: baseRefName,
|
|
36
|
-
worktreePath
|
|
48
|
+
worktreePath,
|
|
49
|
+
path: targetCwd,
|
|
37
50
|
};
|
|
51
|
+
if (args.repo) {
|
|
52
|
+
response.repo = args.repo;
|
|
53
|
+
}
|
|
54
|
+
return response;
|
|
38
55
|
}
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { getChangedFiles } from "../lib/git.js";
|
|
2
|
+
import { getChangedFiles, resolveRepoPath } from "../lib/git.js";
|
|
3
3
|
export const GetChangedFilesInputSchema = z.object({
|
|
4
4
|
base: z.string().optional().default("main"),
|
|
5
5
|
head: z.string().optional().default("HEAD"),
|
|
6
|
+
repo: z.string().optional().describe("Target GitHub repository override."),
|
|
7
|
+
path: z.string().optional().describe("Local filesystem path to target repository root."),
|
|
8
|
+
repoPath: z.string().optional().describe("Alias for path to target repository root."),
|
|
6
9
|
});
|
|
7
10
|
export async function getChangedFilesHandler(args) {
|
|
8
|
-
const
|
|
11
|
+
const targetCwd = resolveRepoPath({
|
|
12
|
+
path: args.path,
|
|
13
|
+
repoPath: args.repoPath,
|
|
14
|
+
repo: args.repo,
|
|
15
|
+
});
|
|
16
|
+
const files = await getChangedFiles(args.base || "main", args.head || "HEAD", targetCwd);
|
|
9
17
|
return { files };
|
|
10
18
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { runTDCli } from "./jules/shared.js";
|
|
3
|
+
export const MultiExecInputSchema = z.object({
|
|
4
|
+
command: z.string().describe("Shell command to execute sequentially across repositories"),
|
|
5
|
+
repos: z.array(z.string()).optional().describe("Optional list of repository names to target"),
|
|
6
|
+
continueOnError: z.boolean().optional().default(true).describe("Whether to continue execution if a repo command fails"),
|
|
7
|
+
});
|
|
8
|
+
export async function multiExecHandler(input) {
|
|
9
|
+
const args = MultiExecInputSchema.parse(input);
|
|
10
|
+
const cliArgs = ["multi-repo", "exec", args.command];
|
|
11
|
+
if (args.repos) {
|
|
12
|
+
for (const r of args.repos) {
|
|
13
|
+
cliArgs.push("-r", r);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
if (args.continueOnError === false) {
|
|
17
|
+
cliArgs.push("--fail-fast");
|
|
18
|
+
}
|
|
19
|
+
const output = await runTDCli(cliArgs, "execute command across ecosystem repositories");
|
|
20
|
+
return output;
|
|
21
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { multiExecHandler } from "./repo.multi_exec.js";
|
|
3
|
+
describe("MultiExec Tool", () => {
|
|
4
|
+
it("should execute a command sequentially across repositories", async () => {
|
|
5
|
+
const res = await multiExecHandler({
|
|
6
|
+
command: "echo test-vitest-exec",
|
|
7
|
+
repos: ["arii/boomtick"],
|
|
8
|
+
});
|
|
9
|
+
expect(res.status).toBe("success");
|
|
10
|
+
expect(res.total_repos).toBe(1);
|
|
11
|
+
expect(res.results[0].stdout).toContain("test-vitest-exec");
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -3,6 +3,7 @@ import { config } from "../config.js";
|
|
|
3
3
|
import { execSync } from "child_process";
|
|
4
4
|
import * as fs from "fs";
|
|
5
5
|
import * as path from "path";
|
|
6
|
+
import { runTDCli } from "./jules/shared.js";
|
|
6
7
|
export const loadStandaloneAgentContext = (repoRoot = process.cwd()) => {
|
|
7
8
|
const configPath = path.join(repoRoot, "project_config.json");
|
|
8
9
|
let projectConfig = {};
|
|
@@ -27,13 +28,74 @@ export const loadStandaloneAgentContext = (repoRoot = process.cwd()) => {
|
|
|
27
28
|
config: projectConfig,
|
|
28
29
|
};
|
|
29
30
|
};
|
|
30
|
-
export const ReadAgentContextInputSchema = z.object({
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
export const ReadAgentContextInputSchema = z.object({
|
|
32
|
+
path: z.string().optional().describe("Local filesystem path to target repository root"),
|
|
33
|
+
repoPath: z.string().optional().describe("Alias for path to target repository root"),
|
|
34
|
+
repo: z.string().optional().describe("Repository name override (e.g. arii/portfolio or portfolio)"),
|
|
35
|
+
repos: z.array(z.string()).optional().describe("List of repository names to fetch context for"),
|
|
36
|
+
all: z.boolean().optional().describe("Whether to fetch context across all discovered ecosystem repositories"),
|
|
37
|
+
});
|
|
38
|
+
export async function readAgentContextHandler(args) {
|
|
39
|
+
// 1. Ecosystem / Multi-repo request
|
|
40
|
+
if (args.all || (args.repos && args.repos.length > 0)) {
|
|
41
|
+
const cliArgs = ["multi-repo", "status"];
|
|
42
|
+
if (args.repos) {
|
|
43
|
+
for (const r of args.repos) {
|
|
44
|
+
cliArgs.push("-r", r);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const output = await runTDCli(cliArgs, "fetch multi-repo context");
|
|
48
|
+
return {
|
|
49
|
+
status: "success",
|
|
50
|
+
ecosystem: true,
|
|
51
|
+
repos: output.repos ?? [],
|
|
52
|
+
total: output.total ?? (output.repos?.length ?? 0)
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
// 2. Specific repository name override
|
|
56
|
+
if (args.repo) {
|
|
57
|
+
let matchedPath;
|
|
58
|
+
try {
|
|
59
|
+
const output = await runTDCli(["multi-repo", "status", "-r", args.repo], "find target repository");
|
|
60
|
+
const matched = output.repos?.[0];
|
|
61
|
+
if (matched?.path) {
|
|
62
|
+
matchedPath = matched.path;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// Fallback to directory inspection if CLI is unavailable or not in TD workspace
|
|
67
|
+
}
|
|
68
|
+
if (!matchedPath) {
|
|
69
|
+
const repoName = args.repo.includes("/") ? args.repo.split("/")[1] : args.repo;
|
|
70
|
+
const cwdParent = path.resolve(process.cwd(), "..", repoName);
|
|
71
|
+
const configParent = path.resolve(config.repoPath, "..", repoName);
|
|
72
|
+
if (fs.existsSync(cwdParent)) {
|
|
73
|
+
matchedPath = cwdParent;
|
|
74
|
+
}
|
|
75
|
+
else if (fs.existsSync(configParent)) {
|
|
76
|
+
matchedPath = configParent;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (matchedPath) {
|
|
80
|
+
const context = loadStandaloneAgentContext(matchedPath);
|
|
81
|
+
return {
|
|
82
|
+
status: "success",
|
|
83
|
+
repo: args.repo,
|
|
84
|
+
path: matchedPath,
|
|
85
|
+
...context
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// 3. Specific filesystem path
|
|
90
|
+
const targetPath = (args.path || args.repoPath) ? path.resolve(args.path || args.repoPath) : config.repoPath;
|
|
91
|
+
const context = loadStandaloneAgentContext(targetPath);
|
|
33
92
|
const result = {
|
|
34
93
|
status: "success",
|
|
35
94
|
...context
|
|
36
95
|
};
|
|
96
|
+
if (args?.repo) {
|
|
97
|
+
result.repo = args.repo;
|
|
98
|
+
}
|
|
37
99
|
delete result.submodules;
|
|
38
100
|
if (result.config) {
|
|
39
101
|
delete result.config.submodules;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { loadStandaloneAgentContext } from "./repo.read_agent_context.js";
|
|
2
|
+
import { loadStandaloneAgentContext, readAgentContextHandler } from "./repo.read_agent_context.js";
|
|
3
3
|
describe("Standalone Agent Context Loader", () => {
|
|
4
4
|
it("should load agent context without submodule references", () => {
|
|
5
5
|
const context = loadStandaloneAgentContext(process.cwd());
|
|
@@ -7,4 +7,33 @@ describe("Standalone Agent Context Loader", () => {
|
|
|
7
7
|
expect(context).toHaveProperty("gitCommit");
|
|
8
8
|
expect(context).not.toHaveProperty("submodules");
|
|
9
9
|
});
|
|
10
|
+
it("should handle readAgentContextHandler with default workspace", async () => {
|
|
11
|
+
const res = await readAgentContextHandler({});
|
|
12
|
+
expect(res.status).toBe("success");
|
|
13
|
+
expect(res).toHaveProperty("packageName");
|
|
14
|
+
expect(res).toHaveProperty("gitCommit");
|
|
15
|
+
});
|
|
16
|
+
it("readAgentContextHandler should accept optional repoPath argument", async () => {
|
|
17
|
+
const res = await readAgentContextHandler({ repoPath: process.cwd() });
|
|
18
|
+
expect(res).toHaveProperty("status", "success");
|
|
19
|
+
expect(res).toHaveProperty("packageName");
|
|
20
|
+
expect(res).toHaveProperty("gitCommit");
|
|
21
|
+
});
|
|
22
|
+
it("readAgentContextHandler should accept path argument", async () => {
|
|
23
|
+
const res = await readAgentContextHandler({ path: process.cwd() });
|
|
24
|
+
expect(res).toHaveProperty("status", "success");
|
|
25
|
+
expect(res).toHaveProperty("packageName");
|
|
26
|
+
});
|
|
27
|
+
it("readAgentContextHandler should accept repo argument", async () => {
|
|
28
|
+
const res = await readAgentContextHandler({ repo: "arii/tech-dancer" });
|
|
29
|
+
expect(res).toHaveProperty("status", "success");
|
|
30
|
+
expect(res).toHaveProperty("repo", "arii/tech-dancer");
|
|
31
|
+
});
|
|
32
|
+
it("should handle multi-repo context querying when all: true is passed", async () => {
|
|
33
|
+
const res = await readAgentContextHandler({ all: true });
|
|
34
|
+
expect(res.status).toBe("success");
|
|
35
|
+
expect(res.ecosystem).toBe(true);
|
|
36
|
+
expect(Array.isArray(res.repos)).toBe(true);
|
|
37
|
+
expect(res.repos.length).toBeGreaterThan(0);
|
|
38
|
+
});
|
|
10
39
|
});
|
package/package.json
CHANGED
package/src/config/dynamic.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { execSync } from "child_process";
|
|
2
|
+
import { findRepoRoot } from "./repo.js";
|
|
2
3
|
|
|
3
4
|
let cachedDynamicConfig: any = null;
|
|
4
5
|
|
|
5
|
-
export function initializeConfig() {
|
|
6
|
+
export function initializeConfig(targetCwd?: string) {
|
|
6
7
|
if (cachedDynamicConfig !== null) {
|
|
7
8
|
return cachedDynamicConfig;
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
try {
|
|
11
|
-
const cwd =
|
|
12
|
+
const cwd = findRepoRoot(targetCwd);
|
|
12
13
|
const cmd = `td config view`;
|
|
13
14
|
const output = execSync(cmd, {
|
|
14
15
|
cwd,
|
package/src/config/repo.ts
CHANGED
|
@@ -4,12 +4,8 @@ import { fileURLToPath } from "url";
|
|
|
4
4
|
|
|
5
5
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
6
|
|
|
7
|
-
export function findRepoRoot(): string {
|
|
8
|
-
|
|
9
|
-
return process.env.BOOMTICK_REPO_PATH;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
let current = process.cwd();
|
|
7
|
+
export function findRepoRoot(startDir?: string): string {
|
|
8
|
+
let current = startDir || process.cwd();
|
|
13
9
|
while (current !== path.parse(current).root) {
|
|
14
10
|
if (
|
|
15
11
|
fs.existsSync(path.join(current, "project_config.json")) ||
|
|
@@ -21,6 +17,10 @@ export function findRepoRoot(): string {
|
|
|
21
17
|
current = path.dirname(current);
|
|
22
18
|
}
|
|
23
19
|
|
|
20
|
+
if (process.env.BOOMTICK_REPO_PATH) {
|
|
21
|
+
return process.env.BOOMTICK_REPO_PATH;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
24
|
current = __dirname;
|
|
25
25
|
while (current !== path.parse(current).root) {
|
|
26
26
|
if (
|
package/src/config.test.ts
CHANGED
|
@@ -25,10 +25,11 @@ describe("config", () => {
|
|
|
25
25
|
vi.mocked(execSync).mockReturnValue(JSON.stringify(mockConfig));
|
|
26
26
|
|
|
27
27
|
const { initializeConfig } = await import("./config.js");
|
|
28
|
+
const { findRepoRoot } = await import("./config/repo.js");
|
|
28
29
|
const result = initializeConfig();
|
|
29
30
|
|
|
30
31
|
expect(execSync).toHaveBeenCalledWith("td config view", expect.objectContaining({
|
|
31
|
-
cwd:
|
|
32
|
+
cwd: findRepoRoot()
|
|
32
33
|
}));
|
|
33
34
|
expect(result).toEqual(mockConfig);
|
|
34
35
|
});
|
|
@@ -133,10 +134,17 @@ describe("config", () => {
|
|
|
133
134
|
expect(() => config.githubRepo).toThrow("GITHUB_REPO must be set via environment variable or project_config.json");
|
|
134
135
|
});
|
|
135
136
|
|
|
136
|
-
it("should resolve
|
|
137
|
+
it("should resolve active workspace from process.cwd() over BOOMTICK_REPO_PATH when inside a repo", async () => {
|
|
137
138
|
process.env.BOOMTICK_REPO_PATH = "/custom/repo/path";
|
|
138
139
|
const { config } = await import("./config.js");
|
|
139
|
-
expect(config.repoPath).toBe("
|
|
140
|
+
expect(config.repoPath).toBe(process.cwd().replace(/\/mcp$/, ""));
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("should fallback to BOOMTICK_REPO_PATH when process.cwd() is outside any repo", async () => {
|
|
144
|
+
process.env.BOOMTICK_REPO_PATH = "/custom/repo/path";
|
|
145
|
+
const { findRepoRoot } = await import("./config/repo.js");
|
|
146
|
+
const rootDir = process.platform === "win32" ? "C:\\" : "/";
|
|
147
|
+
expect(findRepoRoot(rootDir)).toBe("/custom/repo/path");
|
|
140
148
|
});
|
|
141
149
|
|
|
142
150
|
it("should resolve defaultBaseBranch, viteBasePath, and ghPath", async () => {
|
package/src/lib/git.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
2
|
-
import { getChangedFiles, getMergeConflictFiles } from "./git.js";
|
|
2
|
+
import { getChangedFiles, getMergeConflictFiles, resolveRepoPath, createWorktree } from "./git.js";
|
|
3
3
|
import * as shell from "./shell.js";
|
|
4
4
|
|
|
5
5
|
vi.mock("./shell.js", () => ({
|
|
@@ -107,4 +107,48 @@ describe("Git Helpers", () => {
|
|
|
107
107
|
);
|
|
108
108
|
});
|
|
109
109
|
});
|
|
110
|
+
|
|
111
|
+
describe("resolveRepoPath", () => {
|
|
112
|
+
it("should return explicit path when path is provided", () => {
|
|
113
|
+
const resolved = resolveRepoPath({ path: "/tmp/custom-repo" });
|
|
114
|
+
expect(resolved).toBe("/tmp/custom-repo");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("should return explicit repoPath when repoPath is provided", () => {
|
|
118
|
+
const resolved = resolveRepoPath({ repoPath: "/tmp/another-repo" });
|
|
119
|
+
expect(resolved).toBe("/tmp/another-repo");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("should resolve repo path via ecosystem_repos when repo is given", () => {
|
|
123
|
+
const resolved = resolveRepoPath({ repo: "arii/tech-dancer" });
|
|
124
|
+
expect(resolved).toContain("tech-dancer");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("should fall back to current working directory when empty", () => {
|
|
128
|
+
const resolved = resolveRepoPath({});
|
|
129
|
+
expect(typeof resolved).toBe("string");
|
|
130
|
+
expect(resolved.length).toBeGreaterThan(0);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("createWorktree", () => {
|
|
135
|
+
it("should pass baseRepoPath cwd to runCommand when provided", async () => {
|
|
136
|
+
vi.mocked(shell.runCommand).mockResolvedValue({
|
|
137
|
+
exitCode: 0,
|
|
138
|
+
stdout: "",
|
|
139
|
+
stderr: "",
|
|
140
|
+
durationMs: 5,
|
|
141
|
+
command: "git",
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const worktree = await createWorktree("origin/feature", 123, "/home/ari/tech-dancer");
|
|
145
|
+
|
|
146
|
+
expect(worktree).toContain("boomtick-mcp-rescue-123");
|
|
147
|
+
expect(shell.runCommand).toHaveBeenCalledWith(
|
|
148
|
+
"git",
|
|
149
|
+
expect.arrayContaining(["worktree", "add"]),
|
|
150
|
+
{ cwd: "/home/ari/tech-dancer" }
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
110
154
|
});
|