@selfagency/beans-mcp 0.1.3 → 0.4.2

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 (59) hide show
  1. package/README.md +63 -6
  2. package/{dist/beans-mcp-server.cjs → beans-mcp-server.cjs} +269 -34
  3. package/{dist/index.cjs → index.cjs} +269 -34
  4. package/{dist/index.d.ts → index.d.ts} +19 -1
  5. package/{dist/index.js → index.js} +269 -34
  6. package/package.json +28 -64
  7. package/.beans.yml +0 -6
  8. package/.claude/settings.local.json +0 -18
  9. package/.editorconfig +0 -13
  10. package/.github/dependabot.yml +0 -11
  11. package/.github/workflows/release.yml +0 -235
  12. package/.github/workflows/test.yml +0 -84
  13. package/.husky/pre-commit +0 -1
  14. package/.nvmrc +0 -1
  15. package/.oxfmtrc.json +0 -11
  16. package/.oxlintrc.json +0 -37
  17. package/.vscode/settings.json +0 -3
  18. package/CHANGELOG.md +0 -160
  19. package/CONTRIBUTING.md +0 -139
  20. package/LICENSE.txt +0 -21
  21. package/codeql/codeql-custom-queries-actions/README.md +0 -14
  22. package/codeql/codeql-custom-queries-actions/codeql-pack.lock.yml +0 -32
  23. package/codeql/codeql-custom-queries-actions/codeql-pack.yml +0 -7
  24. package/codeql/codeql-custom-queries-actions/qlpack.yml +0 -6
  25. package/codeql/codeql-custom-queries-actions/queries/github-script-without-tojson.ql +0 -18
  26. package/codeql/codeql-custom-queries-actions/queries/strict-external-action-pinning.ql +0 -18
  27. package/codeql/codeql-custom-queries-javascript/README.md +0 -14
  28. package/codeql/codeql-custom-queries-javascript/codeql-pack.lock.yml +0 -30
  29. package/codeql/codeql-custom-queries-javascript/codeql-pack.yml +0 -7
  30. package/codeql/codeql-custom-queries-javascript/qlpack.yml +0 -6
  31. package/codeql/codeql-custom-queries-javascript/queries/child-process-shell-apis.ql +0 -26
  32. package/codeql/codeql-custom-queries-javascript/queries/innerhtml-assignment.ql +0 -24
  33. package/dist/README.md +0 -307
  34. package/dist/beans-mcp-server.cjs.map +0 -1
  35. package/dist/index.cjs.map +0 -1
  36. package/dist/index.js.map +0 -1
  37. package/dist/package.json +0 -43
  38. package/pnpm-workspace.yaml +0 -2
  39. package/scripts/release.js +0 -433
  40. package/scripts/write-dist-package.js +0 -53
  41. package/src/cli.ts +0 -14
  42. package/src/index.ts +0 -21
  43. package/src/internal/graphql.ts +0 -33
  44. package/src/internal/queryHelpers.ts +0 -157
  45. package/src/server/BeansMcpServer.ts +0 -623
  46. package/src/server/backend.ts +0 -364
  47. package/src/test/BeansMcpServer.test.ts +0 -514
  48. package/src/test/handlers.unit.test.ts +0 -201
  49. package/src/test/parseCliArgs.test.ts +0 -69
  50. package/src/test/protocol.e2e.test.ts +0 -884
  51. package/src/test/queryHelpers.test.ts +0 -524
  52. package/src/test/startBeansMcpServer.test.ts +0 -146
  53. package/src/test/tools-integration.test.ts +0 -912
  54. package/src/test/utils.test.ts +0 -81
  55. package/src/types.ts +0 -46
  56. package/src/utils.ts +0 -20
  57. package/tsconfig.json +0 -24
  58. package/tsup.config.ts +0 -42
  59. package/vitest.config.ts +0 -18
@@ -1,364 +0,0 @@
1
- import { execFile } from 'node:child_process';
2
- import { createReadStream } from 'node:fs';
3
- import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
4
- import { dirname, join, resolve } from 'node:path';
5
- import { createInterface } from 'node:readline';
6
- import { promisify } from 'node:util';
7
- import * as graphql from '../internal/graphql';
8
- import { BeanRecord, GraphQLError } from '../types';
9
- import { isPathWithinRoot } from '../utils';
10
-
11
- const execFileAsync = promisify(execFile);
12
-
13
- /**
14
- * Interface for backend implementations.
15
- * Allows for alternative implementations (e.g., test harnesses).
16
- */
17
- export interface BackendInterface {
18
- init(prefix?: string): Promise<Record<string, unknown>>;
19
- list(options?: { status?: string[]; type?: string[]; search?: string }): Promise<BeanRecord[]>;
20
- create(input: {
21
- title: string;
22
- type: string;
23
- status?: string;
24
- priority?: string;
25
- description?: string;
26
- parent?: string;
27
- }): Promise<BeanRecord>;
28
- update(
29
- beanId: string,
30
- updates: {
31
- status?: string;
32
- type?: string;
33
- priority?: string;
34
- parent?: string;
35
- clearParent?: boolean;
36
- blocking?: string[];
37
- blockedBy?: string[];
38
- body?: string;
39
- },
40
- ): Promise<BeanRecord>;
41
- delete(beanId: string): Promise<Record<string, unknown>>;
42
- openConfig(): Promise<{ configPath: string; content: string }>;
43
- graphqlSchema(): Promise<string>;
44
- readOutputLog(options?: { lines?: number }): Promise<{ path: string; content: string; linesReturned: number }>;
45
- readBeanFile(relativePath: string): Promise<{ path: string; content: string }>;
46
- editBeanFile(relativePath: string, content: string): Promise<{ path: string; bytes: number }>;
47
- createBeanFile(
48
- relativePath: string,
49
- content: string,
50
- options?: { overwrite?: boolean },
51
- ): Promise<{ path: string; bytes: number; created: boolean }>;
52
- deleteBeanFile(relativePath: string): Promise<{ path: string; deleted: boolean }>;
53
- }
54
-
55
- /**
56
- * Beans CLI backend implementation.
57
- * Wraps the Beans CLI and provides a typed interface for MCP tools.
58
- */
59
- export class BeansCliBackend implements BackendInterface {
60
- constructor(
61
- private readonly workspaceRoot: string,
62
- private readonly cliPath: string,
63
- private readonly logDir?: string,
64
- ) {}
65
-
66
- /**
67
- * Returns a safe environment for executing the Beans CLI,
68
- * whitelisting only necessary variables.
69
- */
70
- private getSafeEnv(): NodeJS.ProcessEnv {
71
- const whitelist = ['PATH', 'HOME', 'USER', 'LANG', 'LC_ALL', 'LC_CTYPE', 'SHELL'];
72
- const env: NodeJS.ProcessEnv = {};
73
-
74
- for (const key of whitelist) {
75
- if (process.env[key]) {
76
- env[key] = process.env[key];
77
- }
78
- }
79
-
80
- // Include BEANS_ variables
81
- for (const key in process.env) {
82
- if (key.startsWith('BEANS_')) {
83
- env[key] = process.env[key];
84
- }
85
- }
86
-
87
- return env;
88
- }
89
-
90
- private getBeansRoot(): string {
91
- return resolve(this.workspaceRoot, '.beans');
92
- }
93
-
94
- private resolveBeanFilePath(relativePath: string): string {
95
- const cleaned = relativePath.trim().replace(/^\/+/, '');
96
- if (!cleaned) {
97
- throw new Error('Path is required');
98
- }
99
-
100
- const beansRoot = this.getBeansRoot();
101
- const target = resolve(beansRoot, cleaned);
102
-
103
- if (!isPathWithinRoot(beansRoot, target)) {
104
- throw new Error('Path must stay within .beans directory');
105
- }
106
-
107
- return target;
108
- }
109
-
110
- /**
111
- * Execute a GraphQL query via the Beans CLI.
112
- */
113
- private async executeGraphQL<T>(
114
- query: string,
115
- variables?: Record<string, unknown>,
116
- ): Promise<{ data: T; errors?: GraphQLError[] }> {
117
- const args = ['graphql', '--json', query];
118
-
119
- if (variables) {
120
- args.push('--variables', JSON.stringify(variables));
121
- }
122
-
123
- const { stdout } = await execFileAsync(this.cliPath, args, {
124
- cwd: this.workspaceRoot,
125
- env: this.getSafeEnv(),
126
- maxBuffer: 10 * 1024 * 1024,
127
- timeout: 30000,
128
- });
129
-
130
- try {
131
- // CLI outputs the data portion directly (e.g. {"beans": [...]})
132
- // without a {"data": ...} envelope.
133
- return { data: JSON.parse(stdout) as T };
134
- } catch (error) {
135
- throw new Error(
136
- `Failed to parse Beans CLI GraphQL output: ${(error as Error).message}\nOutput: ${stdout.slice(0, 1000)}`,
137
- );
138
- }
139
- }
140
-
141
- async init(prefix?: string): Promise<Record<string, unknown>> {
142
- const args = ['init'];
143
- if (prefix) {
144
- args.push('--prefix', prefix);
145
- }
146
- await execFileAsync(this.cliPath, args, {
147
- cwd: this.workspaceRoot,
148
- env: this.getSafeEnv(),
149
- maxBuffer: 10 * 1024 * 1024,
150
- timeout: 30000,
151
- });
152
-
153
- return { initialized: true };
154
- }
155
-
156
- async list(options?: { status?: string[]; type?: string[]; search?: string }): Promise<BeanRecord[]> {
157
- const filter: { status?: string[]; type?: string[]; search?: string } = {};
158
-
159
- if (options?.status && options.status.length > 0) {
160
- filter.status = options.status;
161
- }
162
-
163
- if (options?.type && options.type.length > 0) {
164
- filter.type = options.type;
165
- }
166
-
167
- if (options?.search) {
168
- filter.search = options.search;
169
- }
170
-
171
- const { data, errors } = await this.executeGraphQL<{ beans: BeanRecord[] }>(graphql.LIST_BEANS_QUERY, { filter });
172
-
173
- if (errors && errors.length > 0) {
174
- throw new Error(`GraphQL error: ${errors.map(e => e.message).join(', ')}`);
175
- }
176
-
177
- return data.beans;
178
- }
179
-
180
- async create(input: {
181
- title: string;
182
- type: string;
183
- status?: string;
184
- priority?: string;
185
- description?: string;
186
- parent?: string;
187
- }): Promise<BeanRecord> {
188
- const createInput: Record<string, unknown> = {
189
- title: input.title,
190
- type: input.type,
191
- status: input.status,
192
- priority: input.priority,
193
- body: input.description,
194
- parent: input.parent,
195
- };
196
-
197
- const { data, errors } = await this.executeGraphQL<{ createBean: BeanRecord }>(graphql.CREATE_BEAN_MUTATION, {
198
- input: createInput,
199
- });
200
-
201
- if (errors && errors.length > 0) {
202
- throw new Error(`GraphQL error: ${errors.map(e => e.message).join(', ')}`);
203
- }
204
-
205
- return data.createBean;
206
- }
207
-
208
- async update(
209
- beanId: string,
210
- updates: {
211
- status?: string;
212
- type?: string;
213
- priority?: string;
214
- parent?: string;
215
- clearParent?: boolean;
216
- blocking?: string[];
217
- blockedBy?: string[];
218
- body?: string;
219
- },
220
- ): Promise<BeanRecord> {
221
- const updateInput: Record<string, unknown> = {
222
- status: updates.status,
223
- type: updates.type,
224
- priority: updates.priority,
225
- };
226
-
227
- if (updates.parent !== undefined) {
228
- updateInput.parent = updates.parent;
229
- } else if (updates.clearParent) {
230
- updateInput.parent = '';
231
- }
232
-
233
- if (updates.blocking) {
234
- updateInput.addBlocking = updates.blocking;
235
- }
236
-
237
- if (updates.blockedBy) {
238
- updateInput.addBlockedBy = updates.blockedBy;
239
- }
240
-
241
- if (updates.body !== undefined) {
242
- updateInput.body = updates.body;
243
- }
244
-
245
- const { data, errors } = await this.executeGraphQL<{ updateBean: BeanRecord }>(graphql.UPDATE_BEAN_MUTATION, {
246
- id: beanId,
247
- input: updateInput,
248
- });
249
-
250
- if (errors && errors.length > 0) {
251
- throw new Error(`GraphQL error: ${errors.map(e => e.message).join(', ')}`);
252
- }
253
-
254
- return data.updateBean;
255
- }
256
-
257
- async delete(beanId: string): Promise<Record<string, unknown>> {
258
- const { errors } = await this.executeGraphQL<{ deleteBean: boolean }>(graphql.DELETE_BEAN_MUTATION, {
259
- id: beanId,
260
- });
261
-
262
- if (errors && errors.length > 0) {
263
- throw new Error(`GraphQL error: ${errors.map(e => e.message).join(', ')}`);
264
- }
265
-
266
- return { deleted: true, beanId };
267
- }
268
-
269
- async openConfig(): Promise<{ configPath: string; content: string }> {
270
- const configPath = join(this.workspaceRoot, '.beans.yml');
271
- const content = await readFile(configPath, 'utf8');
272
- return { configPath, content };
273
- }
274
-
275
- async graphqlSchema(): Promise<string> {
276
- const { stdout } = await execFileAsync(this.cliPath, ['graphql', '--schema'], {
277
- cwd: this.workspaceRoot,
278
- env: this.getSafeEnv(),
279
- maxBuffer: 10 * 1024 * 1024,
280
- timeout: 30000,
281
- });
282
-
283
- return stdout.trim();
284
- }
285
-
286
- async readOutputLog(options?: { lines?: number }): Promise<{ path: string; content: string; linesReturned: number }> {
287
- const outputPath = resolve(
288
- process.env.BEANS_VSCODE_OUTPUT_LOG || join(this.workspaceRoot, '.vscode', 'logs', 'beans-output.log'),
289
- );
290
-
291
- const isWithinWorkspace = isPathWithinRoot(this.workspaceRoot, outputPath);
292
- const vscodeLogDir =
293
- process.env.BEANS_VSCODE_LOG_DIR || this.logDir
294
- ? resolve(process.env.BEANS_VSCODE_LOG_DIR || this.logDir || '')
295
- : undefined;
296
- const isWithinVscodeLogDir = vscodeLogDir ? isPathWithinRoot(vscodeLogDir, outputPath) : false;
297
-
298
- if (!isWithinWorkspace && !isWithinVscodeLogDir) {
299
- throw new Error('Output log path must stay within the workspace or VS Code log directory');
300
- }
301
-
302
- const maxLines = options?.lines && options.lines > 0 ? options.lines : 500;
303
- const ringBuffer: string[] = [];
304
-
305
- const stream = createReadStream(outputPath, { encoding: 'utf8' });
306
- const rl = createInterface({ input: stream, crlfDelay: Infinity });
307
-
308
- for await (const line of rl) {
309
- if (!line) {
310
- continue;
311
- }
312
-
313
- ringBuffer.push(line);
314
- if (ringBuffer.length > maxLines) {
315
- ringBuffer.shift();
316
- }
317
- }
318
-
319
- return {
320
- path: outputPath,
321
- content: ringBuffer.join('\n'),
322
- linesReturned: ringBuffer.length,
323
- };
324
- }
325
-
326
- async readBeanFile(relativePath: string): Promise<{ path: string; content: string }> {
327
- const absolutePath = this.resolveBeanFilePath(relativePath);
328
- const content = await readFile(absolutePath, 'utf8');
329
- return { path: absolutePath, content };
330
- }
331
-
332
- async editBeanFile(relativePath: string, content: string): Promise<{ path: string; bytes: number }> {
333
- const absolutePath = this.resolveBeanFilePath(relativePath);
334
- await mkdir(dirname(absolutePath), { recursive: true });
335
- await writeFile(absolutePath, content, 'utf8');
336
- return { path: absolutePath, bytes: Buffer.byteLength(content, 'utf8') };
337
- }
338
-
339
- async createBeanFile(
340
- relativePath: string,
341
- content: string,
342
- options?: { overwrite?: boolean },
343
- ): Promise<{ path: string; bytes: number; created: boolean }> {
344
- const absolutePath = this.resolveBeanFilePath(relativePath);
345
- await mkdir(dirname(absolutePath), { recursive: true });
346
-
347
- await writeFile(absolutePath, content, {
348
- encoding: 'utf8',
349
- flag: options?.overwrite ? 'w' : 'wx',
350
- });
351
-
352
- return {
353
- path: absolutePath,
354
- bytes: Buffer.byteLength(content, 'utf8'),
355
- created: true,
356
- };
357
- }
358
-
359
- async deleteBeanFile(relativePath: string): Promise<{ path: string; deleted: boolean }> {
360
- const absolutePath = this.resolveBeanFilePath(relativePath);
361
- await rm(absolutePath, { force: false });
362
- return { path: absolutePath, deleted: true };
363
- }
364
- }