@arii/boomtick-mcp 0.4.0 → 0.4.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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.1](https://github.com/arii/boomtick/compare/mcp-v0.4.0...mcp-v0.4.1) (2026-09-22)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **cli,mcp:** improve workspace config resolution and github repo detection ([#787](https://github.com/arii/boomtick/issues/787)) ([0103e90](https://github.com/arii/boomtick/commit/0103e904d30213cc336a1ce9567ad59257494236))
9
+
3
10
  ## [0.4.0](https://github.com/arii/boomtick/compare/mcp-v0.3.7...mcp-v0.4.0) (2026-09-04)
4
11
 
5
12
 
package/dist/config.js CHANGED
@@ -1,13 +1,17 @@
1
- import dotenv from "dotenv";
2
1
  import path from "path";
3
2
  import fs from "fs";
4
3
  import { fileURLToPath } from "url";
5
4
  import { execSync } from "child_process";
6
5
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
- dotenv.config({
8
- path: path.resolve(__dirname, "../.env"),
9
- quiet: true
10
- });
6
+ const envPath = path.resolve(__dirname, "../.env");
7
+ if (fs.existsSync(envPath) && typeof process.loadEnvFile === "function") {
8
+ try {
9
+ process.loadEnvFile(envPath);
10
+ }
11
+ catch {
12
+ // Ignore if already loaded or invalid
13
+ }
14
+ }
11
15
  function getGithubToken() {
12
16
  if (process.env.GITHUB_TOKEN) {
13
17
  return process.env.GITHUB_TOKEN;
@@ -31,9 +35,11 @@ export function initializeConfig() {
31
35
  return cachedDynamicConfig;
32
36
  }
33
37
  try {
34
- // Attempt to load core properties from the Python CLI to avoid duplication
38
+ // Attempt to load core properties from the Python CLI in the active workspace
39
+ const cwd = process.env.BOOMTICK_REPO_PATH || process.cwd();
35
40
  const cmd = `td-cli config view`;
36
41
  const output = execSync(cmd, {
42
+ cwd,
37
43
  encoding: "utf-8",
38
44
  stdio: ["ignore", "pipe", "ignore"]
39
45
  });
@@ -58,10 +64,22 @@ function findRepoRoot() {
58
64
  if (process.env.BOOMTICK_REPO_PATH) {
59
65
  return process.env.BOOMTICK_REPO_PATH;
60
66
  }
61
- // Traverse up to find workspace.json or .git
62
- let current = __dirname;
67
+ // Traverse up from process.cwd() to find project_config.json, workspace.json, or .git
68
+ let current = process.cwd();
63
69
  while (current !== path.parse(current).root) {
64
- if (fs.existsSync(path.join(current, "workspace.json")) || fs.existsSync(path.join(current, ".git"))) {
70
+ if (fs.existsSync(path.join(current, "project_config.json")) ||
71
+ fs.existsSync(path.join(current, "workspace.json")) ||
72
+ fs.existsSync(path.join(current, ".git"))) {
73
+ return current;
74
+ }
75
+ current = path.dirname(current);
76
+ }
77
+ // Fallback to __dirname search if discovery from cwd fails
78
+ current = __dirname;
79
+ while (current !== path.parse(current).root) {
80
+ if (fs.existsSync(path.join(current, "project_config.json")) ||
81
+ fs.existsSync(path.join(current, "workspace.json")) ||
82
+ fs.existsSync(path.join(current, ".git"))) {
65
83
  return current;
66
84
  }
67
85
  current = path.dirname(current);
@@ -72,22 +90,30 @@ function findRepoRoot() {
72
90
  export const config = {
73
91
  get githubToken() { return getGithubToken(); },
74
92
  get githubOwner() {
93
+ const repoString = cachedDynamicConfig?.github_repo;
94
+ if (typeof repoString === "string" && repoString.includes("/")) {
95
+ return repoString.split("/")[0];
96
+ }
75
97
  if (process.env.GITHUB_OWNER)
76
98
  return process.env.GITHUB_OWNER;
77
- const repoString = cachedDynamicConfig?.github_repo;
78
- if (typeof repoString !== "string" || !repoString.includes("/")) {
79
- throw new Error("GITHUB_OWNER must be set via environment variable or project_config.json");
99
+ const envRepo = process.env.GITHUB_REPOSITORY || process.env.GH_REPO;
100
+ if (envRepo && envRepo.includes("/")) {
101
+ return envRepo.split("/")[0];
80
102
  }
81
- return repoString.split("/")[0];
103
+ throw new Error("GITHUB_OWNER must be set via environment variable or project_config.json");
82
104
  },
83
105
  get githubRepo() {
106
+ const repoString = cachedDynamicConfig?.github_repo;
107
+ if (typeof repoString === "string" && repoString.includes("/")) {
108
+ return repoString.split("/")[1];
109
+ }
84
110
  if (process.env.GITHUB_REPO)
85
111
  return process.env.GITHUB_REPO;
86
- const repoString = cachedDynamicConfig?.github_repo;
87
- if (typeof repoString !== "string" || !repoString.includes("/")) {
88
- throw new Error("GITHUB_REPO must be set via environment variable or project_config.json");
112
+ const envRepo = process.env.GITHUB_REPOSITORY || process.env.GH_REPO;
113
+ if (envRepo && envRepo.includes("/")) {
114
+ return envRepo.split("/")[1];
89
115
  }
90
- return repoString.split("/")[1];
116
+ throw new Error("GITHUB_REPO must be set via environment variable or project_config.json");
91
117
  },
92
118
  get repoPath() {
93
119
  return findRepoRoot();
@@ -15,12 +15,14 @@ describe("config", () => {
15
15
  process.env = originalEnv;
16
16
  });
17
17
  describe("initializeConfig", () => {
18
- it("should load dynamic config from td-cli config view", async () => {
18
+ it("should load dynamic config from td-cli config view using active cwd", async () => {
19
19
  const mockConfig = { github_repo: "owner/repo", base_branch: "origin/main", vite_base_path: "/app/" };
20
20
  vi.mocked(execSync).mockReturnValue(JSON.stringify(mockConfig));
21
21
  const { initializeConfig } = await import("./config.js");
22
22
  const result = initializeConfig();
23
- expect(execSync).toHaveBeenCalledWith("td-cli config view", expect.anything());
23
+ expect(execSync).toHaveBeenCalledWith("td-cli config view", expect.objectContaining({
24
+ cwd: process.cwd()
25
+ }));
24
26
  expect(result).toEqual(mockConfig);
25
27
  });
26
28
  it("should return cached dynamic config on subsequent calls", async () => {
@@ -67,7 +69,18 @@ describe("config", () => {
67
69
  });
68
70
  expect(config.githubToken).toBeUndefined();
69
71
  });
70
- it("should resolve githubOwner and githubRepo from env or cached config", async () => {
72
+ it("should prioritize cachedDynamicConfig over environment variables for githubOwner and githubRepo", async () => {
73
+ process.env.GITHUB_OWNER = "env-owner";
74
+ process.env.GITHUB_REPO = "env-repo";
75
+ process.env.GITHUB_REPOSITORY = "env-owner/env-repo";
76
+ const mockConfig = { github_repo: "workspace-owner/workspace-repo" };
77
+ vi.mocked(execSync).mockReturnValue(JSON.stringify(mockConfig));
78
+ const { config, initializeConfig } = await import("./config.js");
79
+ initializeConfig();
80
+ expect(config.githubOwner).toBe("workspace-owner");
81
+ expect(config.githubRepo).toBe("workspace-repo");
82
+ });
83
+ it("should fallback to environment variables when cachedDynamicConfig is empty", async () => {
71
84
  process.env.GITHUB_OWNER = "custom-owner";
72
85
  process.env.GITHUB_REPO = "custom-repo";
73
86
  const { config } = await import("./config.js");
@@ -77,6 +90,8 @@ describe("config", () => {
77
90
  it("should throw when githubOwner / githubRepo cannot be determined", async () => {
78
91
  delete process.env.GITHUB_OWNER;
79
92
  delete process.env.GITHUB_REPO;
93
+ delete process.env.GITHUB_REPOSITORY;
94
+ delete process.env.GH_REPO;
80
95
  vi.mocked(execSync).mockImplementation(() => {
81
96
  throw new Error("td-cli failed");
82
97
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arii/boomtick-mcp",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Boomtick MCP Guide: PR Rescue + Merge Conflict Agent",
5
5
  "repository": {
6
6
  "type": "git",
@@ -20,14 +20,16 @@ describe("config", () => {
20
20
  });
21
21
 
22
22
  describe("initializeConfig", () => {
23
- it("should load dynamic config from td-cli config view", async () => {
23
+ it("should load dynamic config from td-cli config view using active cwd", async () => {
24
24
  const mockConfig = { github_repo: "owner/repo", base_branch: "origin/main", vite_base_path: "/app/" };
25
25
  vi.mocked(execSync).mockReturnValue(JSON.stringify(mockConfig));
26
26
 
27
27
  const { initializeConfig } = await import("./config.js");
28
28
  const result = initializeConfig();
29
29
 
30
- expect(execSync).toHaveBeenCalledWith("td-cli config view", expect.anything());
30
+ expect(execSync).toHaveBeenCalledWith("td-cli config view", expect.objectContaining({
31
+ cwd: process.cwd()
32
+ }));
31
33
  expect(result).toEqual(mockConfig);
32
34
  });
33
35
 
@@ -87,7 +89,22 @@ describe("config", () => {
87
89
  expect(config.githubToken).toBeUndefined();
88
90
  });
89
91
 
90
- it("should resolve githubOwner and githubRepo from env or cached config", async () => {
92
+ it("should prioritize cachedDynamicConfig over environment variables for githubOwner and githubRepo", async () => {
93
+ process.env.GITHUB_OWNER = "env-owner";
94
+ process.env.GITHUB_REPO = "env-repo";
95
+ process.env.GITHUB_REPOSITORY = "env-owner/env-repo";
96
+
97
+ const mockConfig = { github_repo: "workspace-owner/workspace-repo" };
98
+ vi.mocked(execSync).mockReturnValue(JSON.stringify(mockConfig));
99
+
100
+ const { config, initializeConfig } = await import("./config.js");
101
+ initializeConfig();
102
+
103
+ expect(config.githubOwner).toBe("workspace-owner");
104
+ expect(config.githubRepo).toBe("workspace-repo");
105
+ });
106
+
107
+ it("should fallback to environment variables when cachedDynamicConfig is empty", async () => {
91
108
  process.env.GITHUB_OWNER = "custom-owner";
92
109
  process.env.GITHUB_REPO = "custom-repo";
93
110
  const { config } = await import("./config.js");
@@ -99,6 +116,8 @@ describe("config", () => {
99
116
  it("should throw when githubOwner / githubRepo cannot be determined", async () => {
100
117
  delete process.env.GITHUB_OWNER;
101
118
  delete process.env.GITHUB_REPO;
119
+ delete process.env.GITHUB_REPOSITORY;
120
+ delete process.env.GH_REPO;
102
121
  vi.mocked(execSync).mockImplementation(() => {
103
122
  throw new Error("td-cli failed");
104
123
  });
package/src/config.ts CHANGED
@@ -1,4 +1,3 @@
1
- import dotenv from "dotenv";
2
1
  import path from "path";
3
2
  import fs from "fs";
4
3
  import { fileURLToPath } from "url";
@@ -6,10 +5,14 @@ import { execSync } from "child_process";
6
5
 
7
6
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
7
 
9
- dotenv.config({
10
- path: path.resolve(__dirname, "../.env"),
11
- quiet: true
12
- });
8
+ const envPath = path.resolve(__dirname, "../.env");
9
+ if (fs.existsSync(envPath) && typeof process.loadEnvFile === "function") {
10
+ try {
11
+ process.loadEnvFile(envPath);
12
+ } catch {
13
+ // Ignore if already loaded or invalid
14
+ }
15
+ }
13
16
 
14
17
  function getGithubToken(): string | undefined {
15
18
  if (process.env.GITHUB_TOKEN) {
@@ -36,9 +39,11 @@ export function initializeConfig() {
36
39
  }
37
40
 
38
41
  try {
39
- // Attempt to load core properties from the Python CLI to avoid duplication
42
+ // Attempt to load core properties from the Python CLI in the active workspace
43
+ const cwd = process.env.BOOMTICK_REPO_PATH || process.cwd();
40
44
  const cmd = `td-cli config view`;
41
45
  const output = execSync(cmd, {
46
+ cwd,
42
47
  encoding: "utf-8",
43
48
  stdio: ["ignore", "pipe", "ignore"]
44
49
  });
@@ -64,10 +69,27 @@ function findRepoRoot() {
64
69
  return process.env.BOOMTICK_REPO_PATH;
65
70
  }
66
71
 
67
- // Traverse up to find workspace.json or .git
68
- let current = __dirname;
72
+ // Traverse up from process.cwd() to find project_config.json, workspace.json, or .git
73
+ let current = process.cwd();
69
74
  while (current !== path.parse(current).root) {
70
- if (fs.existsSync(path.join(current, "workspace.json")) || fs.existsSync(path.join(current, ".git"))) {
75
+ if (
76
+ fs.existsSync(path.join(current, "project_config.json")) ||
77
+ fs.existsSync(path.join(current, "workspace.json")) ||
78
+ fs.existsSync(path.join(current, ".git"))
79
+ ) {
80
+ return current;
81
+ }
82
+ current = path.dirname(current);
83
+ }
84
+
85
+ // Fallback to __dirname search if discovery from cwd fails
86
+ current = __dirname;
87
+ while (current !== path.parse(current).root) {
88
+ if (
89
+ fs.existsSync(path.join(current, "project_config.json")) ||
90
+ fs.existsSync(path.join(current, "workspace.json")) ||
91
+ fs.existsSync(path.join(current, ".git"))
92
+ ) {
71
93
  return current;
72
94
  }
73
95
  current = path.dirname(current);
@@ -79,20 +101,28 @@ function findRepoRoot() {
79
101
  export const config = {
80
102
  get githubToken() { return getGithubToken(); },
81
103
  get githubOwner() {
82
- if (process.env.GITHUB_OWNER) return process.env.GITHUB_OWNER;
83
104
  const repoString = cachedDynamicConfig?.github_repo;
84
- if (typeof repoString !== "string" || !repoString.includes("/")) {
85
- throw new Error("GITHUB_OWNER must be set via environment variable or project_config.json");
105
+ if (typeof repoString === "string" && repoString.includes("/")) {
106
+ return repoString.split("/")[0];
107
+ }
108
+ if (process.env.GITHUB_OWNER) return process.env.GITHUB_OWNER;
109
+ const envRepo = process.env.GITHUB_REPOSITORY || process.env.GH_REPO;
110
+ if (envRepo && envRepo.includes("/")) {
111
+ return envRepo.split("/")[0];
86
112
  }
87
- return repoString.split("/")[0];
113
+ throw new Error("GITHUB_OWNER must be set via environment variable or project_config.json");
88
114
  },
89
115
  get githubRepo() {
90
- if (process.env.GITHUB_REPO) return process.env.GITHUB_REPO;
91
116
  const repoString = cachedDynamicConfig?.github_repo;
92
- if (typeof repoString !== "string" || !repoString.includes("/")) {
93
- throw new Error("GITHUB_REPO must be set via environment variable or project_config.json");
117
+ if (typeof repoString === "string" && repoString.includes("/")) {
118
+ return repoString.split("/")[1];
119
+ }
120
+ if (process.env.GITHUB_REPO) return process.env.GITHUB_REPO;
121
+ const envRepo = process.env.GITHUB_REPOSITORY || process.env.GH_REPO;
122
+ if (envRepo && envRepo.includes("/")) {
123
+ return envRepo.split("/")[1];
94
124
  }
95
- return repoString.split("/")[1];
125
+ throw new Error("GITHUB_REPO must be set via environment variable or project_config.json");
96
126
  },
97
127
  get repoPath() {
98
128
  return findRepoRoot();