@selfagency/beans-mcp 0.1.0 → 0.1.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 (47) hide show
  1. package/.beans.yml +6 -0
  2. package/.claude/settings.local.json +18 -0
  3. package/.editorconfig +13 -0
  4. package/.github/workflows/release.yml +235 -0
  5. package/.github/workflows/test.yml +80 -0
  6. package/.husky/pre-commit +1 -0
  7. package/.nvmrc +1 -0
  8. package/.oxfmtrc.json +11 -0
  9. package/.oxlintrc.json +37 -0
  10. package/.vscode/settings.json +3 -0
  11. package/CHANGELOG.md +140 -0
  12. package/CONTRIBUTING.md +139 -0
  13. package/LICENSE.txt +21 -0
  14. package/README.md +4 -4
  15. package/dist/README.md +307 -0
  16. package/dist/beans-mcp-server.cjs.map +1 -0
  17. package/dist/index.cjs.map +1 -0
  18. package/dist/index.js +31676 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/package.json +43 -0
  21. package/package.json +64 -27
  22. package/pnpm-workspace.yaml +2 -0
  23. package/scripts/release.js +433 -0
  24. package/scripts/write-dist-package.js +53 -0
  25. package/src/cli.ts +14 -0
  26. package/src/index.ts +21 -0
  27. package/src/internal/graphql.ts +33 -0
  28. package/src/internal/queryHelpers.ts +157 -0
  29. package/src/server/BeansMcpServer.ts +600 -0
  30. package/src/server/backend.ts +358 -0
  31. package/src/test/BeansMcpServer.test.ts +514 -0
  32. package/src/test/handlers.unit.test.ts +184 -0
  33. package/src/test/parseCliArgs.test.ts +69 -0
  34. package/src/test/protocol.e2e.test.ts +884 -0
  35. package/src/test/queryHelpers.test.ts +524 -0
  36. package/src/test/startBeansMcpServer.test.ts +146 -0
  37. package/src/test/tools-integration.test.ts +912 -0
  38. package/src/test/utils.test.ts +80 -0
  39. package/src/types.ts +46 -0
  40. package/src/utils.ts +20 -0
  41. package/tsconfig.json +24 -0
  42. package/tsup.config.ts +42 -0
  43. package/vitest.config.ts +18 -0
  44. package/index.js +0 -15350
  45. /package/{beans-mcp-server.cjs → dist/beans-mcp-server.cjs} +0 -0
  46. /package/{index.cjs → dist/index.cjs} +0 -0
  47. /package/{index.d.ts → dist/index.d.ts} +0 -0
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@selfagency/beans-mcp",
3
+ "version": "0.1.1",
4
+ "description": "MCP (Model Context Protocol) server for Beans issue tracker",
5
+ "keywords": [
6
+ "beans",
7
+ "mcp",
8
+ "model-context-protocol",
9
+ "issue-tracker",
10
+ "ai"
11
+ ],
12
+ "homepage": "https://github.com/hmans/beans",
13
+ "bugs": {
14
+ "url": "https://github.com/selfagency/beans-mcp/issues"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/selfagency/beans-mcp.git"
19
+ },
20
+ "license": "MIT",
21
+ "author": {
22
+ "name": "Daniel Sieradski",
23
+ "email": "daniel@self.agency",
24
+ "url": "https://self.agency"
25
+ },
26
+ "main": "./index.cjs",
27
+ "module": "./index.js",
28
+ "types": "./index.d.ts",
29
+ "files": [
30
+ "./index.cjs",
31
+ "./index.js",
32
+ "./index.d.ts"
33
+ ],
34
+ "bin": {
35
+ "beans-mcp": "./beans-mcp-server.cjs"
36
+ },
37
+ "exports": {
38
+ ".": {
39
+ "import": "./index.js",
40
+ "require": "./index.cjs"
41
+ }
42
+ }
43
+ }
package/package.json CHANGED
@@ -1,14 +1,13 @@
1
1
  {
2
2
  "name": "@selfagency/beans-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
+ "private": false,
4
5
  "description": "MCP (Model Context Protocol) server for Beans issue tracker",
5
- "keywords": [
6
- "beans",
7
- "mcp",
8
- "model-context-protocol",
9
- "issue-tracker",
10
- "ai"
11
- ],
6
+ "author": {
7
+ "name": "Daniel Sieradski",
8
+ "email": "daniel@self.agency",
9
+ "url": "https://self.agency"
10
+ },
12
11
  "homepage": "https://github.com/hmans/beans",
13
12
  "bugs": {
14
13
  "url": "https://github.com/selfagency/beans-mcp/issues"
@@ -17,27 +16,65 @@
17
16
  "type": "git",
18
17
  "url": "https://github.com/selfagency/beans-mcp.git"
19
18
  },
20
- "license": "MIT",
21
- "author": {
22
- "name": "Daniel Sieradski",
23
- "email": "daniel@self.agency",
24
- "url": "https://self.agency"
25
- },
26
- "main": "./index.cjs",
27
- "module": "./index.js",
28
- "types": "./index.d.ts",
29
- "files": [
30
- "./index.cjs",
31
- "./index.js",
32
- "./index.d.ts"
19
+ "keywords": [
20
+ "beans",
21
+ "mcp",
22
+ "model-context-protocol",
23
+ "issue-tracker",
24
+ "ai"
33
25
  ],
34
- "bin": {
35
- "beans-mcp": "./beans-mcp-server.cjs"
36
- },
26
+ "license": "MIT",
27
+ "type": "module",
37
28
  "exports": {
38
29
  ".": {
39
- "import": "./index.js",
40
- "require": "./index.cjs"
30
+ "types": "./dist/index.d.ts",
31
+ "import": "./dist/index.js",
32
+ "require": "./dist/index.cjs"
41
33
  }
34
+ },
35
+ "main": "./dist/index.cjs",
36
+ "module": "./dist/index.js",
37
+ "types": "./dist/index.d.ts",
38
+ "bin": {
39
+ "beans-mcp": "./dist/beans-mcp-server.cjs"
40
+ },
41
+ "devDependencies": {
42
+ "@modelcontextprotocol/sdk": "^1.27.1",
43
+ "@octokit/rest": "^22.0.1",
44
+ "@types/node": "^20.19.0",
45
+ "@vitest/coverage-v8": "^4.0.18",
46
+ "@vitest/ui": "4.0.18",
47
+ "husky": "^9.1.7",
48
+ "lint-staged": "^16.2.7",
49
+ "ora": "^9.3.0",
50
+ "oxfmt": "^0.35.0",
51
+ "oxlint": "^1.50.0",
52
+ "oxlint-tsgolint": "^0.15.0",
53
+ "tsup": "8.5.1",
54
+ "typescript": "^5.9.3",
55
+ "vitest": "4.0.18",
56
+ "zod": "4.3.6",
57
+ "zx": "^8.8.5"
58
+ },
59
+ "engines": {
60
+ "node": ">=18"
61
+ },
62
+ "lint-staged": {
63
+ "src/**/*.ts": [
64
+ "pnpm run lint:fix",
65
+ "pnpm run format"
66
+ ]
67
+ },
68
+ "scripts": {
69
+ "build": "tsup",
70
+ "format": "oxfmt",
71
+ "lint:fix": "oxlint --fix",
72
+ "lint": "oxlint",
73
+ "postbuild": "node ./scripts/write-dist-package.js",
74
+ "release": "zx ./scripts/release.js",
75
+ "test:coverage": "vitest run --coverage",
76
+ "test:watch": "vitest",
77
+ "test": "vitest run",
78
+ "type-check": "tsc --noEmit"
42
79
  }
43
- }
80
+ }
@@ -0,0 +1,2 @@
1
+ onlyBuiltDependencies:
2
+ - esbuild
@@ -0,0 +1,433 @@
1
+ #!/usr/bin/env zx
2
+
3
+ import { Octokit } from '@octokit/rest';
4
+ import { spawnSync } from 'node:child_process';
5
+ import { readFileSync, writeFileSync } from 'node:fs';
6
+ import { dirname, resolve } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import ora from 'ora';
9
+
10
+ $.verbose = false;
11
+
12
+ const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
13
+ cd(ROOT);
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Argument validation
17
+ // ---------------------------------------------------------------------------
18
+
19
+ const version = argv._[0];
20
+ if (!version || !/^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(version)) {
21
+ console.error('Usage: pnpm release <version> (e.g. pnpm release 1.0.5)');
22
+ process.exit(1);
23
+ }
24
+
25
+ const tag = `v${version}`;
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Rollback state
29
+ // commitLocal — release commit exists locally but has not been pushed
30
+ // commitPushed — release commit has been pushed to origin/main
31
+ // tagPushed — tag has been pushed but release workflow has not yet succeeded
32
+ // releaseDone — release workflow succeeded; nothing to undo
33
+ // ---------------------------------------------------------------------------
34
+
35
+ let commitLocal = false;
36
+ let commitPushed = false;
37
+ let tagPushed = false;
38
+ let releaseDone = false;
39
+ let gitCmd = 'git';
40
+
41
+ function runGit(args, options = {}) {
42
+ const result = spawnSync(gitCmd, args, {
43
+ cwd: ROOT,
44
+ encoding: 'utf8',
45
+ shell: false,
46
+ ...options,
47
+ });
48
+
49
+ if (result.status !== 0) {
50
+ const stderr = (result.stderr || '').trim();
51
+ const stdout = (result.stdout || '').trim();
52
+ const details = stderr || stdout || `git ${args.join(' ')} failed with exit code ${result.status}`;
53
+ throw new Error(details);
54
+ }
55
+
56
+ return result;
57
+ }
58
+
59
+ function resolveGitExecutable() {
60
+ const direct = spawnSync('git', ['--version'], { stdio: 'ignore', shell: false });
61
+ if (direct.status === 0) {return 'git';}
62
+
63
+ const locatorCommand = process.platform === 'win32' ? 'where' : 'which';
64
+ const located = spawnSync(locatorCommand, ['git'], { encoding: 'utf8', shell: false });
65
+ if (located.status === 0) {
66
+ const candidate = located.stdout
67
+ .split(/\r?\n/)
68
+ .map((line) => line.trim())
69
+ .find(Boolean);
70
+ if (candidate) {
71
+ return candidate;
72
+ }
73
+ }
74
+
75
+ return null;
76
+ }
77
+
78
+ async function rollback() {
79
+ if (releaseDone) {return;}
80
+ $.verbose = false;
81
+ try {
82
+ if (tagPushed) {
83
+ console.log(`\n⚠️ Release workflow failed or was interrupted. Deleting remote tag ${tag}...`);
84
+ try {
85
+ runGit(['push', 'origin', '--delete', tag]);
86
+ runGit(['tag', '-d', tag]);
87
+ console.log(`↩️ Tag ${tag} deleted from remote and local.`);
88
+ } catch {
89
+ console.error(`❌ Could not delete tag. Manually run:`);
90
+ console.error(` git push origin --delete ${tag} && git tag -d ${tag}`);
91
+ }
92
+ }
93
+ if (commitPushed) {
94
+ console.log('\n⚠️ Reverting release commit on origin/main...');
95
+ try {
96
+ runGit(['revert', '--no-edit', 'HEAD']);
97
+ runGit(['push', 'origin', 'main']);
98
+ console.log('↩️ Release commit reverted and pushed. Working tree is clean.');
99
+ } catch {
100
+ console.error('❌ Automatic revert failed. Manually run:');
101
+ console.error(' git revert HEAD && git push origin main');
102
+ }
103
+ } else if (commitLocal) {
104
+ console.log('\n⚠️ Release aborted before push. Resetting local release commit...');
105
+ try {
106
+ runGit(['reset', '--hard', 'HEAD~1']);
107
+ console.log('↩️ Local release commit removed. Working tree restored.');
108
+ } catch {
109
+ console.error('❌ Reset failed. Manually run: git reset --hard HEAD~1');
110
+ }
111
+ }
112
+ } catch { /* best effort */ }
113
+ }
114
+
115
+ process.on('SIGINT', async () => { await rollback(); process.exit(130); });
116
+ process.on('SIGTERM', async () => { await rollback(); process.exit(143); });
117
+
118
+ // ---------------------------------------------------------------------------
119
+ // Main — wrapped so any unhandled error triggers rollback
120
+ // ---------------------------------------------------------------------------
121
+
122
+ async function main() {
123
+ // --- Prerequisites -------------------------------------------------------
124
+
125
+ const resolvedGit = resolveGitExecutable();
126
+ if (!resolvedGit) {
127
+ console.error("❌ 'git' is required but not found in PATH.");
128
+ process.exit(1);
129
+ }
130
+ gitCmd = resolvedGit;
131
+
132
+ // Check npm credentials.
133
+ try {
134
+ await $`npm whoami`;
135
+ } catch {
136
+ console.error('❌ Not logged in to npm. Run: npm login');
137
+ process.exit(1);
138
+ }
139
+
140
+ // Resolve GitHub auth token: prefer env vars, then ask the gh CLI.
141
+ let githubToken = process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? '';
142
+ if (!githubToken) {
143
+ try {
144
+ githubToken = (await $`gh auth token`).stdout.trim();
145
+ } catch {
146
+ console.error('❌ No GitHub token found. Set GH_TOKEN/GITHUB_TOKEN or run: gh auth login');
147
+ process.exit(1);
148
+ }
149
+ }
150
+
151
+ const octokit = new Octokit({ auth: githubToken });
152
+
153
+ // --- Precondition checks --------------------------------------------------
154
+
155
+ const dirty = runGit(['status', '--porcelain']).stdout.trim();
156
+ if (dirty) {
157
+ console.error('❌ Working tree is not clean. Commit or stash changes first.');
158
+ process.exit(1);
159
+ }
160
+
161
+ const branch = runGit(['rev-parse', '--abbrev-ref', 'HEAD']).stdout.trim();
162
+ if (branch !== 'main') {
163
+ console.error(`❌ Must run from 'main'. Current branch: ${branch}`);
164
+ process.exit(1);
165
+ }
166
+
167
+ console.log('🔄 Fetching latest refs...');
168
+ runGit(['fetch', 'origin', 'main']);
169
+ runGit(['pull', '--ff-only', 'origin', 'main']);
170
+
171
+ // Derive owner/repo from the git remote URL.
172
+ const remoteUrl = runGit(['remote', 'get-url', 'origin']).stdout.trim();
173
+ const repoMatch = remoteUrl.match(/[:/]([^/]+)\/([^/.]+?)(\.git)?$/);
174
+ if (!repoMatch) {
175
+ console.error(`❌ Cannot parse owner/repo from remote URL: ${remoteUrl}`);
176
+ process.exit(1);
177
+ }
178
+ const [, owner, repo] = repoMatch;
179
+
180
+ // Check for existing local tag.
181
+ const localTag = runGit(['tag', '-l', tag]).stdout.trim();
182
+ if (localTag) {
183
+ console.error(`❌ Local tag ${tag} already exists.`);
184
+ process.exit(1);
185
+ }
186
+
187
+ // Check for existing remote tag via the API.
188
+ try {
189
+ await octokit.git.getRef({ owner, repo, ref: `tags/${tag}` });
190
+ console.error(`❌ Remote tag ${tag} already exists.`);
191
+ process.exit(1);
192
+ } catch (err) {
193
+ if (err.status !== 404) {
194
+ throw err;
195
+ }
196
+ // 404 = tag does not exist; that's what we want.
197
+ }
198
+
199
+ // --- Previous tag (for release notes diff) --------------------------------
200
+
201
+ const tagsResp = await octokit.paginate(octokit.git.listMatchingRefs, {
202
+ owner,
203
+ repo,
204
+ ref: 'tags/v',
205
+ per_page: 100,
206
+ });
207
+
208
+ const previousTag = tagsResp
209
+ .map((r) => r.ref.replace('refs/tags/', ''))
210
+ .filter((t) => t !== tag)
211
+ .sort((a, b) => {
212
+ const parse = (v) => v.replace(/^v/, '').split('.').map(Number);
213
+ const [aMaj, aMin, aPatch] = parse(a);
214
+ const [bMaj, bMin, bPatch] = parse(b);
215
+ return aMaj - bMaj || aMin - bMin || aPatch - bPatch;
216
+ })
217
+ .at(-1) ?? '';
218
+
219
+ // --- Release notes --------------------------------------------------------
220
+
221
+ console.log(`📝 Generating release notes for ${tag}...`);
222
+
223
+ const notesResp = await octokit.repos.generateReleaseNotes({
224
+ owner,
225
+ repo,
226
+ tag_name: tag,
227
+ target_commitish: 'main',
228
+ ...(previousTag ? { previous_tag_name: previousTag } : {}),
229
+ });
230
+ const releaseNotes = notesResp.data.body?.trim() || '- No notable changes.';
231
+
232
+ // --- Update package.json --------------------------------------------------
233
+
234
+ console.log(`🧩 Updating package.json to ${version}...`);
235
+ const pkgPath = resolve(ROOT, 'package.json');
236
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
237
+ pkg.version = version;
238
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
239
+
240
+ // --- Update CHANGELOG.md --------------------------------------------------
241
+
242
+ console.log('🧩 Updating CHANGELOG.md...');
243
+ const changelogPath = resolve(ROOT, 'CHANGELOG.md');
244
+ const date = new Date().toISOString().slice(0, 10);
245
+ const heading = `## [${version}] - ${date}`;
246
+ const sourceLine = previousTag ? `\n\n_Source: changes from ${previousTag} to ${tag}._` : '';
247
+ const section = `\n${heading}\n\n${releaseNotes}${sourceLine}\n`;
248
+
249
+ let original;
250
+ try {
251
+ original = readFileSync(changelogPath, 'utf8');
252
+ } catch {
253
+ original = '# Change Log\n\n## [Unreleased]\n';
254
+ }
255
+
256
+ if (!original.includes(heading)) {
257
+ const marker = '## [Unreleased]';
258
+ const idx = original.indexOf(marker);
259
+ const updated =
260
+ idx >= 0
261
+ ? `${original.slice(0, idx + marker.length)}\n${section}${original.slice(idx + marker.length)}`
262
+ : `${original}\n${section}`;
263
+ writeFileSync(changelogPath, updated);
264
+ } else {
265
+ console.log('ℹ️ CHANGELOG already contains this release heading; skipping.');
266
+ }
267
+
268
+ // --- Commit + push --------------------------------------------------------
269
+
270
+ const hasChanges = runGit(['diff', '--name-only', '--', 'package.json', 'CHANGELOG.md']).stdout.trim();
271
+ if (hasChanges) {
272
+ console.log('📦 Committing release metadata changes...');
273
+ runGit(['add', 'package.json', 'CHANGELOG.md']);
274
+ runGit(['commit', '-m', `chore(release): update version and changelog for ${tag}`]);
275
+ commitLocal = true;
276
+ } else {
277
+ console.log('ℹ️ No version/changelog changes detected; nothing to commit.');
278
+ }
279
+
280
+ console.log('🚀 Pushing main...');
281
+ runGit(['push', 'origin', 'main']);
282
+ commitPushed = true;
283
+ commitLocal = false;
284
+
285
+ const headSha = runGit(['rev-parse', 'HEAD']).stdout.trim();
286
+
287
+ // --- Wait for required workflows (sequential to avoid concurrent-spinner visual corruption) ------
288
+
289
+ const shortSha = headSha.slice(0, 7);
290
+ console.log(`🔎 Waiting for required workflows on ${shortSha}...`);
291
+ // Give GitHub a moment to register the push before we start polling.
292
+ await sleep(10_000);
293
+
294
+ const spinner = ora({ text: 'Tests: queued' }).start();
295
+ for (const name of ['Test & Build']) {
296
+ spinner.text = `${name}: queued`;
297
+ spinner.start();
298
+ await waitForWorkflow(octokit, name, owner, repo, headSha, spinner);
299
+ }
300
+
301
+ // --- Tag + publish --------------------------------------------------------
302
+
303
+ console.log(`🏷️ Creating annotated tag ${tag} at ${headSha}...`);
304
+
305
+ const tagMessage = [
306
+ `Release ${tag}`,
307
+ releaseNotes,
308
+ previousTag ? `Source: changes from ${previousTag} to ${tag}.` : '',
309
+ `Target commit: ${headSha}`,
310
+ ]
311
+ .filter(Boolean)
312
+ .join('\n\n');
313
+
314
+ runGit(['tag', '-a', tag, headSha, '-m', tagMessage]);
315
+
316
+ console.log(`🚀 Pushing tag ${tag}...`);
317
+ runGit(['push', 'origin', tag]);
318
+ tagPushed = true;
319
+
320
+ // --- Watch the release workflow ------------------------------------------
321
+
322
+ spinner.text = 'Release: waiting for workflow to trigger...';
323
+ spinner.start();
324
+ await waitForWorkflow(octokit, 'Release', owner, repo, headSha, spinner, {
325
+ autoDispatch: false,
326
+ branch: null,
327
+ });
328
+
329
+ releaseDone = true;
330
+ console.log(`✅ GitHub release complete: ${tag} → ${headSha}`);
331
+
332
+ // --- npm publish ----------------------------------------------------------
333
+
334
+ console.log('📦 Building package...');
335
+ $.verbose = true;
336
+ await $`pnpm build`;
337
+ $.verbose = false;
338
+
339
+ const distTag = version.includes('-') ? 'next' : 'latest';
340
+ console.log(`🚀 Publishing ${tag} to npm (dist-tag: ${distTag})...`);
341
+ $.verbose = true;
342
+ await $`npm publish ./dist --tag ${distTag}`;
343
+ $.verbose = false;
344
+ console.log(`✅ Published ${tag} to npm.`);
345
+ }
346
+
347
+ // ---------------------------------------------------------------------------
348
+ // Workflow polling
349
+ // ---------------------------------------------------------------------------
350
+
351
+ async function waitForWorkflow(
352
+ octokit,
353
+ name,
354
+ owner,
355
+ repo,
356
+ headSha,
357
+ spinner,
358
+ { timeoutMs = 3_600_000, pollMs = 15_000, autoDispatch = true, branch = 'main' } = {},
359
+ ) {
360
+ // Resolve the workflow ID by name.
361
+ const workflowsResp = await octokit.actions.listRepoWorkflows({ owner, repo, per_page: 100 });
362
+ const workflow = workflowsResp.data.workflows.find((w) => w.name === name);
363
+ if (!workflow) {
364
+ spinner.fail(`${name}: workflow not found in ${owner}/${repo}`);
365
+ throw new Error(`[${name}] workflow not found in ${owner}/${repo}`);
366
+ }
367
+
368
+ const deadline = Date.now() + timeoutMs;
369
+ let triggered = false;
370
+ // Track cancelled run IDs so we skip them on subsequent polls and don't
371
+ // mistake them for the new run that was re-dispatched.
372
+ const cancelledRunIds = new Set();
373
+
374
+ while (Date.now() < deadline) {
375
+ const runsResp = await octokit.actions.listWorkflowRuns({
376
+ owner,
377
+ repo,
378
+ workflow_id: workflow.id,
379
+ ...(branch ? { branch } : {}),
380
+ head_sha: headSha,
381
+ per_page: 10,
382
+ });
383
+
384
+ // Find the latest run that isn't one we already marked as cancelled.
385
+ const run = runsResp.data.workflow_runs.find((r) => !cancelledRunIds.has(r.id));
386
+
387
+ if (!run) {
388
+ if (autoDispatch && !triggered) {
389
+ spinner.text = `${name}: no run found — triggering workflow_dispatch...`;
390
+ await octokit.actions.createWorkflowDispatch({ owner, repo, workflow_id: workflow.id, ref: 'main' });
391
+ triggered = true;
392
+ spinner.text = `${name}: waiting for run to appear...`;
393
+ } else {
394
+ spinner.text = `${name}: waiting for run to appear...`;
395
+ }
396
+ } else if (run.status !== 'completed') {
397
+ const elapsed = Math.round((Date.now() - new Date(run.created_at).getTime()) / 1000);
398
+ spinner.text = `${name}: ${run.status} (${elapsed}s elapsed)`;
399
+ } else if (run.conclusion === 'success') {
400
+ spinner.succeed(`${name}: passed`);
401
+ return;
402
+ } else if (run.conclusion === 'cancelled') {
403
+ // Cancelled runs are often caused by a concurrent push racing with CI startup.
404
+ // Record this run so we skip it on future polls, then re-dispatch.
405
+ cancelledRunIds.add(run.id);
406
+ spinner.text = `${name}: run was cancelled — re-dispatching...`;
407
+ triggered = false;
408
+ } else {
409
+ spinner.fail(`${name}: ${run.conclusion}`);
410
+ throw new Error(`[${name}] conclusion=${run.conclusion}\n Run: ${run.html_url}`);
411
+ }
412
+
413
+ await sleep(pollMs);
414
+ }
415
+
416
+ spinner.fail(`${name}: timed out`);
417
+ throw new Error(`[${name}] timed out after ${timeoutMs / 1000}s`);
418
+ }
419
+
420
+ // ---------------------------------------------------------------------------
421
+ // Entry point
422
+ // ---------------------------------------------------------------------------
423
+
424
+ main().catch(async (err) => {
425
+ const msg = err?.message ?? String(err);
426
+ // ProcessOutput errors from zx already printed the command output; only
427
+ // print extra context for our own thrown errors.
428
+ if (!(err instanceof ProcessOutput)) {
429
+ console.error(`❌ ${msg}`);
430
+ }
431
+ await rollback();
432
+ process.exit(err?.exitCode ?? 1);
433
+ });
@@ -0,0 +1,53 @@
1
+ import { copyFile, mkdir, readFile, writeFile } from 'fs/promises';
2
+ import { dirname, resolve } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = dirname(__filename);
7
+
8
+ async function main() {
9
+ const rootPkgPath = resolve(__dirname, '..', 'package.json');
10
+ const outDir = resolve(__dirname, '..', 'dist');
11
+ const raw = await readFile(rootPkgPath, 'utf8');
12
+ const {name, version, description, keywords, homepage, bugs, issues, repository, license, author} = JSON.parse(raw);
13
+
14
+ const distPkg = {
15
+ name,
16
+ version,
17
+ description,
18
+ keywords,
19
+ homepage,
20
+ bugs,
21
+ issues,
22
+ repository,
23
+ license,
24
+ author,
25
+ main: './index.cjs',
26
+ module: './index.js',
27
+ types: './index.d.ts',
28
+ files: ['./index.cjs', './index.js', './index.d.ts'],
29
+ bin: {
30
+ 'beans-mcp': './beans-mcp-server.cjs'
31
+ },
32
+ exports: {
33
+ '.': {
34
+ import: './index.js',
35
+ require: './index.cjs'
36
+ }
37
+ }
38
+ };
39
+
40
+ await mkdir(outDir, { recursive: true });
41
+ await writeFile(resolve(outDir, 'package.json'), JSON.stringify(distPkg, null, 2) + '\n', 'utf8');
42
+ console.log('Wrote', resolve(outDir, 'package.json'));
43
+
44
+ const readmeSrc = resolve(__dirname, '..', 'README.md');
45
+ const readmeDest = resolve(outDir, 'README.md');
46
+ await copyFile(readmeSrc, readmeDest);
47
+ console.log('Copied', readmeSrc, 'to', readmeDest);
48
+ }
49
+
50
+ main().catch((err) => {
51
+ console.error(err);
52
+ process.exitCode = 1;
53
+ });
package/src/cli.ts ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * CLI entry point for beans-mcp-server
3
+ *
4
+ * Usage:
5
+ * beans-mcp-server [workspace-root] [options]
6
+ * beans-mcp-server --help
7
+ */
8
+
9
+ import { startBeansMcpServer } from './server/BeansMcpServer';
10
+
11
+ startBeansMcpServer(process.argv.slice(2)).catch(error => {
12
+ console.error('[beans-mcp-server] fatal:', error);
13
+ process.exit(1);
14
+ });
package/src/index.ts ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Public API for beans-mcp-server
3
+ *
4
+ * Exports:
5
+ * - createBeansMcpServer: Create an MCP server instance
6
+ * - startBeansMcpServer: CLI entrypoint for launching as stdio server
7
+ * - parseCliArgs: Parse CLI arguments for configuration
8
+ * - isPathWithinRoot: Utility to validate file paths stay within root
9
+ * - sortBeans: Sort beans by specified mode
10
+ *
11
+ * Types:
12
+ * - BeanRecord, SortMode, GraphQLError
13
+ * - BackendInterface: Interface for custom backend implementations
14
+ */
15
+
16
+ export { createBeansMcpServer, parseCliArgs, startBeansMcpServer } from './server/BeansMcpServer';
17
+ export { BeansCliBackend, type BackendInterface } from './server/backend';
18
+ export { sortBeans } from './internal/queryHelpers';
19
+ export { isPathWithinRoot, makeTextAndStructured } from './utils';
20
+ export type { BeanRecord, SortMode, GraphQLError } from './types';
21
+ export { DEFAULT_MCP_PORT, MAX_ID_LENGTH, MAX_TITLE_LENGTH, MAX_METADATA_LENGTH } from './types';