@arii/boomtick-mcp 0.3.3

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 (187) hide show
  1. package/.env.example +6 -0
  2. package/.mcp/schemas/agent.search_ddgs.json +1 -0
  3. package/.mcp/schemas/boomtick.health.json +1 -0
  4. package/.mcp/schemas/github.checkout_branch.json +1 -0
  5. package/.mcp/schemas/github.comment_triage_summary.json +1 -0
  6. package/.mcp/schemas/github.create_issue.json +1 -0
  7. package/.mcp/schemas/github.create_pull_request.json +1 -0
  8. package/.mcp/schemas/github.get_merge_conflict_files.json +1 -0
  9. package/.mcp/schemas/github.get_pr.json +1 -0
  10. package/.mcp/schemas/github.get_pr_diff.json +1 -0
  11. package/.mcp/schemas/github.issue_comment.json +1 -0
  12. package/.mcp/schemas/github.issue_update.json +1 -0
  13. package/.mcp/schemas/github.issue_view.json +1 -0
  14. package/.mcp/schemas/github.open_replacement_pr.json +1 -0
  15. package/.mcp/schemas/github.search_open_prs.json +1 -0
  16. package/.mcp/schemas/jules.cancel_session.json +1 -0
  17. package/.mcp/schemas/jules.create_session.json +1 -0
  18. package/.mcp/schemas/jules.get_messages.json +1 -0
  19. package/.mcp/schemas/jules.get_pr.json +1 -0
  20. package/.mcp/schemas/jules.get_session.json +1 -0
  21. package/.mcp/schemas/jules.list_sessions.json +1 -0
  22. package/.mcp/schemas/jules.send_message.json +1 -0
  23. package/.mcp/schemas/jules.trigger_feedback.json +1 -0
  24. package/.mcp/schemas/repo.commit_patch.json +1 -0
  25. package/.mcp/schemas/repo.create_branch.json +1 -0
  26. package/.mcp/schemas/repo.create_repair_branch.json +1 -0
  27. package/.mcp/schemas/repo.get_changed_files.json +1 -0
  28. package/.mcp/schemas/repo.get_command_schema.json +1 -0
  29. package/.mcp/schemas/repo.get_package_scripts.json +1 -0
  30. package/.mcp/schemas/repo.get_route_map.json +1 -0
  31. package/.mcp/schemas/repo.logs.json +1 -0
  32. package/.mcp/schemas/repo.read_agent_context.json +1 -0
  33. package/.mcp/schemas/repo.read_ci_logs.json +1 -0
  34. package/.mcp/schemas/repo.run_lighthouse.json +1 -0
  35. package/.mcp/schemas/repo.run_playwright.json +1 -0
  36. package/.mcp/schemas/repo.run_tests.json +1 -0
  37. package/CHANGELOG.md +50 -0
  38. package/README.md +108 -0
  39. package/actions/ai-review/action.yml +15 -0
  40. package/actions/audit/action.yml +37 -0
  41. package/actions/ci-validate/action.yml +9 -0
  42. package/actions/impact-analysis/action.yml +385 -0
  43. package/actions/lint-typecheck/action.yml +19 -0
  44. package/actions/run-project-gate/action.yml +163 -0
  45. package/actions/setup/action.yml +15 -0
  46. package/actions/setup-workspace/action.yml +179 -0
  47. package/actions/test-build/action.yml +71 -0
  48. package/actions/update-pr-comment/action.yml +101 -0
  49. package/dist/config.js +110 -0
  50. package/dist/evals/run-evals.js +38 -0
  51. package/dist/index.js +10 -0
  52. package/dist/lib/error_utils.js +8 -0
  53. package/dist/lib/git.js +33 -0
  54. package/dist/lib/result.js +21 -0
  55. package/dist/lib/shell.js +91 -0
  56. package/dist/lib/shell.test.js +10 -0
  57. package/dist/lib/td-cli.js +27 -0
  58. package/dist/lib/test-utils.js +20 -0
  59. package/dist/mcp/definitions.js +242 -0
  60. package/dist/mcp/server.js +317 -0
  61. package/dist/mcp/tools.js +18 -0
  62. package/dist/tools/contract.js +2069 -0
  63. package/dist/tools/ddgs.search.js +50 -0
  64. package/dist/tools/ddgs.search.test.js +67 -0
  65. package/dist/tools/ddgs_search.py +23 -0
  66. package/dist/tools/github.checkout_branch.js +20 -0
  67. package/dist/tools/github.comment_triage_summary.js +25 -0
  68. package/dist/tools/github.create_issue.js +28 -0
  69. package/dist/tools/github.create_issue.test.js +54 -0
  70. package/dist/tools/github.create_pull_request.js +44 -0
  71. package/dist/tools/github.get_merge_conflict_files.js +26 -0
  72. package/dist/tools/github.get_pr.js +18 -0
  73. package/dist/tools/github.get_pr_diff.js +24 -0
  74. package/dist/tools/github.issue_comment.js +24 -0
  75. package/dist/tools/github.issue_update.js +29 -0
  76. package/dist/tools/github.issue_view.js +28 -0
  77. package/dist/tools/github.open_replacement_pr.js +41 -0
  78. package/dist/tools/github.search_open_prs.js +31 -0
  79. package/dist/tools/github.search_open_prs.test.js +51 -0
  80. package/dist/tools/jules/cancel-session.js +20 -0
  81. package/dist/tools/jules/create-session.js +20 -0
  82. package/dist/tools/jules/create-session.test.js +99 -0
  83. package/dist/tools/jules/get-messages.js +19 -0
  84. package/dist/tools/jules/get-messages.test.js +45 -0
  85. package/dist/tools/jules/get-pr.js +14 -0
  86. package/dist/tools/jules/get-pr.test.js +37 -0
  87. package/dist/tools/jules/get-session.js +10 -0
  88. package/dist/tools/jules/list-sessions.js +43 -0
  89. package/dist/tools/jules/send-message.js +22 -0
  90. package/dist/tools/jules/send-message.test.js +56 -0
  91. package/dist/tools/jules/shared.js +26 -0
  92. package/dist/tools/jules/trigger-feedback.js +16 -0
  93. package/dist/tools/jules/trigger-feedback.test.js +42 -0
  94. package/dist/tools/repo.commit_patch.js +33 -0
  95. package/dist/tools/repo.create_branch.js +21 -0
  96. package/dist/tools/repo.create_branch.test.js +42 -0
  97. package/dist/tools/repo.create_repair_branch.js +38 -0
  98. package/dist/tools/repo.get_changed_files.js +21 -0
  99. package/dist/tools/repo.get_command_schema.js +27 -0
  100. package/dist/tools/repo.get_package_scripts.js +35 -0
  101. package/dist/tools/repo.get_package_scripts.test.js +39 -0
  102. package/dist/tools/repo.get_route_map.js +37 -0
  103. package/dist/tools/repo.logs.js +16 -0
  104. package/dist/tools/repo.read_agent_context.js +20 -0
  105. package/dist/tools/repo.read_ci_logs.js +18 -0
  106. package/dist/tools/repo.run_lighthouse.js +37 -0
  107. package/dist/tools/repo.run_playwright.js +29 -0
  108. package/dist/tools/repo.run_tests.js +43 -0
  109. package/dist/tools/types.js +1 -0
  110. package/docs/testing.md +73 -0
  111. package/package.json +45 -0
  112. package/scripts/create_instructions.sh +42 -0
  113. package/scripts/export-mcp-schema.ts +7 -0
  114. package/scripts/sync-contracts.ts +74 -0
  115. package/scripts/sync-mcp-schemas.ts +97 -0
  116. package/src/agents/conflict-scout.prompt.md +20 -0
  117. package/src/agents/pr-consolidation.prompt.md +32 -0
  118. package/src/agents/pr-writer.prompt.md +13 -0
  119. package/src/agents/repair-agent.prompt.md +18 -0
  120. package/src/agents/repo-context.prompt.md +20 -0
  121. package/src/agents/verifier-agent.prompt.md +22 -0
  122. package/src/config.ts +113 -0
  123. package/src/evals/fixtures/merge-conflict-simple/README.md +0 -0
  124. package/src/evals/run-evals.ts +40 -0
  125. package/src/index.ts +12 -0
  126. package/src/lib/error_utils.ts +8 -0
  127. package/src/lib/git.ts +41 -0
  128. package/src/lib/result.ts +28 -0
  129. package/src/lib/shell.test.ts +11 -0
  130. package/src/lib/shell.ts +118 -0
  131. package/src/lib/td-cli.ts +38 -0
  132. package/src/lib/test-utils.ts +23 -0
  133. package/src/mcp/definitions.ts +278 -0
  134. package/src/mcp/server.ts +359 -0
  135. package/src/mcp/tools.ts +20 -0
  136. package/src/tools/contract.ts +2245 -0
  137. package/src/tools/ddgs.search.test.ts +92 -0
  138. package/src/tools/ddgs.search.ts +58 -0
  139. package/src/tools/ddgs_search.py +23 -0
  140. package/src/tools/github.checkout_branch.ts +25 -0
  141. package/src/tools/github.comment_triage_summary.ts +30 -0
  142. package/src/tools/github.create_issue.test.ts +63 -0
  143. package/src/tools/github.create_issue.ts +34 -0
  144. package/src/tools/github.create_pull_request.ts +50 -0
  145. package/src/tools/github.get_merge_conflict_files.ts +31 -0
  146. package/src/tools/github.get_pr.ts +24 -0
  147. package/src/tools/github.get_pr_diff.ts +29 -0
  148. package/src/tools/github.issue_comment.ts +31 -0
  149. package/src/tools/github.issue_update.ts +31 -0
  150. package/src/tools/github.issue_view.ts +35 -0
  151. package/src/tools/github.open_replacement_pr.ts +48 -0
  152. package/src/tools/github.search_open_prs.test.ts +60 -0
  153. package/src/tools/github.search_open_prs.ts +40 -0
  154. package/src/tools/jules/cancel-session.ts +26 -0
  155. package/src/tools/jules/create-session.test.ts +114 -0
  156. package/src/tools/jules/create-session.ts +24 -0
  157. package/src/tools/jules/get-messages.test.ts +52 -0
  158. package/src/tools/jules/get-messages.ts +24 -0
  159. package/src/tools/jules/get-pr.test.ts +43 -0
  160. package/src/tools/jules/get-pr.ts +17 -0
  161. package/src/tools/jules/get-session.ts +12 -0
  162. package/src/tools/jules/list-sessions.ts +50 -0
  163. package/src/tools/jules/send-message.test.ts +66 -0
  164. package/src/tools/jules/send-message.ts +27 -0
  165. package/src/tools/jules/shared.ts +28 -0
  166. package/src/tools/jules/trigger-feedback.test.ts +50 -0
  167. package/src/tools/jules/trigger-feedback.ts +21 -0
  168. package/src/tools/repo.commit_patch.ts +41 -0
  169. package/src/tools/repo.create_branch.test.ts +55 -0
  170. package/src/tools/repo.create_branch.ts +29 -0
  171. package/src/tools/repo.create_repair_branch.ts +47 -0
  172. package/src/tools/repo.get_changed_files.ts +26 -0
  173. package/src/tools/repo.get_command_schema.ts +30 -0
  174. package/src/tools/repo.get_package_scripts.test.ts +51 -0
  175. package/src/tools/repo.get_package_scripts.ts +36 -0
  176. package/src/tools/repo.get_route_map.ts +41 -0
  177. package/src/tools/repo.logs.ts +21 -0
  178. package/src/tools/repo.read_agent_context.ts +22 -0
  179. package/src/tools/repo.read_ci_logs.ts +21 -0
  180. package/src/tools/repo.run_lighthouse.ts +40 -0
  181. package/src/tools/repo.run_playwright.ts +35 -0
  182. package/src/tools/repo.run_tests.ts +47 -0
  183. package/src/tools/types.ts +8 -0
  184. package/start_browsermcp.sh +14 -0
  185. package/start_github_mcp.sh +6 -0
  186. package/tsconfig.json +15 -0
  187. package/vitest.config.ts +9 -0
@@ -0,0 +1,179 @@
1
+ name: 'Setup Workspace'
2
+ description: 'Bootstraps the workspace by installing Node.js, Python, and dev-tools'
3
+ inputs:
4
+ setup-node:
5
+ description: "Install Node.js and pnpm"
6
+ required: false
7
+ default: 'true'
8
+ setup-python:
9
+ description: "Install dev-tools (Python)"
10
+ required: false
11
+ default: 'true'
12
+ node-version-file:
13
+ description: "Path to .node-version file"
14
+ required: false
15
+ default: ".node-version"
16
+ pnpm-version:
17
+ description: "Version of pnpm to activate"
18
+ required: false
19
+ default: "10.28.2"
20
+
21
+ runs:
22
+ using: 'composite'
23
+ steps:
24
+ - name: Setup pnpm
25
+ if: inputs.setup-node == 'true'
26
+ uses: pnpm/action-setup@v6 # nosemgrep: yaml.github-actions.security.github-actions-mutable-action-tag.github-actions-mutable-action-tag
27
+ with:
28
+ version: ${{ inputs.pnpm-version }}
29
+
30
+ - name: Get pnpm store directory
31
+ if: inputs.setup-node == 'true'
32
+ id: pnpm-cache
33
+ shell: bash
34
+ run: |
35
+ echo "STORE_PATH=$(pnpm store path)" >> "$GITHUB_OUTPUT"
36
+
37
+ - name: Cache pnpm store
38
+ if: inputs.setup-node == 'true'
39
+ uses: actions/cache@v4.2.0 # nosemgrep: yaml.github-actions.security.github-actions-mutable-action-tag.github-actions-mutable-action-tag
40
+ with:
41
+ path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
42
+ key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
43
+ restore-keys: |
44
+ ${{ runner.os }}-pnpm-store-
45
+
46
+ - name: Setup Node.js
47
+ if: inputs.setup-node == 'true'
48
+ uses: actions/setup-node@v7 # nosemgrep: yaml.github-actions.security.github-actions-mutable-action-tag.github-actions-mutable-action-tag
49
+ with:
50
+ node-version-file: ${{ inputs.node-version-file }}
51
+
52
+ - name: Enable Corepack
53
+ if: inputs.setup-node == 'true'
54
+ run: corepack enable
55
+ shell: bash
56
+
57
+ - name: Activate pinned pnpm
58
+ if: inputs.setup-node == 'true'
59
+ env:
60
+ PNPM_VERSION: ${{ inputs.pnpm-version }}
61
+ run: corepack prepare pnpm@"$PNPM_VERSION" --activate
62
+ shell: bash
63
+
64
+ - name: Resolve Paths
65
+ id: resolve-paths
66
+ shell: bash
67
+ run: |
68
+ # Use centralized script for CLI path resolution
69
+ if [ -f "scripts/resolve-cli.sh" ]; then
70
+ CLI_DIR=$(bash scripts/resolve-cli.sh | sed "s|$(pwd)/||")
71
+ elif [ -f "boomtick-pkg/scripts/resolve-cli.sh" ]; then
72
+ CLI_DIR=$(bash boomtick-pkg/scripts/resolve-cli.sh | sed "s|$(pwd)/||")
73
+ elif [ -f "boomtick/scripts/resolve-cli.sh" ]; then
74
+ CLI_DIR=$(bash boomtick/scripts/resolve-cli.sh | sed "s|$(pwd)/||")
75
+ elif [ -f "${{ github.action_path }}/../../../scripts/resolve-cli.sh" ]; then
76
+ CLI_DIR=$(REPO_ROOT="${{ github.action_path }}/../../../" bash "${{ github.action_path }}/../../../scripts/resolve-cli.sh" | sed "s|$(pwd)/||")
77
+ elif [ -d "cli" ]; then
78
+ CLI_DIR="cli"
79
+ elif [ -d "boomtick-pkg/cli" ]; then
80
+ CLI_DIR="boomtick-pkg/cli"
81
+ elif [ -d "boomtick/cli" ]; then
82
+ CLI_DIR="boomtick/cli"
83
+ else
84
+ CLI_DIR="cli"
85
+ fi
86
+
87
+ # Validate that the resolved CLI directory exists
88
+ if [ -z "$CLI_DIR" ] || { [ ! -d "$CLI_DIR" ] && [ ! -d "${{ github.action_path }}/../../../$CLI_DIR" ]; }; then
89
+ echo "::error::Could not resolve a valid CLI directory. Resolved: '$CLI_DIR'"
90
+ exit 1
91
+ fi
92
+
93
+ echo "CLI_DIR=$CLI_DIR" >> "$GITHUB_OUTPUT"
94
+
95
+ # Consolidate GITHUB_PATH updates
96
+ echo "$HOME/.local/bin" >> "$GITHUB_PATH"
97
+ echo "/github/home/.local/bin" >> "$GITHUB_PATH"
98
+
99
+ - name: Get pip cache directory
100
+ if: inputs.setup-python == 'true'
101
+ id: pip-cache
102
+ shell: bash
103
+ run: |
104
+ echo "DIR=$(pip cache dir)" >> "$GITHUB_OUTPUT"
105
+
106
+ - name: Cache pip
107
+ if: inputs.setup-python == 'true'
108
+ uses: actions/cache@v4.2.0 # nosemgrep: yaml.github-actions.security.github-actions-mutable-action-tag.github-actions-mutable-action-tag
109
+ with:
110
+ path: ${{ steps.pip-cache.outputs.DIR }}
111
+ key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml') }}
112
+ restore-keys: |
113
+ ${{ runner.os }}-pip-
114
+
115
+ - name: Install dev-tools
116
+ if: inputs.setup-python == 'true'
117
+ env:
118
+ CLI_DIR: ${{ steps.resolve-paths.outputs.CLI_DIR }}
119
+ run: |
120
+ echo "::group::Install dev-tools"
121
+ if [ -f "requirements-dev.txt" ]; then
122
+ pip install -r requirements-dev.txt --break-system-packages
123
+ fi
124
+ pip install -e "$CLI_DIR" --break-system-packages
125
+
126
+ echo "::endgroup::"
127
+ shell: bash
128
+
129
+ - name: Export Environment Variables
130
+ if: inputs.setup-python == 'true'
131
+ id: export-env
132
+ env:
133
+ CLI_DIR: ${{ steps.resolve-paths.outputs.CLI_DIR }}
134
+ run: |
135
+ # Use $(pwd) to get the container-internal path, avoiding host-path leakage from $GITHUB_WORKSPACE
136
+ REPO_DIR=$(pwd)
137
+ if [[ "$CLI_DIR" = /* ]]; then
138
+ CLI_DIR_ABS="$CLI_DIR"
139
+ else
140
+ CLI_DIR_ABS="$REPO_DIR/$CLI_DIR"
141
+ fi
142
+
143
+ # 1. Update PYTHONPATH for subsequent steps
144
+ # Ensure it works even if PYTHONPATH is initially empty
145
+ if [ -z "$PYTHONPATH" ]; then
146
+ echo "PYTHONPATH=$CLI_DIR_ABS" >> "$GITHUB_ENV"
147
+ else
148
+ echo "PYTHONPATH=$CLI_DIR_ABS:$PYTHONPATH" >> "$GITHUB_ENV"
149
+ fi
150
+ echo "cli_dir_abs=$CLI_DIR_ABS" >> "$GITHUB_OUTPUT"
151
+
152
+ echo "✅ Environment configured: PYTHONPATH includes $CLI_DIR_ABS"
153
+ shell: bash
154
+
155
+ - name: Initialize Schemas and Contracts
156
+ if: inputs.setup-node == 'true' && inputs.setup-python == 'true'
157
+ env:
158
+ CLI_DIR_ABS: ${{ steps.export-env.outputs.cli_dir_abs }}
159
+ run: |
160
+ echo "::group::Initialize Schemas and Contracts"
161
+ export PYTHONPATH="$CLI_DIR_ABS:$PYTHONPATH"
162
+ python3 "$CLI_DIR_ABS/dev_tools/schema_gen.py"
163
+
164
+ MCP_DIR="mcp"
165
+
166
+ # Check for sync-contracts script in the resolved MCP directory
167
+ if [ -f "$MCP_DIR/package.json" ] && grep -q "sync-contracts" "$MCP_DIR/package.json"; then
168
+ echo "::group::Run sync-contracts in $MCP_DIR"
169
+ # Ensure dependencies are installed if we're in the MCP directory
170
+ if [ ! -d "$MCP_DIR/node_modules" ]; then
171
+ (cd "$MCP_DIR" && pnpm install --frozen-lockfile --prefer-offline)
172
+ fi
173
+ pnpm --filter "@arii/boomtick-mcp" run sync-contracts || (cd "$MCP_DIR" && pnpm run sync-contracts)
174
+ echo "::endgroup::"
175
+ else
176
+ echo "⚠️ Skipping: sync-contracts script not found in $MCP_DIR/package.json"
177
+ fi
178
+ echo "::endgroup::"
179
+ shell: bash
@@ -0,0 +1,71 @@
1
+ name: BoomTick Build & Test
2
+ description: Encapsulates build, bundle size check, Playwright E2E, and Lighthouse CI
3
+ inputs:
4
+ github_token:
5
+ description: 'GitHub token'
6
+ required: true
7
+ run_lighthouse:
8
+ description: 'Whether to run Lighthouse CI'
9
+ required: false
10
+ default: 'true'
11
+ runs:
12
+ using: composite
13
+ steps:
14
+ - uses: ./mcp/actions/setup
15
+
16
+ - name: pnpm install
17
+ run: pnpm install --frozen-lockfile --prefer-offline
18
+ shell: bash
19
+
20
+ - name: Build App
21
+ run: |
22
+ VITE_BASE_PATH=/; export VITE_BASE_PATH
23
+ pnpm run build
24
+ shell: bash
25
+
26
+ - name: Bundle Size Check
27
+ env:
28
+ GITHUB_TOKEN: ${{ inputs.github_token }}
29
+ run: |
30
+ export PATH="$HOME/.local/bin:/github/home/.local/bin:$PATH"
31
+ python3 -m dev_tools.cli gh bundle-size
32
+ shell: bash
33
+
34
+ - name: Run Playwright Smoke Test
35
+ run: |
36
+ VITE_BASE_PATH=/ pnpm run test:e2e
37
+ shell: bash
38
+
39
+ - name: Run Lighthouse CI
40
+ if: inputs.run_lighthouse == 'true'
41
+ run: |
42
+ set -eu
43
+ VITE_BASE_PATH="/"
44
+ CHROME_PATH="$(node -e "console.log(require('playwright').chromium.executablePath())")"
45
+ export VITE_BASE_PATH
46
+ export CHROME_PATH
47
+ echo "CHROME_PATH=$CHROME_PATH"
48
+ pnpm run lighthouse --collect.url="http://localhost:4173${VITE_BASE_PATH}"
49
+ env:
50
+ LHCI_GITHUB_TOKEN: ${{ inputs.github_token }}
51
+ shell: bash
52
+
53
+ - name: Check Playwright Report Exists
54
+ id: check_report
55
+ if: always()
56
+ run: |
57
+ if [ -d "playwright-report/" ]; then
58
+ echo "exists=true" >> "$GITHUB_OUTPUT"
59
+ else
60
+ echo "exists=false" >> "$GITHUB_OUTPUT"
61
+ fi
62
+ shell: bash
63
+
64
+ - name: Upload Test Results
65
+ if: always() && steps.check_report.outputs.exists == 'true'
66
+ uses: actions/upload-artifact@v7 # nosemgrep: yaml.github-actions.security.github-actions-mutable-action-tag.github-actions-mutable-action-tag
67
+ with:
68
+ name: playwright-report-${{ github.run_id }}
69
+ path: playwright-report/
70
+ retention-days: 7
71
+ if-no-files-found: ignore
@@ -0,0 +1,101 @@
1
+ name: 'Update PR Comment'
2
+ description: 'Updates or creates a PR comment with the given status and body'
3
+
4
+ inputs:
5
+ github_token:
6
+ description: 'GitHub Token'
7
+ required: true
8
+ status_line:
9
+ description: 'Status line (e.g., ⏳ **Impact analysis running...**)'
10
+ required: true
11
+ impact_body:
12
+ description: 'Body text (leave empty for initial state)'
13
+ required: false
14
+ default: ''
15
+
16
+ runs:
17
+ using: 'composite'
18
+ steps:
19
+ - uses: actions/github-script@v8 # nosemgrep: yaml.github-actions.security.github-actions-mutable-action-tag.github-actions-mutable-action-tag
20
+ env:
21
+ INPUT_STATUS_LINE: ${{ inputs.status_line }}
22
+ INPUT_IMPACT_BODY: ${{ inputs.impact_body }}
23
+ with:
24
+ github-token: ${{ inputs.github_token }}
25
+ script: |
26
+ const owner = context.repo.owner;
27
+ const repo = context.repo.repo;
28
+ const branchName = context.ref.replace('refs/heads/', '');
29
+
30
+ const pstTime = new Date().toLocaleString('en-US', {
31
+ timeZone: 'America/Los_Angeles',
32
+ dateStyle: 'medium',
33
+ timeStyle: 'short'
34
+ });
35
+
36
+ if (branchName === 'main') return;
37
+
38
+ const pullRequests = await github.paginate(github.rest.pulls.list, {
39
+ owner,
40
+ repo,
41
+ state: 'open'
42
+ });
43
+ const matchingPRs = pullRequests.filter(pr => pr.head.ref === branchName);
44
+
45
+ const statusLine = process.env.INPUT_STATUS_LINE;
46
+ // Simple sanitization to satisfy XSS concerns in GitHub comments
47
+ let impactBodyText = (process.env.INPUT_IMPACT_BODY || '')
48
+ .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '[REMOVED SCRIPT]');
49
+
50
+ for (const pr of matchingPRs) {
51
+ const commentTag = `<!-- impact-analysis-comment-${branchName} -->`;
52
+ const bodyParts = [
53
+ `🚀 **Impact Analysis Details** (Last updated: ${pstTime} PST)`,
54
+ '',
55
+ statusLine,
56
+ `- **Commit SHA:** \`${process.env.GITHUB_SHA.slice(0, 7)}\``,
57
+ `- [Link to artifacts](https://github.com/${owner}/${repo}/actions/runs/${context.runId})`,
58
+ ''
59
+ ];
60
+
61
+ if (impactBodyText) {
62
+ bodyParts.push(impactBodyText, '');
63
+ }
64
+
65
+ const MAX_LENGTH = 65536;
66
+ const truncationNotice = '\n\n... (truncated due to GitHub comment size limits)\n';
67
+ // Determine length of string we are about to join
68
+ let currentContent = bodyParts.join('\n');
69
+ // Adding 1 because joining `bodyParts` with `\n` and `commentTag` will add a newline
70
+ let projectedLength = currentContent.length + commentTag.length + 1;
71
+
72
+ if (projectedLength > MAX_LENGTH) {
73
+ // subtracting 2 to account for the newlines joining bodyParts
74
+ const availableLength = MAX_LENGTH - commentTag.length - truncationNotice.length - 2;
75
+ // Truncate safely, avoiding cutting midway through a unicode character or making things weird
76
+ let safeContent = currentContent.slice(0, availableLength);
77
+ // Simple attempt to cut at last newline if possible
78
+ let lastNewline = safeContent.lastIndexOf('\n');
79
+ if (lastNewline > availableLength * 0.9) {
80
+ safeContent = safeContent.slice(0, lastNewline);
81
+ }
82
+ bodyParts = [safeContent, truncationNotice];
83
+ }
84
+
85
+ bodyParts.push(commentTag);
86
+ const body = bodyParts.join('\n');
87
+
88
+ const comments = await github.paginate(github.rest.issues.listComments, {
89
+ owner,
90
+ repo,
91
+ issue_number: pr.number,
92
+ });
93
+
94
+ const existingComment = comments.find(c => c.body.includes(commentTag));
95
+
96
+ if (existingComment) {
97
+ await github.rest.issues.updateComment({ owner, repo, comment_id: existingComment.id, body });
98
+ } else {
99
+ await github.rest.issues.createComment({ issue_number: pr.number, owner, repo, body });
100
+ }
101
+ }
package/dist/config.js ADDED
@@ -0,0 +1,110 @@
1
+ import dotenv from "dotenv";
2
+ import path from "path";
3
+ import fs from "fs";
4
+ import { fileURLToPath } from "url";
5
+ import { execSync } from "child_process";
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ dotenv.config({
8
+ path: path.resolve(__dirname, "../.env"),
9
+ quiet: true
10
+ });
11
+ function getGithubToken() {
12
+ if (process.env.GITHUB_TOKEN) {
13
+ return process.env.GITHUB_TOKEN;
14
+ }
15
+ try {
16
+ const token = execSync("gh auth token", { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
17
+ if (token) {
18
+ return token;
19
+ }
20
+ }
21
+ catch (e) {
22
+ throw e;
23
+ }
24
+ return undefined;
25
+ }
26
+ let cachedDynamicConfig = null;
27
+ /**
28
+ * Explicitly initializes dynamic configuration from the Python CLI.
29
+ * Should be called once during server startup.
30
+ */
31
+ export function initializeConfig() {
32
+ if (cachedDynamicConfig !== null) {
33
+ return cachedDynamicConfig;
34
+ }
35
+ try {
36
+ // Attempt to load core properties from the Python CLI to avoid duplication
37
+ const cmd = `td-cli config view`;
38
+ const output = execSync(cmd, {
39
+ encoding: "utf-8",
40
+ stdio: ["ignore", "pipe", "ignore"]
41
+ });
42
+ cachedDynamicConfig = JSON.parse(output);
43
+ return cachedDynamicConfig;
44
+ }
45
+ catch (e) {
46
+ const message = e instanceof Error ? e.message : String(e);
47
+ const errorPrefix = `CRITICAL: Failed to load dynamic config from Python CLI. Ensure Python 3 is installed and boomtick-pkg/cli is in your PYTHONPATH. Details: ${message}`;
48
+ if (process.env.NODE_ENV === "development" || process.env.CI === "true") {
49
+ throw new Error(errorPrefix);
50
+ }
51
+ else {
52
+ console.warn(errorPrefix);
53
+ }
54
+ // Set an empty object to prevent repeated attempts if initialization fails in non-blocking mode
55
+ cachedDynamicConfig = {};
56
+ }
57
+ return cachedDynamicConfig;
58
+ }
59
+ function findRepoRoot() {
60
+ if (process.env.BOOMTICK_REPO_PATH) {
61
+ return process.env.BOOMTICK_REPO_PATH;
62
+ }
63
+ // Traverse up to find workspace.json or .git
64
+ let current = __dirname;
65
+ while (current !== path.parse(current).root) {
66
+ if (fs.existsSync(path.join(current, "workspace.json")) || fs.existsSync(path.join(current, ".git"))) {
67
+ return current;
68
+ }
69
+ current = path.dirname(current);
70
+ }
71
+ // Fallback to the monolithic relative path if discovery fails
72
+ return path.resolve(__dirname, "../../../");
73
+ }
74
+ export const config = {
75
+ get githubToken() { return getGithubToken(); },
76
+ get githubOwner() {
77
+ if (process.env.GITHUB_OWNER)
78
+ return process.env.GITHUB_OWNER;
79
+ const repoString = cachedDynamicConfig?.github_repo;
80
+ if (typeof repoString !== "string" || !repoString.includes("/")) {
81
+ throw new Error("GITHUB_OWNER must be set via environment variable or project_config.json");
82
+ }
83
+ return repoString.split("/")[0];
84
+ },
85
+ get githubRepo() {
86
+ if (process.env.GITHUB_REPO)
87
+ return process.env.GITHUB_REPO;
88
+ const repoString = cachedDynamicConfig?.github_repo;
89
+ if (typeof repoString !== "string" || !repoString.includes("/")) {
90
+ throw new Error("GITHUB_REPO must be set via environment variable or project_config.json");
91
+ }
92
+ return repoString.split("/")[1];
93
+ },
94
+ get repoPath() {
95
+ return findRepoRoot();
96
+ },
97
+ get defaultBaseBranch() {
98
+ return process.env.DEFAULT_BASE_BRANCH || cachedDynamicConfig?.base_branch?.split("/").pop() || "main";
99
+ },
100
+ get viteBasePath() {
101
+ const path = process.env.VITE_BASE_PATH || cachedDynamicConfig?.vite_base_path;
102
+ if (!path) {
103
+ throw new Error("VITE_BASE_PATH must be set via environment variable or project_config.json");
104
+ }
105
+ return path;
106
+ },
107
+ get ghPath() {
108
+ return process.env.GH_PATH || cachedDynamicConfig?.gh_path || "gh";
109
+ }
110
+ };
@@ -0,0 +1,38 @@
1
+ import { healthHandler } from "../mcp/tools.js";
2
+ import { searchOpenPrsHandler } from "../tools/github.search_open_prs.js";
3
+ import { getPackageScriptsHandler } from "../tools/repo.get_package_scripts.js";
4
+ async function runEvals() {
5
+ console.log("Starting Boomtick MCP Evaluations...");
6
+ try {
7
+ console.log("\n1. Health Check:");
8
+ try {
9
+ const health = await healthHandler({ checkDeep: false });
10
+ console.log(JSON.stringify(health, null, 2));
11
+ }
12
+ catch (e) {
13
+ console.log("Health check failed:", e instanceof Error ? e.message : String(e));
14
+ }
15
+ console.log("\n2. Repository Scripts:");
16
+ try {
17
+ const scripts = await getPackageScriptsHandler({ filter: "" });
18
+ console.log(`Found ${Object.keys(scripts.scripts).length} scripts.`);
19
+ }
20
+ catch (e) {
21
+ console.log("Repository scripts fetch failed:", e instanceof Error ? e.message : String(e));
22
+ }
23
+ console.log("\n3. GitHub PR Search (Dry Run):");
24
+ try {
25
+ const prs = await searchOpenPrsHandler({ state: "open", includeDrafts: true, limit: 100 });
26
+ console.log(`Found ${prs.prs.length} open PRs.`);
27
+ }
28
+ catch (e) {
29
+ console.log("GitHub search failed (likely due to environment/auth):", e instanceof Error ? e.message : String(e));
30
+ }
31
+ console.log("\nEvaluations complete.");
32
+ }
33
+ catch (error) {
34
+ console.error("\nEvaluation failed:", error);
35
+ process.exit(1);
36
+ }
37
+ }
38
+ runEvals();
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import { BoomtickMCPServer } from "./mcp/server.js";
3
+ import { initializeConfig } from "./config.js";
4
+ // Initialize configuration once at startup
5
+ initializeConfig();
6
+ const server = new BoomtickMCPServer();
7
+ server.run().catch((error) => {
8
+ console.error("Fatal error running server:", error);
9
+ process.exit(1);
10
+ });
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Sanitizes stderr from CLI commands to prevent implementation detail leakage
3
+ * in error messages returned to the AI agent.
4
+ */
5
+ export function sanitizeError(stderr) {
6
+ // Take first line and truncate to 200 chars to balance detail vs security
7
+ return (stderr.split("\n")[0] || "Unknown error").slice(0, 200);
8
+ }
@@ -0,0 +1,33 @@
1
+ import { runCommand } from "./shell.js";
2
+ import path from "path";
3
+ import fs from "fs/promises";
4
+ function validatePrNumber(value) {
5
+ const prNumber = Number(value);
6
+ if (!Number.isInteger(prNumber) || prNumber <= 0) {
7
+ throw new Error("Invalid PR number");
8
+ }
9
+ return prNumber;
10
+ }
11
+ export async function createWorktree(branch, prNumber) {
12
+ const safePrNumber = validatePrNumber(prNumber);
13
+ const workspaceRoot = "/tmp/boomtick-worktrees";
14
+ // nosemgrep
15
+ const worktreePath = path.join(workspaceRoot, `boomtick-mcp-rescue-${safePrNumber}`);
16
+ // Clean up if exists
17
+ try {
18
+ await fs.rm(worktreePath, { recursive: true, force: true });
19
+ await runCommand("git", ["worktree", "prune"]);
20
+ }
21
+ catch (e) {
22
+ throw e;
23
+ }
24
+ const result = await runCommand("git", ["worktree", "add", "-b", `repair-pr-${prNumber}-${Date.now()}`, worktreePath, branch]);
25
+ if (result.exitCode !== 0) {
26
+ throw new Error(`Failed to create worktree: ${result.stderr}`);
27
+ }
28
+ return worktreePath;
29
+ }
30
+ export async function removeWorktree(worktreePath) {
31
+ await runCommand("git", ["worktree", "remove", "--force", worktreePath]);
32
+ await runCommand("git", ["worktree", "prune"]);
33
+ }
@@ -0,0 +1,21 @@
1
+ export function createSuccessResult(data) {
2
+ return {
3
+ content: [
4
+ {
5
+ type: "text",
6
+ text: typeof data === "string" ? data : JSON.stringify(data, null, 2),
7
+ },
8
+ ],
9
+ };
10
+ }
11
+ export function createErrorResult(message) {
12
+ return {
13
+ content: [
14
+ {
15
+ type: "text",
16
+ text: message,
17
+ },
18
+ ],
19
+ isError: true,
20
+ };
21
+ }
@@ -0,0 +1,91 @@
1
+ import { spawn } from "child_process";
2
+ import { config } from "../config.js";
3
+ import path from "path";
4
+ import fs from "fs";
5
+ export const ALLOWED_COMMANDS = {
6
+ git: "git",
7
+ gh: "gh",
8
+ "td-cli": "td-cli",
9
+ pnpm: "pnpm",
10
+ ls: "ls",
11
+ rm: "rm",
12
+ mkdir: "mkdir",
13
+ cp: "cp",
14
+ python3: "python3"
15
+ };
16
+ export async function runCommand(cmd, args, options = {}) {
17
+ if (!(cmd in ALLOWED_COMMANDS)) {
18
+ throw new Error(`Command not allowed: ${cmd}`);
19
+ }
20
+ const safeCmd = cmd;
21
+ const start = Date.now();
22
+ const timeout = options.timeout || 60000;
23
+ // Resolve path for 'gh' or 'td-cli' if specified
24
+ let finalCmd = ALLOWED_COMMANDS[safeCmd];
25
+ if (safeCmd === "gh") {
26
+ finalCmd = config.ghPath;
27
+ }
28
+ else if (safeCmd === "td-cli") {
29
+ // If we have a repo-relative .venv, prioritize it
30
+ const venvBin = path.join(config.repoPath, ".venv", "bin", "td-cli");
31
+ const localVenvBin = path.join(process.cwd(), ".venv", "bin", "td-cli");
32
+ const parentVenvBin = path.join(config.repoPath, "..", ".venv", "bin", "td-cli");
33
+ // In standalone mode, config.repoPath is the boomtick-pkg root.
34
+ // We check common venv locations to ensure we call the isolated binary.
35
+ if (fs.existsSync(venvBin)) {
36
+ finalCmd = venvBin;
37
+ }
38
+ else if (fs.existsSync(localVenvBin)) {
39
+ finalCmd = localVenvBin;
40
+ }
41
+ else if (fs.existsSync(parentVenvBin)) {
42
+ finalCmd = parentVenvBin;
43
+ }
44
+ }
45
+ const env = {
46
+ ...process.env,
47
+ ...options.env,
48
+ GH_TOKEN: config.githubToken,
49
+ GITHUB_TOKEN: config.githubToken
50
+ };
51
+ return new Promise((resolve, reject) => {
52
+ // nosemgrep
53
+ const child = spawn(finalCmd, args, {
54
+ cwd: options.cwd || config.repoPath,
55
+ env
56
+ });
57
+ let stdout = "";
58
+ let stderr = "";
59
+ child.stdout.on("data", (data) => {
60
+ stdout += data.toString();
61
+ });
62
+ child.stderr.on("data", (data) => {
63
+ stderr += data.toString();
64
+ });
65
+ const timer = setTimeout(() => {
66
+ child.kill();
67
+ reject(new Error(`Command timed out: ${cmd} ${args.join(" ")}`));
68
+ }, timeout);
69
+ child.on("close", (code) => {
70
+ clearTimeout(timer);
71
+ const durationMs = Date.now() - start;
72
+ // Redact token from output
73
+ if (config.githubToken) {
74
+ const redact = (str) => str.replace(new RegExp(config.githubToken, "g"), "REDACTED");
75
+ stdout = redact(stdout);
76
+ stderr = redact(stderr);
77
+ }
78
+ resolve({
79
+ stdout,
80
+ stderr,
81
+ exitCode: code,
82
+ durationMs,
83
+ command: `${cmd} ${args.join(" ")}`
84
+ });
85
+ });
86
+ child.on("error", (err) => {
87
+ clearTimeout(timer);
88
+ reject(err);
89
+ });
90
+ });
91
+ }
@@ -0,0 +1,10 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import path from "path";
3
+ // Simple test to verify environment and vitest setup
4
+ describe("Basic Environment", () => {
5
+ it("should have a valid repo path", () => {
6
+ const repoPath = process.env.BOOMTICK_REPO_PATH || "/app";
7
+ expect(repoPath).toBeDefined();
8
+ expect(path.isAbsolute(repoPath)).toBe(true);
9
+ });
10
+ });