@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.
Files changed (109) hide show
  1. package/.mcp/schemas/boomtick.health.json +22 -3
  2. package/.mcp/schemas/boomtick_health.json +22 -3
  3. package/.mcp/schemas/github.checkout_branch.json +16 -3
  4. package/.mcp/schemas/github.comment_triage_summary.json +16 -3
  5. package/.mcp/schemas/github.create_pull_request.json +16 -3
  6. package/.mcp/schemas/github.get_merge_conflict_files.json +55 -5
  7. package/.mcp/schemas/github.get_pr.json +12 -3
  8. package/.mcp/schemas/github.get_pr_commits.json +12 -3
  9. package/.mcp/schemas/github.get_pr_reviews.json +24 -3
  10. package/.mcp/schemas/github.issue_comment.json +19 -3
  11. package/.mcp/schemas/github.issue_view.json +17 -3
  12. package/.mcp/schemas/github.open_replacement_pr.json +16 -3
  13. package/.mcp/schemas/github_checkout_branch.json +16 -3
  14. package/.mcp/schemas/github_comment_triage_summary.json +16 -3
  15. package/.mcp/schemas/github_create_pull_request.json +16 -3
  16. package/.mcp/schemas/github_get_merge_conflict_files.json +55 -5
  17. package/.mcp/schemas/github_get_pr.json +12 -3
  18. package/.mcp/schemas/github_get_pr_commits.json +12 -3
  19. package/.mcp/schemas/github_get_pr_reviews.json +24 -3
  20. package/.mcp/schemas/github_issue_comment.json +19 -3
  21. package/.mcp/schemas/github_issue_view.json +17 -3
  22. package/.mcp/schemas/github_open_replacement_pr.json +16 -3
  23. package/.mcp/schemas/jules.cancel_session.json +38 -3
  24. package/.mcp/schemas/jules.create_session.json +52 -3
  25. package/.mcp/schemas/jules.get_messages.json +19 -3
  26. package/.mcp/schemas/jules.get_pr.json +23 -3
  27. package/.mcp/schemas/jules.get_session.json +38 -3
  28. package/.mcp/schemas/jules.list_sessions.json +53 -3
  29. package/.mcp/schemas/jules.send_message.json +31 -3
  30. package/.mcp/schemas/jules.trigger_feedback.json +28 -3
  31. package/.mcp/schemas/jules_cancel_session.json +38 -3
  32. package/.mcp/schemas/jules_create_session.json +52 -3
  33. package/.mcp/schemas/jules_get_messages.json +19 -3
  34. package/.mcp/schemas/jules_get_pr.json +23 -3
  35. package/.mcp/schemas/jules_get_session.json +38 -3
  36. package/.mcp/schemas/jules_list_sessions.json +53 -3
  37. package/.mcp/schemas/jules_send_message.json +31 -3
  38. package/.mcp/schemas/jules_trigger_feedback.json +28 -3
  39. package/.mcp/schemas/repo.check_jules_status.json +93 -3
  40. package/.mcp/schemas/repo.commit_patch.json +23 -3
  41. package/.mcp/schemas/repo.create_branch.json +91 -3
  42. package/.mcp/schemas/repo.create_repair_branch.json +94 -3
  43. package/.mcp/schemas/repo.get_changed_files.json +50 -3
  44. package/.mcp/schemas/repo.get_command_schema.json +29 -3
  45. package/.mcp/schemas/repo.get_package_scripts.json +11 -3
  46. package/.mcp/schemas/repo.get_workflow_runs.json +12 -3
  47. package/.mcp/schemas/repo.logs.json +14 -3
  48. package/.mcp/schemas/repo.multi_exec.json +89 -0
  49. package/.mcp/schemas/repo.read_agent_context.json +117 -5
  50. package/.mcp/schemas/repo.read_ci_logs.json +46 -3
  51. package/.mcp/schemas/repo_check_jules_status.json +93 -3
  52. package/.mcp/schemas/repo_commit_patch.json +23 -3
  53. package/.mcp/schemas/repo_create_branch.json +91 -3
  54. package/.mcp/schemas/repo_create_repair_branch.json +94 -3
  55. package/.mcp/schemas/repo_get_changed_files.json +50 -3
  56. package/.mcp/schemas/repo_get_command_schema.json +29 -3
  57. package/.mcp/schemas/repo_get_package_scripts.json +11 -3
  58. package/.mcp/schemas/repo_get_workflow_runs.json +12 -3
  59. package/.mcp/schemas/repo_logs.json +14 -3
  60. package/.mcp/schemas/repo_multi_exec.json +89 -0
  61. package/.mcp/schemas/repo_read_agent_context.json +117 -5
  62. package/.mcp/schemas/repo_read_ci_logs.json +46 -3
  63. package/CHANGELOG.md +22 -0
  64. package/README.md +7 -5
  65. package/dist/config/dynamic.js +3 -2
  66. package/dist/config/repo.js +5 -5
  67. package/dist/config.test.js +10 -3
  68. package/dist/lib/git.js +78 -6
  69. package/dist/lib/git.test.js +34 -1
  70. package/dist/mcp/definitions.js +49 -36
  71. package/dist/mcp/http.js +15 -0
  72. package/dist/mcp/http.test.js +8 -0
  73. package/dist/mcp/server.js +34 -0
  74. package/dist/mcp/server.test.js +31 -0
  75. package/dist/tools/contract.js +1264 -92
  76. package/dist/tools/github.get_merge_conflict_files.js +10 -2
  77. package/dist/tools/jules/create-session.js +12 -2
  78. package/dist/tools/jules/create-session.test.js +7 -0
  79. package/dist/tools/repo.create_branch.js +18 -4
  80. package/dist/tools/repo.create_branch.test.js +41 -5
  81. package/dist/tools/repo.create_repair_branch.js +26 -9
  82. package/dist/tools/repo.get_changed_files.js +10 -2
  83. package/dist/tools/repo.multi_exec.js +21 -0
  84. package/dist/tools/repo.multi_exec.test.js +13 -0
  85. package/dist/tools/repo.read_agent_context.js +65 -3
  86. package/dist/tools/repo.read_agent_context.test.js +30 -1
  87. package/package.json +1 -1
  88. package/src/config/dynamic.ts +3 -2
  89. package/src/config/repo.ts +6 -6
  90. package/src/config.test.ts +11 -3
  91. package/src/lib/git.test.ts +45 -1
  92. package/src/lib/git.ts +94 -6
  93. package/src/mcp/definitions.ts +77 -36
  94. package/src/mcp/http.test.ts +10 -0
  95. package/src/mcp/http.ts +20 -0
  96. package/src/mcp/server.test.ts +38 -0
  97. package/src/mcp/server.ts +36 -0
  98. package/src/tools/contract.ts +1355 -67
  99. package/src/tools/github.get_merge_conflict_files.ts +11 -2
  100. package/src/tools/jules/create-session.test.ts +9 -0
  101. package/src/tools/jules/create-session.ts +13 -2
  102. package/src/tools/repo.create_branch.test.ts +73 -5
  103. package/src/tools/repo.create_branch.ts +30 -4
  104. package/src/tools/repo.create_repair_branch.ts +31 -9
  105. package/src/tools/repo.get_changed_files.ts +10 -2
  106. package/src/tools/repo.multi_exec.test.ts +15 -0
  107. package/src/tools/repo.multi_exec.ts +23 -0
  108. package/src/tools/repo.read_agent_context.test.ts +35 -1
  109. 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 conflictFiles = await getMergeConflictFiles(baseBranch, headRef);
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 prData = await runTDCli(["gh", "view", input.pr.toString()]);
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 output = await runTDCli(["agent", "dispatch", branch, input.task]);
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
- return {
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).toEqual({
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 prResult = await runCommand("gh", [
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
- return {
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 files = await getChangedFiles(args.base || "main", args.head || "HEAD");
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
- export async function readAgentContextHandler(_args) {
32
- const context = loadStandaloneAgentContext(config.repoPath);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arii/boomtick-mcp",
3
- "version": "0.6.2",
3
+ "version": "0.7.1",
4
4
  "description": "Boomtick MCP Guide: PR Rescue + Merge Conflict Agent",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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 = process.env.BOOMTICK_REPO_PATH || process.cwd();
12
+ const cwd = findRepoRoot(targetCwd);
12
13
  const cmd = `td config view`;
13
14
  const output = execSync(cmd, {
14
15
  cwd,
@@ -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
- if (process.env.BOOMTICK_REPO_PATH) {
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 (
@@ -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: process.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 repoPath from BOOMTICK_REPO_PATH or findRepoRoot fallback", async () => {
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("/custom/repo/path");
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 () => {
@@ -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
  });