@selfagency/beans-mcp 0.1.3 → 0.1.4
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/{dist/beans-mcp-server.cjs → beans-mcp-server.cjs} +2 -1
- package/{dist/index.cjs → index.cjs} +2 -1
- package/{dist/index.js → index.js} +2 -1
- package/package.json +28 -64
- package/.beans.yml +0 -6
- package/.claude/settings.local.json +0 -18
- package/.editorconfig +0 -13
- package/.github/dependabot.yml +0 -11
- package/.github/workflows/release.yml +0 -235
- package/.github/workflows/test.yml +0 -84
- package/.husky/pre-commit +0 -1
- package/.nvmrc +0 -1
- package/.oxfmtrc.json +0 -11
- package/.oxlintrc.json +0 -37
- package/.vscode/settings.json +0 -3
- package/CHANGELOG.md +0 -160
- package/CONTRIBUTING.md +0 -139
- package/LICENSE.txt +0 -21
- package/codeql/codeql-custom-queries-actions/README.md +0 -14
- package/codeql/codeql-custom-queries-actions/codeql-pack.lock.yml +0 -32
- package/codeql/codeql-custom-queries-actions/codeql-pack.yml +0 -7
- package/codeql/codeql-custom-queries-actions/qlpack.yml +0 -6
- package/codeql/codeql-custom-queries-actions/queries/github-script-without-tojson.ql +0 -18
- package/codeql/codeql-custom-queries-actions/queries/strict-external-action-pinning.ql +0 -18
- package/codeql/codeql-custom-queries-javascript/README.md +0 -14
- package/codeql/codeql-custom-queries-javascript/codeql-pack.lock.yml +0 -30
- package/codeql/codeql-custom-queries-javascript/codeql-pack.yml +0 -7
- package/codeql/codeql-custom-queries-javascript/qlpack.yml +0 -6
- package/codeql/codeql-custom-queries-javascript/queries/child-process-shell-apis.ql +0 -26
- package/codeql/codeql-custom-queries-javascript/queries/innerhtml-assignment.ql +0 -24
- package/dist/README.md +0 -307
- package/dist/beans-mcp-server.cjs.map +0 -1
- package/dist/index.cjs.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/package.json +0 -43
- package/pnpm-workspace.yaml +0 -2
- package/scripts/release.js +0 -433
- package/scripts/write-dist-package.js +0 -53
- package/src/cli.ts +0 -14
- package/src/index.ts +0 -21
- package/src/internal/graphql.ts +0 -33
- package/src/internal/queryHelpers.ts +0 -157
- package/src/server/BeansMcpServer.ts +0 -623
- package/src/server/backend.ts +0 -364
- package/src/test/BeansMcpServer.test.ts +0 -514
- package/src/test/handlers.unit.test.ts +0 -201
- package/src/test/parseCliArgs.test.ts +0 -69
- package/src/test/protocol.e2e.test.ts +0 -884
- package/src/test/queryHelpers.test.ts +0 -524
- package/src/test/startBeansMcpServer.test.ts +0 -146
- package/src/test/tools-integration.test.ts +0 -912
- package/src/test/utils.test.ts +0 -81
- package/src/types.ts +0 -46
- package/src/utils.ts +0 -20
- package/tsconfig.json +0 -24
- package/tsup.config.ts +0 -42
- package/vitest.config.ts +0 -18
- /package/{dist/index.d.ts → index.d.ts} +0 -0
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
beanFileHandler,
|
|
4
|
-
createHandler,
|
|
5
|
-
deleteHandler,
|
|
6
|
-
editHandler,
|
|
7
|
-
getBeanById,
|
|
8
|
-
initHandler,
|
|
9
|
-
outputHandler,
|
|
10
|
-
queryHandler,
|
|
11
|
-
reopenHandler,
|
|
12
|
-
updateHandler,
|
|
13
|
-
viewHandler,
|
|
14
|
-
} from '../server/BeansMcpServer';
|
|
15
|
-
|
|
16
|
-
const sampleBean = {
|
|
17
|
-
id: 'b1',
|
|
18
|
-
slug: 'b1',
|
|
19
|
-
path: '.beans/b1.md',
|
|
20
|
-
title: 'B1',
|
|
21
|
-
body: 'body',
|
|
22
|
-
status: 'completed',
|
|
23
|
-
type: 'task',
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
function makeBackend(overrides: Partial<any> = {}) {
|
|
27
|
-
return {
|
|
28
|
-
list: vi.fn(async () => [sampleBean, { ...sampleBean, id: 'b2', status: 'draft' }]),
|
|
29
|
-
init: vi.fn(async (p?: string) => ({ ok: true, prefix: p })),
|
|
30
|
-
create: vi.fn(async (input: any) => ({
|
|
31
|
-
...sampleBean,
|
|
32
|
-
...input,
|
|
33
|
-
id: 'new',
|
|
34
|
-
})),
|
|
35
|
-
update: vi.fn(async (id: string, updates: any) => ({
|
|
36
|
-
...sampleBean,
|
|
37
|
-
id,
|
|
38
|
-
...updates,
|
|
39
|
-
})),
|
|
40
|
-
delete: vi.fn(async (id: string) => ({ ok: true, id })),
|
|
41
|
-
openConfig: vi.fn(async () => ({ configPath: '.beans.yml', content: 'x' })),
|
|
42
|
-
graphqlSchema: vi.fn(async () => ''),
|
|
43
|
-
readOutputLog: vi.fn(async ({ lines }: any) => ({
|
|
44
|
-
path: 'p',
|
|
45
|
-
content: 'log',
|
|
46
|
-
linesReturned: lines ?? 0,
|
|
47
|
-
})),
|
|
48
|
-
readBeanFile: vi.fn(async (path: string) => ({ path, content: 'x' })),
|
|
49
|
-
editBeanFile: vi.fn(async (path: string, content: string) => ({
|
|
50
|
-
path,
|
|
51
|
-
bytes: Buffer.byteLength(content, 'utf8'),
|
|
52
|
-
})),
|
|
53
|
-
createBeanFile: vi.fn(async (path: string, content: string, opts: any) => ({
|
|
54
|
-
path,
|
|
55
|
-
bytes: Buffer.byteLength(content, 'utf8'),
|
|
56
|
-
created: true,
|
|
57
|
-
})),
|
|
58
|
-
deleteBeanFile: vi.fn(async (path: string) => ({ path, deleted: true })),
|
|
59
|
-
...overrides,
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
describe('Handlers (unit)', () => {
|
|
64
|
-
it('getBeanById returns bean when found', async () => {
|
|
65
|
-
const backend = makeBackend();
|
|
66
|
-
const b = await getBeanById(backend, 'b1');
|
|
67
|
-
expect(b.id).toBe('b1');
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('getBeanById throws when not found', async () => {
|
|
71
|
-
const backend = makeBackend({ list: vi.fn(async () => []) });
|
|
72
|
-
await expect(getBeanById(backend, 'missing')).rejects.toThrow(/Bean not found/);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('initHandler calls backend.init and wraps result', async () => {
|
|
76
|
-
const backend = makeBackend();
|
|
77
|
-
const res = await initHandler(backend)({ prefix: 'pfx' });
|
|
78
|
-
expect(backend.init).toHaveBeenCalledWith('pfx');
|
|
79
|
-
const data = JSON.parse(res.content?.[0]?.text ?? '{}');
|
|
80
|
-
expect(data).toBeDefined();
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('viewHandler returns bean structured content', async () => {
|
|
84
|
-
const backend = makeBackend();
|
|
85
|
-
const res = await viewHandler(backend)({ beanId: 'b1' });
|
|
86
|
-
const data = JSON.parse(res.content?.[0]?.text ?? '{}');
|
|
87
|
-
expect(data.bean.id).toBe('b1');
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('createHandler delegates to backend.create', async () => {
|
|
91
|
-
const backend = makeBackend();
|
|
92
|
-
const res = await createHandler(backend)({ title: 'T', type: 't' });
|
|
93
|
-
expect(backend.create).toHaveBeenCalled();
|
|
94
|
-
const data = JSON.parse(res.content?.[0]?.text ?? '{}');
|
|
95
|
-
expect(data.bean.id).toBe('new');
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('editHandler delegates to backend.update', async () => {
|
|
99
|
-
const backend = makeBackend();
|
|
100
|
-
const res = await editHandler(backend)({ beanId: 'b1', status: 'todo' });
|
|
101
|
-
expect(backend.update).toHaveBeenCalledWith('b1', { status: 'todo' });
|
|
102
|
-
const data = JSON.parse(res.content?.[0]?.text ?? '{}');
|
|
103
|
-
expect(data.bean.status).toBe('todo');
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('updateHandler delegates body updates to backend.update', async () => {
|
|
107
|
-
const backend = makeBackend({
|
|
108
|
-
update: vi.fn(async (id: string, updates: any) => ({
|
|
109
|
-
...sampleBean,
|
|
110
|
-
id,
|
|
111
|
-
...updates,
|
|
112
|
-
})),
|
|
113
|
-
});
|
|
114
|
-
const res = await updateHandler(backend)({ beanId: 'b1', body: 'new body text' } as any);
|
|
115
|
-
expect(backend.update).toHaveBeenCalledWith('b1', expect.objectContaining({ body: 'new body text' }));
|
|
116
|
-
const data = JSON.parse(res.content?.[0]?.text ?? '{}');
|
|
117
|
-
expect(data.bean.body).toBe('new body text');
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it('reopenHandler throws if current status mismatches', async () => {
|
|
121
|
-
const backend = makeBackend();
|
|
122
|
-
await expect(
|
|
123
|
-
reopenHandler(backend)({
|
|
124
|
-
beanId: 'b1',
|
|
125
|
-
requiredCurrentStatus: 'scrapped',
|
|
126
|
-
targetStatus: 'todo',
|
|
127
|
-
}),
|
|
128
|
-
).rejects.toThrow(/is not scrapped/);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it('reopenHandler updates when status matches', async () => {
|
|
132
|
-
const backend = makeBackend();
|
|
133
|
-
const res = await reopenHandler(backend)({
|
|
134
|
-
beanId: 'b1',
|
|
135
|
-
requiredCurrentStatus: 'completed',
|
|
136
|
-
targetStatus: 'todo',
|
|
137
|
-
});
|
|
138
|
-
expect(backend.update).toHaveBeenCalled();
|
|
139
|
-
const data = JSON.parse(res.content?.[0]?.text ?? '{}');
|
|
140
|
-
expect(data.bean.status).toBe('todo');
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it('deleteHandler enforces draft/scrapped unless force', async () => {
|
|
144
|
-
const backend = makeBackend();
|
|
145
|
-
await expect(deleteHandler(backend)({ beanId: 'b1', force: false })).rejects.toThrow(
|
|
146
|
-
/Only draft and scrapped beans are deletable/,
|
|
147
|
-
);
|
|
148
|
-
const res = await deleteHandler(backend)({ beanId: 'b1', force: true });
|
|
149
|
-
expect(backend.delete).toHaveBeenCalledWith('b1');
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it('beanFileHandler routes operations', async () => {
|
|
153
|
-
const backend = makeBackend();
|
|
154
|
-
const _read = await beanFileHandler(backend)({
|
|
155
|
-
operation: 'read',
|
|
156
|
-
path: 'p',
|
|
157
|
-
});
|
|
158
|
-
expect(backend.readBeanFile).toHaveBeenCalledWith('p');
|
|
159
|
-
const _edit = await beanFileHandler(backend)({
|
|
160
|
-
operation: 'edit',
|
|
161
|
-
path: 'p',
|
|
162
|
-
content: 'c',
|
|
163
|
-
});
|
|
164
|
-
expect(backend.editBeanFile).toHaveBeenCalledWith('p', 'c');
|
|
165
|
-
const _create = await beanFileHandler(backend)({
|
|
166
|
-
operation: 'create',
|
|
167
|
-
path: 'p',
|
|
168
|
-
content: 'c',
|
|
169
|
-
overwrite: true,
|
|
170
|
-
});
|
|
171
|
-
expect(backend.createBeanFile).toHaveBeenCalled();
|
|
172
|
-
const _del = await beanFileHandler(backend)({
|
|
173
|
-
operation: 'delete',
|
|
174
|
-
path: 'p',
|
|
175
|
-
});
|
|
176
|
-
expect(backend.deleteBeanFile).toHaveBeenCalledWith('p');
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it('beanFileHandler throws on unsupported operation', async () => {
|
|
180
|
-
const backend = makeBackend();
|
|
181
|
-
await expect(beanFileHandler(backend)({ operation: 'noop' as 'read', path: 'p' })).rejects.toThrow(
|
|
182
|
-
'Unsupported operation',
|
|
183
|
-
);
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it('outputHandler read and show', async () => {
|
|
187
|
-
const backend = makeBackend();
|
|
188
|
-
const _r = await outputHandler(backend)({ operation: 'read', lines: 10 });
|
|
189
|
-
expect(backend.readOutputLog).toHaveBeenCalled();
|
|
190
|
-
const s = await outputHandler(backend)({ operation: 'show' });
|
|
191
|
-
const data = JSON.parse(s.content?.[0]?.text ?? '{}');
|
|
192
|
-
expect(data.message).toMatch(/When using VS Code UI/);
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it('queryHandler delegates to handleQueryOperation', async () => {
|
|
196
|
-
const backend = makeBackend();
|
|
197
|
-
const res = await queryHandler(backend)({ operation: 'refresh' });
|
|
198
|
-
// handleQueryOperation returns value directly; ensure promise resolves
|
|
199
|
-
expect(res).toBeDefined();
|
|
200
|
-
});
|
|
201
|
-
});
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { parseCliArgs } from '../server/BeansMcpServer';
|
|
3
|
-
|
|
4
|
-
describe('parseCliArgs', () => {
|
|
5
|
-
it('should parse workspace root positional argument', () => {
|
|
6
|
-
const result = parseCliArgs(['/workspace']);
|
|
7
|
-
expect(result.workspaceRoot).toBe('/workspace');
|
|
8
|
-
expect(result.cliPath).toBe('beans');
|
|
9
|
-
expect(result.workspaceExplicit).toBe(true);
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it('should parse --workspace-root flag', () => {
|
|
13
|
-
const result = parseCliArgs(['--workspace-root', '/workspace']);
|
|
14
|
-
expect(result.workspaceRoot).toBe('/workspace');
|
|
15
|
-
expect(result.workspaceExplicit).toBe(true);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it('should mark workspaceExplicit false when no workspace arg given', () => {
|
|
19
|
-
const result = parseCliArgs([]);
|
|
20
|
-
expect(result.workspaceExplicit).toBe(false);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('should parse --cli-path flag', () => {
|
|
24
|
-
const result = parseCliArgs(['--cli-path', '/usr/local/bin/beans']);
|
|
25
|
-
expect(result.cliPath).toBe('/usr/local/bin/beans');
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('should parse --port flag', () => {
|
|
29
|
-
const result = parseCliArgs(['--port', '8080']);
|
|
30
|
-
expect(result.port).toBe(8080);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('should parse --log-dir flag', () => {
|
|
34
|
-
const result = parseCliArgs(['--log-dir', '/tmp/logs']);
|
|
35
|
-
expect(result.logDir).toBe('/tmp/logs');
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('should reject suspicious CLI paths', () => {
|
|
39
|
-
expect(() => {
|
|
40
|
-
parseCliArgs(['--cli-path', 'beans; rm -rf /']);
|
|
41
|
-
}).toThrow('Invalid CLI path');
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
describe('--help / -h', () => {
|
|
45
|
-
afterEach(() => {
|
|
46
|
-
vi.restoreAllMocks();
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('--help writes help text to stdout and exits with 0', () => {
|
|
50
|
-
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
51
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => {}) as (code?: number) => never);
|
|
52
|
-
|
|
53
|
-
parseCliArgs(['--help']);
|
|
54
|
-
|
|
55
|
-
expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('Usage'));
|
|
56
|
-
expect(exitSpy).toHaveBeenCalledWith(0);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('-h writes help text to stdout and exits with 0', () => {
|
|
60
|
-
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
61
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => {}) as (code?: number) => never);
|
|
62
|
-
|
|
63
|
-
parseCliArgs(['-h']);
|
|
64
|
-
|
|
65
|
-
expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('Usage'));
|
|
66
|
-
expect(exitSpy).toHaveBeenCalledWith(0);
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
});
|