@biomate/connect 0.1.0

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/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # @biomate/connect
2
+
3
+ > One-line installer that wires BioMate AI into Claude Code, Claude Desktop, Cursor, OpenAI Codex, ChatGPT, and Open Claw (WeChat).
4
+
5
+ Real bioinformatics — `WGS variant-calling pipeline`, CryoSPARC, AlphaFold, ADMET screens, PBPK — runs from your favorite AI coding assistant. No SaaS lock-in: tokens are stored locally and revocable per-surface from your BioMate account.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ # Connect Claude Code
11
+ npx @biomate/connect claude-code
12
+
13
+ # Connect Cursor
14
+ npx @biomate/connect cursor
15
+
16
+ # Connect Codex CLI
17
+ npx @biomate/connect codex
18
+ ```
19
+
20
+ That's it. The CLI opens your browser, runs OAuth 2.1 + PKCE against `https://biomate.ai`, writes the MCP server entry into the right host config file, and stashes a refresh token in your OS keychain.
21
+
22
+ ## Supported surfaces
23
+
24
+ | Surface | Install command | Config file written |
25
+ |---|---|---|
26
+ | Claude Code | `npx @biomate/connect claude-code` | `~/.claude.json` |
27
+ | Claude Desktop | `npx @biomate/connect claude-desktop` | `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) |
28
+ | Cursor | `npx @biomate/connect cursor` | `~/.cursor/mcp.json` |
29
+ | Codex CLI | `npx @biomate/connect codex` | `~/.codex/config.toml` |
30
+ | ChatGPT | `npx @biomate/connect chatgpt` | (Custom GPT — instructions printed) |
31
+ | Open Claw | `npx @biomate/connect open-claw` | (hosted webhook — account linked) |
32
+
33
+ ## What you get
34
+
35
+ After connecting, your AI assistant has access to BioMate's 14-tool MCP surface:
36
+
37
+ - **`biomate_session`** — natural-language goal → streaming workflow run with phase/step/QC/findings events
38
+ - **`search_workflow`** + **`get_workflow_spec`** — discover from 2,455 indexed workflows
39
+ - **`run_workflow`** + **`get_run`** + **`cancel_run`** — fine control
40
+ - **`preview_file`**, **`export_report`**, **`recall_memory`**, **`upload_file`**, **`analyze_results`**, **`explain_error`**, **`query_database`**
41
+
42
+ ## Try it
43
+
44
+ Open your assistant and paste:
45
+
46
+ ```
47
+ Screen aspirin and caffeine for hERG and CYP3A4 inhibition.
48
+ ```
49
+
50
+ ```
51
+ Run RNA-seq differential expression on these FASTQs:
52
+ s3://biomate-demo/sample_R1.fq.gz, s3://biomate-demo/sample_R2.fq.gz
53
+ Compare treated vs control.
54
+ ```
55
+
56
+ ```
57
+ Look up UniProt P04637.
58
+ ```
59
+
60
+ ## Privacy & security
61
+
62
+ - OAuth 2.1 + PKCE; tokens scoped per surface, revocable individually at
63
+ https://biomate.ai/account/connectors
64
+ - Refresh tokens hashed at rest with HMAC-SHA256
65
+ - Access tokens are 30-minute JWTs; refresh tokens rotate on use (OAuth 2.1 §6.1)
66
+ - No telemetry beyond standard API logs
67
+
68
+ ## Development
69
+
70
+ ```bash
71
+ npm install
72
+ npm run build
73
+ npm test
74
+ ```
75
+
76
+ ## License
77
+
78
+ MIT
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const vitest_1 = require("vitest");
37
+ const fs_1 = require("fs");
38
+ const os = __importStar(require("os"));
39
+ const path = __importStar(require("path"));
40
+ const configWriters_js_1 = require("../configWriters.js");
41
+ async function tmpDir() {
42
+ return fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'biomate-test-'));
43
+ }
44
+ (0, vitest_1.describe)('upsertJSON', () => {
45
+ (0, vitest_1.it)('creates new file with content', async () => {
46
+ const dir = await tmpDir();
47
+ const f = path.join(dir, 'a', 'b', 'cfg.json');
48
+ await (0, configWriters_js_1.upsertJSON)(f, (cfg) => {
49
+ cfg.mcpServers = { biomate: { command: 'npx', args: [] } };
50
+ return cfg;
51
+ });
52
+ const got = JSON.parse(await fs_1.promises.readFile(f, 'utf8'));
53
+ (0, vitest_1.expect)(got.mcpServers.biomate.command).toBe('npx');
54
+ });
55
+ (0, vitest_1.it)('preserves existing keys', async () => {
56
+ const dir = await tmpDir();
57
+ const f = path.join(dir, 'cfg.json');
58
+ await fs_1.promises.writeFile(f, JSON.stringify({ theme: 'dark', mcpServers: { other: { command: 'foo' } } }));
59
+ await (0, configWriters_js_1.upsertJSON)(f, (cfg) => {
60
+ cfg.mcpServers.biomate = { command: 'npx' };
61
+ return cfg;
62
+ });
63
+ const got = JSON.parse(await fs_1.promises.readFile(f, 'utf8'));
64
+ (0, vitest_1.expect)(got.theme).toBe('dark');
65
+ (0, vitest_1.expect)(got.mcpServers.other.command).toBe('foo');
66
+ (0, vitest_1.expect)(got.mcpServers.biomate.command).toBe('npx');
67
+ });
68
+ });
69
+ (0, vitest_1.describe)('upsertTOML', () => {
70
+ (0, vitest_1.it)('appends a new biomate block', async () => {
71
+ const dir = await tmpDir();
72
+ const f = path.join(dir, 'config.toml');
73
+ await fs_1.promises.writeFile(f, '[other]\nfoo = "bar"\n');
74
+ await (0, configWriters_js_1.upsertTOML)(f, '\n[mcp_servers.biomate]\ncommand = "npx"\n');
75
+ const got = await fs_1.promises.readFile(f, 'utf8');
76
+ (0, vitest_1.expect)(got).toContain('[other]');
77
+ (0, vitest_1.expect)(got).toContain('[mcp_servers.biomate]');
78
+ (0, vitest_1.expect)(got).toContain('command = "npx"');
79
+ });
80
+ (0, vitest_1.it)('replaces existing biomate block on re-install', async () => {
81
+ const dir = await tmpDir();
82
+ const f = path.join(dir, 'config.toml');
83
+ await (0, configWriters_js_1.upsertTOML)(f, '\n[mcp_servers.biomate]\ntoken = "old"\n');
84
+ await (0, configWriters_js_1.upsertTOML)(f, '\n[mcp_servers.biomate]\ntoken = "new"\n');
85
+ const got = await fs_1.promises.readFile(f, 'utf8');
86
+ (0, vitest_1.expect)(got).toContain('token = "new"');
87
+ (0, vitest_1.expect)(got).not.toContain('token = "old"');
88
+ });
89
+ });
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Installer end-to-end flow tests.
3
+ *
4
+ * Covers TEST_PLAN.md §3.2 (12 cases). Each test spins up a mock OAuth
5
+ * server (Node http) that mirrors api.biomate.ai's /oauth/authorize and
6
+ * /oauth/token endpoints, then drives the installer against a tmp HOME so
7
+ * file writes don't touch the developer's real config.
8
+ */
9
+ export {};
@@ -0,0 +1,291 @@
1
+ "use strict";
2
+ /**
3
+ * Installer end-to-end flow tests.
4
+ *
5
+ * Covers TEST_PLAN.md §3.2 (12 cases). Each test spins up a mock OAuth
6
+ * server (Node http) that mirrors api.biomate.ai's /oauth/authorize and
7
+ * /oauth/token endpoints, then drives the installer against a tmp HOME so
8
+ * file writes don't touch the developer's real config.
9
+ */
10
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ var desc = Object.getOwnPropertyDescriptor(m, k);
13
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
14
+ desc = { enumerable: true, get: function() { return m[k]; } };
15
+ }
16
+ Object.defineProperty(o, k2, desc);
17
+ }) : (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ o[k2] = m[k];
20
+ }));
21
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
22
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
23
+ }) : function(o, v) {
24
+ o["default"] = v;
25
+ });
26
+ var __importStar = (this && this.__importStar) || (function () {
27
+ var ownKeys = function(o) {
28
+ ownKeys = Object.getOwnPropertyNames || function (o) {
29
+ var ar = [];
30
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
31
+ return ar;
32
+ };
33
+ return ownKeys(o);
34
+ };
35
+ return function (mod) {
36
+ if (mod && mod.__esModule) return mod;
37
+ var result = {};
38
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
39
+ __setModuleDefault(result, mod);
40
+ return result;
41
+ };
42
+ })();
43
+ Object.defineProperty(exports, "__esModule", { value: true });
44
+ const vitest_1 = require("vitest");
45
+ const fs_1 = require("fs");
46
+ const os = __importStar(require("os"));
47
+ const path = __importStar(require("path"));
48
+ // os.homedir() in Node is non-configurable; we can't replace it directly.
49
+ // Instead, mock the entire 'os' module so surfaces.ts gets our stub.
50
+ let __testHome = process.env.HOME ?? '/tmp';
51
+ vitest_1.vi.mock('os', async () => {
52
+ const actual = await vitest_1.vi.importActual('os');
53
+ return { ...actual, homedir: () => __testHome };
54
+ });
55
+ const http_1 = require("http");
56
+ const url_1 = require("url");
57
+ // Hold the per-test handler that the mocked `open` will call.
58
+ let __openHandler = null;
59
+ vitest_1.vi.mock('open', () => ({
60
+ default: async (url) => {
61
+ if (__openHandler)
62
+ await __openHandler(typeof url === 'string' ? url : url.toString());
63
+ return { unref: () => undefined };
64
+ },
65
+ }));
66
+ const oauth_js_1 = require("../oauth.js");
67
+ const surfaces_js_1 = require("../surfaces.js");
68
+ // Assign each test a unique port to avoid TCP TIME_WAIT collisions across tests.
69
+ let __portCursor = 60000;
70
+ function uniquePort() {
71
+ return __portCursor++;
72
+ }
73
+ function withPort(s) {
74
+ return { ...s, callbackPort: uniquePort() };
75
+ }
76
+ function startMockServer() {
77
+ const behavior = {};
78
+ let lastVerifier = null;
79
+ let issuedCodes = new Map();
80
+ const server = (0, http_1.createServer)(async (req, res) => {
81
+ const u = new url_1.URL(req.url ?? '/', `http://${req.headers.host}`);
82
+ if (u.pathname === '/oauth/authorize' && req.method === 'GET') {
83
+ // The installer never hits /oauth/authorize as an HTTP client — its
84
+ // browser does. We simulate by auto-redirecting on a different path.
85
+ res.writeHead(200);
86
+ res.end('ok');
87
+ return;
88
+ }
89
+ if (u.pathname === '/simulate-callback' && req.method === 'GET') {
90
+ // Test helper: forwards a code (or error) to the installer's callback port.
91
+ const code = u.searchParams.get('code');
92
+ const error = u.searchParams.get('error');
93
+ const state = u.searchParams.get('state');
94
+ const port = u.searchParams.get('port');
95
+ const params = new URLSearchParams();
96
+ if (code)
97
+ params.set('code', code);
98
+ if (error)
99
+ params.set('error', error);
100
+ if (state)
101
+ params.set('state', state);
102
+ const cbUrl = `http://127.0.0.1:${port}/callback?${params.toString()}`;
103
+ await fetch(cbUrl).catch(() => { });
104
+ res.writeHead(200);
105
+ res.end('forwarded');
106
+ return;
107
+ }
108
+ if (u.pathname === '/oauth/token' && req.method === 'POST') {
109
+ const body = await readBody(req);
110
+ const form = new URLSearchParams(body);
111
+ lastVerifier = form.get('code_verifier');
112
+ const status = behavior.tokenStatus ?? 200;
113
+ res.writeHead(status, { 'content-type': 'application/json' });
114
+ if (status !== 200) {
115
+ res.end(JSON.stringify({ error: 'invalid_grant', error_description: 'mock' }));
116
+ return;
117
+ }
118
+ res.end(JSON.stringify({
119
+ access_token: 'mock_access_token',
120
+ refresh_token: 'mock_refresh_token',
121
+ token_type: 'Bearer',
122
+ expires_in: 1800,
123
+ scope: form.get('scope') ?? 'runs:read runs:write',
124
+ }));
125
+ return;
126
+ }
127
+ res.writeHead(404);
128
+ res.end();
129
+ });
130
+ return new Promise((resolve) => {
131
+ server.listen(0, '127.0.0.1', () => {
132
+ const addr = server.address();
133
+ const port = typeof addr === 'object' && addr ? addr.port : 0;
134
+ resolve({
135
+ url: `http://127.0.0.1:${port}`,
136
+ close: () => new Promise((r) => server.close(() => r(undefined))),
137
+ lastVerifier: () => lastVerifier,
138
+ behavior,
139
+ });
140
+ });
141
+ });
142
+ }
143
+ function readBody(req) {
144
+ return new Promise((resolve, reject) => {
145
+ const chunks = [];
146
+ req.on('data', (c) => chunks.push(c));
147
+ req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
148
+ req.on('error', reject);
149
+ });
150
+ }
151
+ /**
152
+ * Replace `open` with a function that, instead of launching a browser, hits
153
+ * /simulate-callback on the mock server with the right code + state.
154
+ */
155
+ function installOpenStub(mockUrl, port, opts) {
156
+ __openHandler = async (authUrl) => {
157
+ const u = new url_1.URL(authUrl);
158
+ const state = opts.stateOverride ?? (u.searchParams.get('state') ?? '');
159
+ const params = new URLSearchParams({ port: String(port) });
160
+ if (opts.error)
161
+ params.set('error', opts.error);
162
+ else
163
+ params.set('code', 'mock_code_' + Math.random().toString(36).slice(2));
164
+ params.set('state', state);
165
+ await fetch(`${mockUrl}/simulate-callback?${params.toString()}`);
166
+ };
167
+ }
168
+ (0, vitest_1.describe)('installer end-to-end flow', () => {
169
+ let mock;
170
+ let tmpHome;
171
+ let origHome;
172
+ (0, vitest_1.beforeEach)(async () => {
173
+ mock = await startMockServer();
174
+ tmpHome = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'biomate-flow-'));
175
+ origHome = process.env.HOME;
176
+ process.env.HOME = tmpHome;
177
+ __testHome = tmpHome;
178
+ process.env.BIOMATE_API_BASE = 'https://mock.api.example';
179
+ });
180
+ (0, vitest_1.afterEach)(async () => {
181
+ __openHandler = null;
182
+ await mock.close();
183
+ if (origHome)
184
+ process.env.HOME = origHome;
185
+ else
186
+ delete process.env.HOME;
187
+ await fs_1.promises.rm(tmpHome, { recursive: true, force: true });
188
+ });
189
+ // --- §3.2.1, §3.2.2, §3.2.3, §3.2.4, §3.2.5, §3.2.6, §3.2.7 — per-surface ---
190
+ (0, vitest_1.it)('§3.2.1 claude-code: writes ~/.claude.json with mcpServers.biomate on fresh machine', async () => {
191
+ const surface = withPort(surfaces_js_1.SURFACES['claude-code']);
192
+ installOpenStub(mock.url, surface.callbackPort, {});
193
+ const tokens = await (0, oauth_js_1.runPKCEFlow)(surface, mock.url);
194
+ await surface.install(tokens.access_token, tokens.refresh_token);
195
+ const cfg = JSON.parse(await fs_1.promises.readFile(path.join(tmpHome, '.claude.json'), 'utf8'));
196
+ (0, vitest_1.expect)(cfg.mcpServers.biomate.command).toBe('npx');
197
+ (0, vitest_1.expect)(cfg.mcpServers.biomate.env.BIOMATE_REFRESH_TOKEN).toBe('mock_refresh_token');
198
+ });
199
+ (0, vitest_1.it)('§3.2.2 claude-code: preserves existing keys in ~/.claude.json', async () => {
200
+ await fs_1.promises.writeFile(path.join(tmpHome, '.claude.json'), JSON.stringify({ theme: 'dark', mcpServers: { other: { command: 'foo' } } }));
201
+ const surface = withPort(surfaces_js_1.SURFACES['claude-code']);
202
+ installOpenStub(mock.url, surface.callbackPort, {});
203
+ const tokens = await (0, oauth_js_1.runPKCEFlow)(surface, mock.url);
204
+ await surface.install(tokens.access_token, tokens.refresh_token);
205
+ const cfg = JSON.parse(await fs_1.promises.readFile(path.join(tmpHome, '.claude.json'), 'utf8'));
206
+ (0, vitest_1.expect)(cfg.theme).toBe('dark');
207
+ (0, vitest_1.expect)(cfg.mcpServers.other.command).toBe('foo');
208
+ (0, vitest_1.expect)(cfg.mcpServers.biomate.command).toBe('npx');
209
+ });
210
+ (0, vitest_1.it)('§3.2.3 claude-code: re-install is idempotent (no duplicate biomate entry)', async () => {
211
+ const surface1 = withPort(surfaces_js_1.SURFACES['claude-code']);
212
+ installOpenStub(mock.url, surface1.callbackPort, {});
213
+ let tokens = await (0, oauth_js_1.runPKCEFlow)(surface1, mock.url);
214
+ await surface1.install(tokens.access_token, tokens.refresh_token);
215
+ __openHandler = null;
216
+ const surface2 = withPort(surfaces_js_1.SURFACES['claude-code']); // fresh port
217
+ installOpenStub(mock.url, surface2.callbackPort, {});
218
+ tokens = await (0, oauth_js_1.runPKCEFlow)(surface2, mock.url);
219
+ await surface2.install(tokens.access_token, tokens.refresh_token);
220
+ const cfg = JSON.parse(await fs_1.promises.readFile(path.join(tmpHome, '.claude.json'), 'utf8'));
221
+ // Only one `biomate` key — JSON objects can't have duplicates anyway, but
222
+ // we assert structure is unchanged after second install.
223
+ (0, vitest_1.expect)(Object.keys(cfg.mcpServers)).toEqual(['biomate']);
224
+ });
225
+ (0, vitest_1.it)('§3.2.4 cursor: writes ~/.cursor/mcp.json', async () => {
226
+ const surface = withPort(surfaces_js_1.SURFACES['cursor']);
227
+ installOpenStub(mock.url, surface.callbackPort, {});
228
+ const tokens = await (0, oauth_js_1.runPKCEFlow)(surface, mock.url);
229
+ await surface.install(tokens.access_token, tokens.refresh_token);
230
+ const cfg = JSON.parse(await fs_1.promises.readFile(path.join(tmpHome, '.cursor', 'mcp.json'), 'utf8'));
231
+ (0, vitest_1.expect)(cfg.mcpServers.biomate).toBeDefined();
232
+ });
233
+ (0, vitest_1.it)('§3.2.5 codex: writes [mcp_servers.biomate] to ~/.codex/config.toml', async () => {
234
+ const surface = withPort(surfaces_js_1.SURFACES['codex']);
235
+ installOpenStub(mock.url, surface.callbackPort, {});
236
+ const tokens = await (0, oauth_js_1.runPKCEFlow)(surface, mock.url);
237
+ await surface.install(tokens.access_token, tokens.refresh_token);
238
+ const cfg = await fs_1.promises.readFile(path.join(tmpHome, '.codex', 'config.toml'), 'utf8');
239
+ (0, vitest_1.expect)(cfg).toContain('[mcp_servers.biomate]');
240
+ (0, vitest_1.expect)(cfg).toContain('mock_refresh_token');
241
+ });
242
+ (0, vitest_1.it)('§3.2.6 chatgpt: prints instructions, writes no file', async () => {
243
+ const surface = surfaces_js_1.SURFACES['chatgpt'];
244
+ const logSpy = vitest_1.vi.spyOn(console, 'log').mockImplementation(() => undefined);
245
+ const result = await surface.install('access', 'refresh');
246
+ (0, vitest_1.expect)(result).toMatch(/ChatGPT/);
247
+ // No .chatgpt-style files in $HOME.
248
+ const entries = await fs_1.promises.readdir(tmpHome);
249
+ (0, vitest_1.expect)(entries).toEqual([]);
250
+ logSpy.mockRestore();
251
+ });
252
+ (0, vitest_1.it)('§3.2.7 open-claw: prints WeChat instructions, writes no file', async () => {
253
+ const surface = surfaces_js_1.SURFACES['open-claw'];
254
+ const logSpy = vitest_1.vi.spyOn(console, 'log').mockImplementation(() => undefined);
255
+ const result = await surface.install('access', 'refresh');
256
+ (0, vitest_1.expect)(result).toBe('(linked)');
257
+ const entries = await fs_1.promises.readdir(tmpHome);
258
+ (0, vitest_1.expect)(entries).toEqual([]);
259
+ logSpy.mockRestore();
260
+ });
261
+ // --- §3.2.9–§3.2.12 — failure modes ---
262
+ (0, vitest_1.it)('§3.2.10 OAuth callback error=access_denied surfaces cleanly', async () => {
263
+ const surface = withPort(surfaces_js_1.SURFACES['cursor']);
264
+ installOpenStub(mock.url, surface.callbackPort, { error: 'access_denied' });
265
+ await (0, vitest_1.expect)((0, oauth_js_1.runPKCEFlow)(surface, mock.url)).rejects.toThrow(/access_denied/);
266
+ });
267
+ (0, vitest_1.it)('§3.2.11 state mismatch on callback rejected', async () => {
268
+ const surface = withPort(surfaces_js_1.SURFACES['cursor']);
269
+ installOpenStub(mock.url, surface.callbackPort, { stateOverride: 'wrong-state' });
270
+ await (0, vitest_1.expect)((0, oauth_js_1.runPKCEFlow)(surface, mock.url)).rejects.toThrow(/invalid callback/);
271
+ });
272
+ (0, vitest_1.it)('§3.2.12 token exchange 400 surfaces the server reply', async () => {
273
+ const surface = withPort(surfaces_js_1.SURFACES['cursor']);
274
+ mock.behavior.tokenStatus = 400;
275
+ installOpenStub(mock.url, surface.callbackPort, {});
276
+ await (0, vitest_1.expect)((0, oauth_js_1.runPKCEFlow)(surface, mock.url)).rejects.toThrow(/token exchange failed: 400/);
277
+ });
278
+ // --- §3.2.8 — unknown surface (CLI-level; verified via lookup) ---
279
+ (0, vitest_1.it)('§3.2.8 unknown surface id returns undefined from SURFACES map', () => {
280
+ (0, vitest_1.expect)(surfaces_js_1.SURFACES['not-a-surface']).toBeUndefined();
281
+ });
282
+ // --- bonus: PKCE verifier is forwarded to token endpoint ---
283
+ (0, vitest_1.it)('bonus: code_verifier is presented to /oauth/token', async () => {
284
+ const surface = withPort(surfaces_js_1.SURFACES['cursor']);
285
+ installOpenStub(mock.url, surface.callbackPort, {});
286
+ await (0, oauth_js_1.runPKCEFlow)(surface, mock.url);
287
+ const v = mock.lastVerifier();
288
+ (0, vitest_1.expect)(v).not.toBeNull();
289
+ (0, vitest_1.expect)(v.length).toBeGreaterThanOrEqual(43);
290
+ });
291
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const crypto_1 = require("crypto");
5
+ const pkce_js_1 = require("../pkce.js");
6
+ (0, vitest_1.describe)('PKCE', () => {
7
+ (0, vitest_1.it)('verifier length is in [43, 128]', () => {
8
+ for (let i = 0; i < 20; i++) {
9
+ const p = (0, pkce_js_1.generatePKCE)();
10
+ (0, vitest_1.expect)(p.verifier.length).toBeGreaterThanOrEqual(43);
11
+ (0, vitest_1.expect)(p.verifier.length).toBeLessThanOrEqual(128);
12
+ }
13
+ });
14
+ (0, vitest_1.it)('challenge equals base64url(sha256(verifier))', () => {
15
+ const p = (0, pkce_js_1.generatePKCE)();
16
+ const computed = (0, crypto_1.createHash)('sha256').update(p.verifier).digest('base64')
17
+ .replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
18
+ (0, vitest_1.expect)(p.challenge).toBe(computed);
19
+ });
20
+ (0, vitest_1.it)('uses S256 method', () => {
21
+ (0, vitest_1.expect)((0, pkce_js_1.generatePKCE)().method).toBe('S256');
22
+ });
23
+ (0, vitest_1.it)('verifier and challenge are URL-safe (no +,/,=)', () => {
24
+ const p = (0, pkce_js_1.generatePKCE)();
25
+ (0, vitest_1.expect)(p.verifier).not.toMatch(/[+/=]/);
26
+ (0, vitest_1.expect)(p.challenge).not.toMatch(/[+/=]/);
27
+ });
28
+ });
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ const oauth_js_1 = require("./oauth.js");
38
+ const surfaces_js_1 = require("./surfaces.js");
39
+ const AUTH_BASE = process.env.BIOMATE_AUTH_BASE ?? 'https://api.biomate.ai';
40
+ const USAGE = `\nUsage: npx @biomate/connect <surface> [--auth-base URL]\n\nSurfaces:\n claude-code Anthropic Claude Code (CLI)\n claude-desktop Anthropic Claude Desktop\n cursor Cursor IDE\n codex OpenAI Codex CLI\n chatgpt ChatGPT (Custom GPT Actions)\n open-claw WeChat / Open Claw\n\nExamples:\n npx @biomate/connect claude-code\n npx @biomate/connect cursor\n\nEnvironment:\n BIOMATE_AUTH_BASE Auth server URL (default ${AUTH_BASE})\n BIOMATE_API_BASE API base URL for installed configs\n`;
41
+ async function main() {
42
+ const args = process.argv.slice(2);
43
+ if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
44
+ console.log(USAGE);
45
+ return 0;
46
+ }
47
+ const id = args[0];
48
+ const surface = surfaces_js_1.SURFACES[id];
49
+ if (!surface) {
50
+ console.error(`Unknown surface: ${id}\n${USAGE}`);
51
+ return 1;
52
+ }
53
+ try {
54
+ const tokens = await (0, oauth_js_1.runPKCEFlow)(surface, AUTH_BASE);
55
+ const where = await surface.install(tokens.access_token, tokens.refresh_token);
56
+ // Stash the refresh token in the OS keychain too (best-effort).
57
+ try {
58
+ const keytar = await Promise.resolve().then(() => __importStar(require('keytar')));
59
+ await keytar.default.setPassword('biomate', surface.id, tokens.refresh_token);
60
+ }
61
+ catch {
62
+ /* keychain optional */
63
+ }
64
+ console.log(`\n✓ ${surface.label} connected.`);
65
+ if (where && !where.startsWith('(')) {
66
+ console.log(` Config written: ${where}`);
67
+ }
68
+ console.log(` Scopes granted: ${tokens.scope}`);
69
+ if (id !== 'chatgpt' && id !== 'open-claw') {
70
+ console.log(`\nRestart ${surface.label} to pick up the new MCP server.`);
71
+ }
72
+ return 0;
73
+ }
74
+ catch (err) {
75
+ console.error(`\n✗ Connect failed: ${err.message ?? err}`);
76
+ return 1;
77
+ }
78
+ }
79
+ main().then((c) => process.exit(c));
@@ -0,0 +1,2 @@
1
+ export declare function upsertJSON(filePath: string, mutate: (obj: any) => any): Promise<void>;
2
+ export declare function upsertTOML(filePath: string, append: string): Promise<void>;
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.upsertJSON = upsertJSON;
37
+ exports.upsertTOML = upsertTOML;
38
+ const fs_1 = require("fs");
39
+ const path = __importStar(require("path"));
40
+ async function upsertJSON(filePath, mutate) {
41
+ await fs_1.promises.mkdir(path.dirname(filePath), { recursive: true });
42
+ let existing = {};
43
+ try {
44
+ const raw = await fs_1.promises.readFile(filePath, 'utf8');
45
+ existing = JSON.parse(raw);
46
+ }
47
+ catch (e) {
48
+ if (e.code !== 'ENOENT')
49
+ throw e;
50
+ }
51
+ const next = mutate(existing) ?? existing;
52
+ await atomicWrite(filePath, JSON.stringify(next, null, 2));
53
+ }
54
+ async function upsertTOML(filePath, append) {
55
+ await fs_1.promises.mkdir(path.dirname(filePath), { recursive: true });
56
+ let existing = '';
57
+ try {
58
+ existing = await fs_1.promises.readFile(filePath, 'utf8');
59
+ }
60
+ catch (e) {
61
+ if (e.code !== 'ENOENT')
62
+ throw e;
63
+ }
64
+ if (existing.includes('[mcp_servers.biomate]')) {
65
+ // Naive replacement: drop the existing block and re-append.
66
+ existing = existing.replace(/\n\[mcp_servers\.biomate\][\s\S]*?(?=\n\[|$)/g, '');
67
+ }
68
+ await atomicWrite(filePath, existing + append);
69
+ }
70
+ async function atomicWrite(filePath, contents) {
71
+ const tmp = `${filePath}.${process.pid}.tmp`;
72
+ await fs_1.promises.writeFile(tmp, contents, 'utf8');
73
+ await fs_1.promises.rename(tmp, filePath);
74
+ }
@@ -0,0 +1,3 @@
1
+ export { generatePKCE } from './pkce.js';
2
+ export { runPKCEFlow } from './oauth.js';
3
+ export { SURFACES, SurfaceConfig, SurfaceId } from './surfaces.js';
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SURFACES = exports.runPKCEFlow = exports.generatePKCE = void 0;
4
+ var pkce_js_1 = require("./pkce.js");
5
+ Object.defineProperty(exports, "generatePKCE", { enumerable: true, get: function () { return pkce_js_1.generatePKCE; } });
6
+ var oauth_js_1 = require("./oauth.js");
7
+ Object.defineProperty(exports, "runPKCEFlow", { enumerable: true, get: function () { return oauth_js_1.runPKCEFlow; } });
8
+ var surfaces_js_1 = require("./surfaces.js");
9
+ Object.defineProperty(exports, "SURFACES", { enumerable: true, get: function () { return surfaces_js_1.SURFACES; } });
@@ -0,0 +1,8 @@
1
+ import { SurfaceConfig } from './surfaces.js';
2
+ export interface TokenSet {
3
+ access_token: string;
4
+ refresh_token: string;
5
+ expires_in: number;
6
+ scope: string;
7
+ }
8
+ export declare function runPKCEFlow(surface: SurfaceConfig, authBaseUrl: string): Promise<TokenSet>;
package/dist/oauth.js ADDED
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runPKCEFlow = runPKCEFlow;
7
+ const http_1 = require("http");
8
+ const url_1 = require("url");
9
+ const open_1 = __importDefault(require("open"));
10
+ const pkce_js_1 = require("./pkce.js");
11
+ async function runPKCEFlow(surface, authBaseUrl) {
12
+ const { verifier, challenge } = (0, pkce_js_1.generatePKCE)();
13
+ const port = surface.callbackPort;
14
+ const redirectUri = `http://127.0.0.1:${port}/callback`;
15
+ const state = Math.random().toString(36).slice(2);
16
+ const codePromise = catchCallback(port, state);
17
+ // Mark the rejection as handled — the awaiter below will still receive it.
18
+ // Without this, Node's unhandled-rejection detector fires when the callback
19
+ // server rejects before `await codePromise` has attached a handler.
20
+ codePromise.catch(() => undefined);
21
+ const authUrl = new url_1.URL(`${authBaseUrl}/oauth/authorize`);
22
+ authUrl.searchParams.set('response_type', 'code');
23
+ authUrl.searchParams.set('client_id', surface.clientId);
24
+ authUrl.searchParams.set('redirect_uri', redirectUri);
25
+ authUrl.searchParams.set('code_challenge', challenge);
26
+ authUrl.searchParams.set('code_challenge_method', 'S256');
27
+ authUrl.searchParams.set('scope', surface.scopes.join(' '));
28
+ authUrl.searchParams.set('state', state);
29
+ console.log(`\nOpening browser to authorize ${surface.label}…`);
30
+ console.log(`If it doesn't open, visit:\n ${authUrl.toString()}\n`);
31
+ await (0, open_1.default)(authUrl.toString());
32
+ const code = await codePromise;
33
+ const body = new URLSearchParams({
34
+ grant_type: 'authorization_code',
35
+ code,
36
+ redirect_uri: redirectUri,
37
+ client_id: surface.clientId,
38
+ code_verifier: verifier,
39
+ });
40
+ const resp = await fetch(`${authBaseUrl}/oauth/token`, {
41
+ method: 'POST',
42
+ headers: { 'content-type': 'application/x-www-form-urlencoded' },
43
+ body,
44
+ });
45
+ if (!resp.ok) {
46
+ throw new Error(`token exchange failed: ${resp.status} ${await resp.text()}`);
47
+ }
48
+ return (await resp.json());
49
+ }
50
+ function catchCallback(port, expectedState) {
51
+ return new Promise((resolve, reject) => {
52
+ const server = (0, http_1.createServer)((req, res) => {
53
+ try {
54
+ const url = new url_1.URL(req.url ?? '/', `http://127.0.0.1:${port}`);
55
+ if (url.pathname !== '/callback') {
56
+ res.writeHead(404);
57
+ res.end();
58
+ return;
59
+ }
60
+ const code = url.searchParams.get('code');
61
+ const state = url.searchParams.get('state');
62
+ const error = url.searchParams.get('error');
63
+ res.writeHead(200, { 'content-type': 'text/html' });
64
+ if (error) {
65
+ res.end(htmlResult(`Authorization failed: ${error}`, false));
66
+ server.close();
67
+ reject(new Error(error));
68
+ return;
69
+ }
70
+ if (!code || state !== expectedState) {
71
+ res.end(htmlResult('Invalid callback (missing code or state mismatch).', false));
72
+ server.close();
73
+ reject(new Error('invalid callback'));
74
+ return;
75
+ }
76
+ res.end(htmlResult('Connected! You can close this tab.', true));
77
+ server.close();
78
+ resolve(code);
79
+ }
80
+ catch (e) {
81
+ reject(e);
82
+ }
83
+ });
84
+ server.on('error', reject);
85
+ server.listen(port, '127.0.0.1');
86
+ setTimeout(() => {
87
+ server.close();
88
+ reject(new Error('OAuth callback timed out after 3 minutes'));
89
+ }, 3 * 60 * 1000);
90
+ });
91
+ }
92
+ function htmlResult(message, ok) {
93
+ const color = ok ? '#0a7' : '#c33';
94
+ return `<!doctype html><html><body style="font:14px/1.5 -apple-system,sans-serif;text-align:center;margin-top:80px">
95
+ <h1 style="color:${color}">${message}</h1>
96
+ <p style="color:#666">BioMate connector installer</p></body></html>`;
97
+ }
package/dist/pkce.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ export interface PKCEPair {
2
+ verifier: string;
3
+ challenge: string;
4
+ method: 'S256';
5
+ }
6
+ export declare function generatePKCE(): PKCEPair;
package/dist/pkce.js ADDED
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generatePKCE = generatePKCE;
4
+ const crypto_1 = require("crypto");
5
+ function generatePKCE() {
6
+ const verifier = base64url((0, crypto_1.randomBytes)(48));
7
+ const challenge = base64url((0, crypto_1.createHash)('sha256').update(verifier).digest());
8
+ return { verifier, challenge, method: 'S256' };
9
+ }
10
+ function base64url(buf) {
11
+ return buf
12
+ .toString('base64')
13
+ .replace(/=/g, '')
14
+ .replace(/\+/g, '-')
15
+ .replace(/\//g, '_');
16
+ }
@@ -0,0 +1,13 @@
1
+ export type SurfaceId = 'claude-code' | 'claude-desktop' | 'cursor' | 'codex' | 'chatgpt' | 'open-claw';
2
+ export interface SurfaceConfig {
3
+ id: SurfaceId;
4
+ label: string;
5
+ clientId: string;
6
+ callbackPort: number;
7
+ scopes: string[];
8
+ /** Path of the host's MCP config file. */
9
+ configPath: () => string;
10
+ /** Path-agnostic config writer. Returns a description of what changed. */
11
+ install: (token: string, refreshToken: string) => Promise<string>;
12
+ }
13
+ export declare const SURFACES: Record<SurfaceId, SurfaceConfig>;
@@ -0,0 +1,176 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.SURFACES = void 0;
37
+ const os = __importStar(require("os"));
38
+ const path = __importStar(require("path"));
39
+ const DEFAULT_SCOPES = [
40
+ 'runs:read',
41
+ 'runs:write',
42
+ 'workflows:search',
43
+ 'memory:read',
44
+ 'memory:write',
45
+ 'files:upload',
46
+ 'reports:export',
47
+ 'billing:read',
48
+ ];
49
+ function home() {
50
+ return os.homedir();
51
+ }
52
+ function platformConfigDir() {
53
+ if (process.platform === 'darwin')
54
+ return path.join(home(), 'Library', 'Application Support');
55
+ if (process.platform === 'win32')
56
+ return process.env.APPDATA ?? home();
57
+ return path.join(home(), '.config');
58
+ }
59
+ function mcpServerEntry(refreshToken) {
60
+ const apiBase = process.env.BIOMATE_API_BASE ?? 'https://api.biomate.ai';
61
+ return {
62
+ command: 'npx',
63
+ args: ['-y', '@biomate/mcp-server'],
64
+ env: {
65
+ BIOMATE_API_BASE: apiBase,
66
+ BIOMATE_REFRESH_TOKEN: refreshToken,
67
+ },
68
+ };
69
+ }
70
+ const configWriters_js_1 = require("./configWriters.js");
71
+ exports.SURFACES = {
72
+ 'claude-code': {
73
+ id: 'claude-code',
74
+ label: 'Claude Code',
75
+ clientId: 'biomate-claude-code',
76
+ callbackPort: 53682,
77
+ scopes: DEFAULT_SCOPES,
78
+ configPath: () => path.join(home(), '.claude.json'),
79
+ install: async (_t, refresh) => {
80
+ const p = path.join(home(), '.claude.json');
81
+ await (0, configWriters_js_1.upsertJSON)(p, (cfg) => {
82
+ cfg.mcpServers ??= {};
83
+ cfg.mcpServers.biomate = mcpServerEntry(refresh);
84
+ return cfg;
85
+ });
86
+ return p;
87
+ },
88
+ },
89
+ 'claude-desktop': {
90
+ id: 'claude-desktop',
91
+ label: 'Claude Desktop',
92
+ clientId: 'biomate-claude-desktop',
93
+ callbackPort: 53683,
94
+ scopes: DEFAULT_SCOPES,
95
+ configPath: () => path.join(platformConfigDir(), 'Claude', 'claude_desktop_config.json'),
96
+ install: async (_t, refresh) => {
97
+ const p = path.join(platformConfigDir(), 'Claude', 'claude_desktop_config.json');
98
+ await (0, configWriters_js_1.upsertJSON)(p, (cfg) => {
99
+ cfg.mcpServers ??= {};
100
+ cfg.mcpServers.biomate = mcpServerEntry(refresh);
101
+ return cfg;
102
+ });
103
+ return p;
104
+ },
105
+ },
106
+ cursor: {
107
+ id: 'cursor',
108
+ label: 'Cursor',
109
+ clientId: 'biomate-cursor',
110
+ callbackPort: 53684,
111
+ scopes: DEFAULT_SCOPES,
112
+ configPath: () => path.join(home(), '.cursor', 'mcp.json'),
113
+ install: async (_t, refresh) => {
114
+ const p = path.join(home(), '.cursor', 'mcp.json');
115
+ await (0, configWriters_js_1.upsertJSON)(p, (cfg) => {
116
+ cfg.mcpServers ??= {};
117
+ cfg.mcpServers.biomate = mcpServerEntry(refresh);
118
+ return cfg;
119
+ });
120
+ return p;
121
+ },
122
+ },
123
+ codex: {
124
+ id: 'codex',
125
+ label: 'Codex CLI',
126
+ clientId: 'biomate-codex',
127
+ callbackPort: 53685,
128
+ scopes: DEFAULT_SCOPES,
129
+ configPath: () => path.join(home(), '.codex', 'config.toml'),
130
+ install: async (_t, refresh) => {
131
+ const p = path.join(home(), '.codex', 'config.toml');
132
+ const apiBase = process.env.BIOMATE_API_BASE ?? 'https://api.biomate.ai';
133
+ await (0, configWriters_js_1.upsertTOML)(p, `\n[mcp_servers.biomate]\ncommand = "npx"\nargs = ["-y", "@biomate/mcp-server"]\n\n[mcp_servers.biomate.env]\nBIOMATE_API_BASE = "${apiBase}"\nBIOMATE_REFRESH_TOKEN = "${refresh}"\n`);
134
+ return p;
135
+ },
136
+ },
137
+ chatgpt: {
138
+ id: 'chatgpt',
139
+ label: 'ChatGPT',
140
+ clientId: 'biomate-chatgpt',
141
+ callbackPort: 53686, // Not actually used — ChatGPT does OAuth in-app.
142
+ scopes: DEFAULT_SCOPES,
143
+ configPath: () => '(ChatGPT Actions — configured in chat.openai.com)',
144
+ install: async () => {
145
+ console.log(`
146
+ ChatGPT connects to BioMate via Custom GPT Actions, not a local config file.
147
+ Install steps:
148
+ 1. Visit https://chatgpt.com/gpts/editor and create a new GPT.
149
+ 2. Import OpenAPI from: https://api.biomate.ai/connectors/chatgpt/openapi.json
150
+ 3. Enable OAuth in the Actions panel:
151
+ Authorization URL: https://api.biomate.ai/oauth/authorize
152
+ Token URL: https://api.biomate.ai/oauth/token
153
+ Client ID: biomate-chatgpt
154
+ Scope: ${DEFAULT_SCOPES.join(' ')}
155
+ 4. Save and test with: "Screen aspirin for hERG"
156
+ `);
157
+ return '(ChatGPT in-app setup)';
158
+ },
159
+ },
160
+ 'open-claw': {
161
+ id: 'open-claw',
162
+ label: 'Open Claw (WeChat)',
163
+ clientId: 'biomate-open-claw',
164
+ callbackPort: 53687,
165
+ scopes: DEFAULT_SCOPES,
166
+ configPath: () => '(Open Claw — hosted by BioMate)',
167
+ install: async (_t, refresh) => {
168
+ console.log(`
169
+ Open Claw runs as a hosted webhook — there's nothing to install locally.
170
+ We've linked your account; in WeChat search the BioMate Open Claw bot and
171
+ say hi.
172
+ `);
173
+ return '(linked)';
174
+ },
175
+ },
176
+ };
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@biomate/connect",
3
+ "version": "0.1.0",
4
+ "description": "One-line installer that connects BioMate to Claude Code, Claude Desktop, Cursor, Codex, and ChatGPT via OAuth 2.1 + PKCE.",
5
+ "bin": {
6
+ "biomate-connect": "./dist/cli.js"
7
+ },
8
+ "main": "./dist/index.js",
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "test": "vitest run",
12
+ "prepublishOnly": "npm run build"
13
+ },
14
+ "keywords": ["biomate", "mcp", "claude", "cursor", "codex", "chatgpt", "bioinformatics"],
15
+ "author": "BioMate AI",
16
+ "license": "MIT",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/bioMate-AI/biomate-connectors.git",
20
+ "directory": "installer"
21
+ },
22
+ "engines": {
23
+ "node": ">=18"
24
+ },
25
+ "dependencies": {
26
+ "keytar": "^7.9.0",
27
+ "open": "^9.1.0"
28
+ },
29
+ "devDependencies": {
30
+ "@types/node": "^20.0.0",
31
+ "typescript": "^5.4.0",
32
+ "vitest": "^1.0.0"
33
+ },
34
+ "files": ["dist", "README.md"],
35
+ "publishConfig": {
36
+ "access": "public"
37
+ }
38
+ }