opencode-bioresearcher 1.11.1 → 1.12.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.
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env bash
2
+ # Stage 0 Zero-Dependency Bootstrap for bioresearcher-onboard.
3
+ # Checks for existing Node >= 22.13; if missing, downloads pinned portable
4
+ # Node.js LTS into .bioresearcher-runtime/node, verifies SHA256, and hands off
5
+ # execution to onboard.mjs.
6
+ set -euo pipefail
7
+
8
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
9
+ RUNTIME_ROOT="${BIORESEARCHER_RUNTIME_DIR:-$PWD/.bioresearcher-runtime}"
10
+ NODE_DIR="$RUNTIME_ROOT/node"
11
+ NODE_BIN="$NODE_DIR/bin/node"
12
+
13
+ NODE_REQ_MAJ=22
14
+ NODE_REQ_MIN=13
15
+ NODE_VER="v22.14.0"
16
+
17
+ # 1. Probe if host already satisfies Node >= 22.13
18
+ if command -v node >/dev/null 2>&1; then
19
+ if node -e "var v=process.versions.node.split('.');process.exit((v[0]>$NODE_REQ_MAJ||(v[0]==$NODE_REQ_MAJ&&v[1]>=$NODE_REQ_MIN))?0:1)" 2>/dev/null; then
20
+ echo "[bioresearcher-onboard] Found host $(node -v) at $(command -v node)"
21
+ exec node "$SCRIPT_DIR/onboard.mjs" "$@"
22
+ fi
23
+ fi
24
+
25
+ # If already vendored in working directory, reuse directly
26
+ if [ -x "$NODE_BIN" ]; then
27
+ export PATH="$NODE_DIR/bin:$PATH"
28
+ exec "$NODE_BIN" "$SCRIPT_DIR/onboard.mjs" "$@"
29
+ fi
30
+
31
+ echo "[bioresearcher-onboard] Host Node >= 22.13 not found. Bootstrapping portable Node.js $NODE_VER into .bioresearcher-runtime/..."
32
+
33
+ # 2. OS and Architecture Detection
34
+ OS_RAW="$(uname -s)"
35
+ case "$OS_RAW" in
36
+ Linux*) OS="linux" ;;
37
+ Darwin*) OS="darwin" ;;
38
+ *) echo "Error: Unsupported operating system $OS_RAW. Windows users should run bootstrap.ps1." >&2; exit 1 ;;
39
+ esac
40
+
41
+ # Linux C library gate
42
+ if [ "$OS" = "linux" ]; then
43
+ if ldd /bin/ls 2>&1 | grep -qi "musl"; then
44
+ echo "Error: Alpine/musl Linux detected. Official Node prebuilts require glibc." >&2
45
+ echo "Please install Node >= 22.13 via your system package manager (e.g. apk add nodejs)." >&2
46
+ exit 1
47
+ fi
48
+ fi
49
+
50
+ ARCH_RAW="$(uname -m)"
51
+ case "$ARCH_RAW" in
52
+ x86_64|amd64) ARCH="x64" ;;
53
+ arm64|aarch64) ARCH="arm64" ;;
54
+ *) echo "Error: Unsupported CPU architecture $ARCH_RAW." >&2; exit 1 ;;
55
+ esac
56
+
57
+ # macOS Rosetta detection: probe true hardware arch
58
+ if [ "$OS" = "darwin" ]; then
59
+ if [ "$(sysctl -in sysctl.proc_translated 2>/dev/null || echo 0)" = "1" ]; then
60
+ ARCH="arm64"
61
+ fi
62
+ fi
63
+
64
+ # 3. Mirror Selection Probe (1.2s timeout)
65
+ if curl -s -I --connect-timeout 1.0 --max-time 1.2 "https://nodejs.org/dist/" >/dev/null 2>&1; then
66
+ MIRROR_BASE="https://nodejs.org/dist/${NODE_VER}"
67
+ NPM_REGISTRY="https://registry.npmjs.org"
68
+ else
69
+ echo "[bioresearcher-onboard] nodejs.org unreachable or timed out; using npmmirror.com fast mirror"
70
+ MIRROR_BASE="https://npmmirror.com/mirrors/node/${NODE_VER}"
71
+ NPM_REGISTRY="https://registry.npmmirror.com"
72
+ fi
73
+
74
+ FILE="node-${NODE_VER}-${OS}-${ARCH}.tar.gz"
75
+ DOWNLOAD_URL="${MIRROR_BASE}/${FILE}"
76
+
77
+ # Pinned SHA256 checksums from official Node.js SHASUMS256.txt
78
+ case "${OS}-${ARCH}" in
79
+ linux-x64) EXPECTED_SHA="9d942932535988091034dc94cc5f42b6dc8784d6366df3a36c4c9ccb3996f0c2" ;;
80
+ linux-arm64) EXPECTED_SHA="8cf30ff7250f9463b53c18f89c6c606dfda70378215b2c905d0a9a8b08bd45e0" ;;
81
+ darwin-x64) EXPECTED_SHA="6698587713ab565a94a360e091df9f6d91c8fadda6d00f0cf6526e9b40bed250" ;;
82
+ darwin-arm64) EXPECTED_SHA="e9404633bc02a5162c5c573b1e2490f5fb44648345d64a958b17e325729a5e42" ;;
83
+ *) echo "Error: Missing checksum for target ${OS}-${ARCH}." >&2; exit 1 ;;
84
+ esac
85
+
86
+ mkdir -p "$RUNTIME_ROOT"
87
+ TMP_ARCHIVE="$RUNTIME_ROOT/node.tar.gz"
88
+
89
+ echo "[bioresearcher-onboard] Downloading $FILE..."
90
+ curl -f -s -L --retry 2 -o "$TMP_ARCHIVE" "$DOWNLOAD_URL"
91
+
92
+ # 4. Checksum verification
93
+ if command -v sha256sum >/dev/null 2>&1; then
94
+ ACTUAL_SHA="$(sha256sum "$TMP_ARCHIVE" | awk '{print $1}')"
95
+ elif command -v shasum >/dev/null 2>&1; then
96
+ ACTUAL_SHA="$(shasum -a 256 "$TMP_ARCHIVE" | awk '{print $1}')"
97
+ else
98
+ ACTUAL_SHA=""
99
+ fi
100
+
101
+ if [ -n "$ACTUAL_SHA" ] && [ "$ACTUAL_SHA" != "$EXPECTED_SHA" ]; then
102
+ echo "Error: SHA256 checksum verification failed for $FILE." >&2
103
+ echo "Expected: $EXPECTED_SHA" >&2
104
+ echo "Actual: $ACTUAL_SHA" >&2
105
+ rm -f "$TMP_ARCHIVE"
106
+ exit 1
107
+ fi
108
+
109
+ # 5. Extract and prune
110
+ mkdir -p "$NODE_DIR"
111
+ tar -xzf "$TMP_ARCHIVE" -C "$NODE_DIR" --strip-components=1
112
+ rm -f "$TMP_ARCHIVE"
113
+
114
+ # Remove non-runtime artifacts (saves ~49 MB)
115
+ rm -rf "$NODE_DIR/include" "$NODE_DIR/share" "$NODE_DIR"/*.md "$NODE_DIR"/LICENSE
116
+
117
+ # macOS Gatekeeper quarantine removal
118
+ if [ "$OS" = "darwin" ]; then
119
+ xattr -dr com.apple.quarantine "$NODE_DIR" 2>/dev/null || true
120
+ fi
121
+
122
+ echo "[bioresearcher-onboard] Portable Node.js ready: $("$NODE_BIN" -v)"
123
+
124
+ # 6. Hand off to Stage 1
125
+ export PATH="$NODE_DIR/bin:$PATH"
126
+ export NPM_CONFIG_REGISTRY="$NPM_REGISTRY"
127
+ exec "$NODE_BIN" "$SCRIPT_DIR/onboard.mjs" "$@"
@@ -0,0 +1,512 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Stage 1 Onboarding Engine for bioresearcher-onboard.
4
+ * Installs biomcp@1.6.0 into .bioresearcher-runtime/node_modules, configures
5
+ * optional features via .biomcp.json, non-destructively merges harness
6
+ * configuration files, and runs pre-flight doctor verification.
7
+ */
8
+
9
+ import fs from "node:fs";
10
+ import path from "node:path";
11
+ import process from "node:process";
12
+ import { spawnSync } from "node:child_process";
13
+
14
+ const CWD = process.cwd();
15
+ const RUNTIME_ROOT = process.env.BIORESEARCHER_RUNTIME_DIR || path.resolve(CWD, ".bioresearcher-runtime");
16
+ const CACHE_DIR = path.join(RUNTIME_ROOT, "npm-cache");
17
+ const IS_WIN = process.platform === "win32";
18
+
19
+ // 1. Resolve Node and npm executables
20
+ let nodeBin = process.execPath;
21
+ const vendoredNode = IS_WIN
22
+ ? path.join(RUNTIME_ROOT, "node", "node.exe")
23
+ : path.join(RUNTIME_ROOT, "node", "bin", "node");
24
+
25
+ if (fs.existsSync(vendoredNode)) {
26
+ nodeBin = vendoredNode;
27
+ }
28
+
29
+ let npmBin = IS_WIN
30
+ ? path.join(RUNTIME_ROOT, "node", "npm.cmd")
31
+ : path.join(RUNTIME_ROOT, "node", "bin", "npm");
32
+
33
+ if (!fs.existsSync(npmBin)) {
34
+ npmBin = "npm";
35
+ }
36
+
37
+ // 2. Parse CLI options
38
+ const args = process.argv.slice(2);
39
+ const getArgVal = (prefix) => {
40
+ const match = args.find((a) => a.startsWith(prefix));
41
+ return match ? match.slice(match.indexOf("=") + 1) : null;
42
+ };
43
+
44
+ const options = {
45
+ withR: args.includes("--with-r") || process.env.ONBOARD_R === "1",
46
+ withMysql: args.includes("--with-mysql") || process.env.ONBOARD_MYSQL === "1",
47
+ withBiowasm: args.includes("--with-biowasm") || process.env.ONBOARD_BIOWASM === "1",
48
+ sqlitePath: getArgVal("--sqlite-path=") || process.env.ONBOARD_SQLITE_PATH || null,
49
+ client: getArgVal("--client=") || null,
50
+ registry: getArgVal("--registry=") || process.env.NPM_CONFIG_REGISTRY || "https://registry.npmjs.org",
51
+ nonInteractive: args.includes("--non-interactive") || !process.stdin.isTTY,
52
+ };
53
+
54
+ console.log("[bioresearcher-onboard] Initializing project-local BioMCP runtime...");
55
+
56
+ // 3. Prepare isolated .bioresearcher-runtime directory
57
+ fs.mkdirSync(RUNTIME_ROOT, { recursive: true });
58
+ fs.mkdirSync(CACHE_DIR, { recursive: true });
59
+
60
+ const runtimePkgJson = path.join(RUNTIME_ROOT, "package.json");
61
+ if (!fs.existsSync(runtimePkgJson)) {
62
+ fs.writeFileSync(runtimePkgJson, JSON.stringify({ name: "bioresearcher-runtime", private: true }, null, 2));
63
+ }
64
+
65
+ // 4. Determine packages to install
66
+ const packagesToInstall = ["biomcp@1.6.0"];
67
+ if (options.withR) packagesToInstall.push("webr@0.6");
68
+ if (options.withMysql) packagesToInstall.push("mysql2@3");
69
+
70
+ console.log(`[bioresearcher-onboard] Installing dependencies (${packagesToInstall.join(", ")}) via ${options.registry}...`);
71
+
72
+ const npmEnv = {
73
+ ...process.env,
74
+ npm_config_cache: CACHE_DIR,
75
+ npm_config_userconfig: IS_WIN ? "NUL" : "/dev/null",
76
+ npm_config_audit: "false",
77
+ npm_config_fund: "false",
78
+ npm_config_update_notifier: "false",
79
+ npm_config_registry: options.registry,
80
+ };
81
+
82
+ const installArgs = ["install", "--prefix", RUNTIME_ROOT, "--ignore-scripts", ...packagesToInstall];
83
+ const installRes = spawnSync(npmBin, installArgs, {
84
+ env: npmEnv,
85
+ stdio: "inherit",
86
+ cwd: RUNTIME_ROOT,
87
+ });
88
+
89
+ if (installRes.status !== 0) {
90
+ console.error("[bioresearcher-onboard] Error: Failed to install packages into .bioresearcher-runtime");
91
+ process.exit(installRes.status || 1);
92
+ }
93
+
94
+ // 5. Verify bundle path
95
+ const bundlePath = path.join(RUNTIME_ROOT, "node_modules", "biomcp", "dist", "bundle.js");
96
+ if (!fs.existsSync(bundlePath)) {
97
+ console.error(`[bioresearcher-onboard] Error: BioMCP bundle not found at ${bundlePath}`);
98
+ process.exit(1);
99
+ }
100
+
101
+ // 6. Generate .biomcp.json if optional features are requested
102
+ const featuresConfig = {};
103
+ if (options.withR) {
104
+ featuresConfig.analysis_r = { enabled: true };
105
+ }
106
+ if (options.withBiowasm) {
107
+ featuresConfig.analysis_biowasm = { enabled: true };
108
+ }
109
+ if (options.sqlitePath) {
110
+ featuresConfig.database = {
111
+ enabled: true,
112
+ type: "sqlite",
113
+ sqlite_path: options.sqlitePath,
114
+ };
115
+ } else if (options.withMysql) {
116
+ featuresConfig.database = {
117
+ enabled: true,
118
+ type: "mysql",
119
+ };
120
+ }
121
+
122
+ const biomcpJsonPath = path.resolve(CWD, ".biomcp.json");
123
+ if (Object.keys(featuresConfig).length > 0) {
124
+ let existing = {};
125
+ if (fs.existsSync(biomcpJsonPath)) {
126
+ try {
127
+ existing = JSON.parse(fs.readFileSync(biomcpJsonPath, "utf8"));
128
+ } catch {}
129
+ }
130
+ existing.features = { ...existing.features, ...featuresConfig };
131
+
132
+ // Write atomically with 0o600 permissions
133
+ const tmpPath = `${biomcpJsonPath}.${Date.now()}.tmp`;
134
+ fs.writeFileSync(tmpPath, JSON.stringify(existing, null, 2), { mode: 0o600 });
135
+ fs.renameSync(tmpPath, biomcpJsonPath);
136
+ console.log(`[bioresearcher-onboard] Configured optional features in .biomcp.json`);
137
+ }
138
+
139
+ // 7. Non-destructive deep-merge of client harness configuration files
140
+ // Normalize Windows paths to POSIX forward slashes for valid JSON strings
141
+ const normalizedNodePath = IS_WIN ? nodeBin.replace(/\\/g, "/") : nodeBin;
142
+ const normalizedBundlePath = IS_WIN ? bundlePath.replace(/\\/g, "/") : bundlePath;
143
+
144
+ function stripJsonc(content) {
145
+ let result = "";
146
+ let inString = false;
147
+ let inSingleComment = false;
148
+ let inMultiComment = false;
149
+ let isEscaped = false;
150
+
151
+ for (let i = 0; i < content.length; i++) {
152
+ const ch = content[i];
153
+ const next = content[i + 1];
154
+
155
+ if (inSingleComment) {
156
+ if (ch === "\n") {
157
+ inSingleComment = false;
158
+ result += ch;
159
+ }
160
+ continue;
161
+ }
162
+
163
+ if (inMultiComment) {
164
+ if (ch === "*" && next === "/") {
165
+ inMultiComment = false;
166
+ i++;
167
+ }
168
+ continue;
169
+ }
170
+
171
+ if (inString) {
172
+ result += ch;
173
+ if (isEscaped) {
174
+ isEscaped = false;
175
+ } else if (ch === "\\") {
176
+ isEscaped = true;
177
+ } else if (ch === "\"") {
178
+ inString = false;
179
+ }
180
+ continue;
181
+ }
182
+
183
+ if (ch === "\"") {
184
+ inString = true;
185
+ isEscaped = false;
186
+ result += ch;
187
+ continue;
188
+ }
189
+
190
+ if (ch === "/" && next === "/") {
191
+ inSingleComment = true;
192
+ i++;
193
+ continue;
194
+ }
195
+
196
+ if (ch === "/" && next === "*") {
197
+ inMultiComment = true;
198
+ i++;
199
+ continue;
200
+ }
201
+
202
+ if (ch === ",") {
203
+ let j = i + 1;
204
+ let isTrailing = false;
205
+ let tempInSingle = false;
206
+ let tempInMulti = false;
207
+ while (j < content.length) {
208
+ const cj = content[j];
209
+ const nj = content[j + 1];
210
+ if (tempInSingle) {
211
+ if (cj === "\n") tempInSingle = false;
212
+ j++;
213
+ continue;
214
+ }
215
+ if (tempInMulti) {
216
+ if (cj === "*" && nj === "/") {
217
+ tempInMulti = false;
218
+ j += 2;
219
+ continue;
220
+ }
221
+ j++;
222
+ continue;
223
+ }
224
+ if (cj === "/" && nj === "/") {
225
+ tempInSingle = true;
226
+ j += 2;
227
+ continue;
228
+ }
229
+ if (cj === "/" && nj === "*") {
230
+ tempInMulti = true;
231
+ j += 2;
232
+ continue;
233
+ }
234
+ if (/\s/.test(cj)) {
235
+ j++;
236
+ continue;
237
+ }
238
+ if (cj === "}" || cj === "]") {
239
+ isTrailing = true;
240
+ }
241
+ break;
242
+ }
243
+ if (isTrailing) {
244
+ continue;
245
+ }
246
+ }
247
+
248
+ result += ch;
249
+ }
250
+
251
+ return result.trim();
252
+ }
253
+
254
+ function safeMergeJson(filePath, updater) {
255
+ let data = {};
256
+ let backedUp = false;
257
+ if (fs.existsSync(filePath)) {
258
+ const raw = fs.readFileSync(filePath, "utf8");
259
+ if (raw.trim().length > 0) {
260
+ try {
261
+ data = JSON.parse(stripJsonc(raw));
262
+ } catch (e) {
263
+ console.error(`[bioresearcher-onboard] Error: Failed to parse existing JSONC at ${filePath}: ${e.message}`);
264
+ console.error(`[bioresearcher-onboard] Aborting modification to prevent data loss. Existing file was not modified.`);
265
+ process.exit(1);
266
+ }
267
+ }
268
+ fs.copyFileSync(filePath, `${filePath}.bak`);
269
+ backedUp = true;
270
+ }
271
+
272
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
273
+ data = {};
274
+ }
275
+
276
+ const updated = updater(data);
277
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
278
+ const tmp = `${filePath}.${Date.now()}.tmp`;
279
+ fs.writeFileSync(tmp, JSON.stringify(updated, null, 2));
280
+ fs.renameSync(tmp, filePath);
281
+ if (backedUp) {
282
+ console.log(`[bioresearcher-onboard] Updated ${path.basename(filePath)} (backup saved to ${path.basename(filePath)}.bak)`);
283
+ } else {
284
+ console.log(`[bioresearcher-onboard] Created ${path.basename(filePath)}`);
285
+ }
286
+ }
287
+
288
+ // Probe harness configs upfront to avoid generating unintended files
289
+ const opencodePath = path.resolve(CWD, "opencode.json");
290
+ const claudePath = path.resolve(CWD, ".mcp.json");
291
+ const cursorDir = path.resolve(CWD, ".cursor");
292
+ const cursorPath = path.join(cursorDir, "mcp.json");
293
+
294
+ const zcodeDir = path.resolve(CWD, ".zcode");
295
+ const zcodePath = path.join(zcodeDir, "config.json");
296
+
297
+ const piDir = path.resolve(CWD, ".pi");
298
+ const piPath = path.join(piDir, "mcp.json");
299
+
300
+ const codebuddyDir = path.resolve(CWD, ".codebuddy");
301
+ const codebuddySettingsPath = path.join(codebuddyDir, "settings.json");
302
+ const codebuddyMdPath = path.resolve(CWD, "CODEBUDDY.md");
303
+
304
+ const workbuddyDir = path.resolve(CWD, ".workbuddy");
305
+ const workbuddyPath = path.join(workbuddyDir, "mcp.json");
306
+ const workbuddyMdPath = path.resolve(CWD, "WORKBUDDY.md");
307
+
308
+ const opencodeExists = fs.existsSync(opencodePath);
309
+ const claudeExists = fs.existsSync(claudePath);
310
+ const cursorExists = fs.existsSync(cursorDir) || fs.existsSync(cursorPath);
311
+ const zcodeExists = fs.existsSync(zcodeDir) || fs.existsSync(zcodePath) || Boolean(process.env.ZCODE_WORKSPACE);
312
+ const piExists = fs.existsSync(piDir) || fs.existsSync(piPath) || Boolean(process.env.PI_CODING_AGENT_DIR);
313
+ const codebuddyExists = fs.existsSync(codebuddyDir) || fs.existsSync(codebuddyMdPath) || Boolean(process.env.CODEBUDDY_CLI);
314
+ const workbuddyExists = fs.existsSync(workbuddyDir) || fs.existsSync(workbuddyPath) || fs.existsSync(workbuddyMdPath);
315
+
316
+ // Normalize client argument with aliases
317
+ const clientArg = (options.client || "").toLowerCase().trim();
318
+ const clientAliases = {
319
+ "claude": "claude-code",
320
+ "code-buddy": "codebuddy",
321
+ "work-buddy": "workbuddy",
322
+ "pi-agent": "pi",
323
+ };
324
+ const resolvedClient = clientAliases[clientArg] || clientArg;
325
+
326
+ const validClients = ["opencode", "claude-code", "cursor", "zcode", "pi", "codebuddy", "workbuddy"];
327
+ if (resolvedClient && !validClients.includes(resolvedClient)) {
328
+ console.error(`[bioresearcher-onboard] Error: Unknown client "${options.client}". Valid options: ${validClients.join(", ")}`);
329
+ process.exit(1);
330
+ }
331
+
332
+ let targetOpencode = resolvedClient === "opencode" || (!resolvedClient && opencodeExists);
333
+ let targetClaude = resolvedClient === "claude-code" || (!resolvedClient && claudeExists && !codebuddyExists);
334
+ let targetCursor = resolvedClient === "cursor" || (!resolvedClient && cursorExists);
335
+ let targetZcode = resolvedClient === "zcode" || (!resolvedClient && zcodeExists);
336
+ let targetPi = resolvedClient === "pi" || (!resolvedClient && piExists);
337
+ let targetCodebuddy = resolvedClient === "codebuddy" || (!resolvedClient && codebuddyExists);
338
+ let targetWorkbuddy = resolvedClient === "workbuddy" || (!resolvedClient && workbuddyExists);
339
+
340
+ // Default to opencode.json if no client specified and no config file exists yet
341
+ const anyDetected = opencodeExists || claudeExists || cursorExists || zcodeExists || piExists || codebuddyExists || workbuddyExists;
342
+ if (!resolvedClient && !anyDetected) {
343
+ targetOpencode = true;
344
+ }
345
+
346
+ // OpenCode: opencode.json (canonical v2 shape mcp.servers.<name>)
347
+ if (targetOpencode) {
348
+ const npxCmd = IS_WIN ? "npx.cmd" : "npx";
349
+ safeMergeJson(opencodePath, (data) => {
350
+ data.$schema = data.$schema || "https://opencode.ai/config.json";
351
+ data.mcp = data.mcp || {};
352
+ data.mcp.servers = data.mcp.servers || {};
353
+ // Migrate legacy flat entry (pre-v2 onboard / old plugin config hook):
354
+ // preserve environment, convert flat timeout N to the triple form.
355
+ const legacy = data.mcp.biomcp;
356
+ if (data.mcp.servers.biomcp === undefined && legacy && typeof legacy === "object" && !Array.isArray(legacy)) {
357
+ const flatTimeout = typeof legacy.timeout === "number" ? legacy.timeout : 120000;
358
+ data.mcp.servers.biomcp = {
359
+ type: "local",
360
+ command: [npxCmd, "-y", "-p", "biomcp@1.6.0", "biomcp"],
361
+ environment: legacy.environment || {},
362
+ timeout: { startup: flatTimeout, catalog: flatTimeout, execution: flatTimeout },
363
+ };
364
+ } else if (data.mcp.servers.biomcp === undefined) {
365
+ data.mcp.servers.biomcp = {
366
+ type: "local",
367
+ command: [npxCmd, "-y", "-p", "biomcp@1.6.0", "biomcp"],
368
+ environment: {},
369
+ timeout: { startup: 120000, catalog: 120000, execution: 120000 },
370
+ };
371
+ }
372
+ // An existing canonical entry is left untouched; drop the migrated flat key.
373
+ if (legacy !== undefined) delete data.mcp.biomcp;
374
+ if (Object.keys(data.mcp).length === 0) delete data.mcp;
375
+ return data;
376
+ });
377
+ }
378
+
379
+ // Claude Code: .mcp.json
380
+ if (targetClaude) {
381
+ safeMergeJson(claudePath, (data) => {
382
+ data.mcpServers = data.mcpServers || {};
383
+ data.mcpServers.biomcp = {
384
+ type: "stdio",
385
+ command: normalizedNodePath,
386
+ args: [normalizedBundlePath],
387
+ env: data.mcpServers.biomcp?.env || {},
388
+ };
389
+ return data;
390
+ });
391
+ }
392
+
393
+ // Cursor: .cursor/mcp.json
394
+ if (targetCursor) {
395
+ safeMergeJson(cursorPath, (data) => {
396
+ data.mcpServers = data.mcpServers || {};
397
+ data.mcpServers.biomcp = {
398
+ command: normalizedNodePath,
399
+ args: [normalizedBundlePath],
400
+ env: data.mcpServers.biomcp?.env || {},
401
+ };
402
+ return data;
403
+ });
404
+ }
405
+
406
+ // ZCode: .zcode/config.json
407
+ if (targetZcode) {
408
+ safeMergeJson(zcodePath, (data) => {
409
+ data.mcp = data.mcp || {};
410
+ data.mcp.servers = data.mcp.servers || {};
411
+ data.mcp.servers.biomcp = {
412
+ command: normalizedNodePath,
413
+ args: [normalizedBundlePath],
414
+ env: data.mcp.servers.biomcp?.env || {},
415
+ enable: true,
416
+ };
417
+ return data;
418
+ });
419
+ }
420
+
421
+ // Pi Coding Agent: .pi/mcp.json
422
+ if (targetPi) {
423
+ safeMergeJson(piPath, (data) => {
424
+ data.mcpServers = data.mcpServers || {};
425
+ data.mcpServers.biomcp = {
426
+ command: normalizedNodePath,
427
+ args: [normalizedBundlePath],
428
+ env: data.mcpServers.biomcp?.env || {},
429
+ };
430
+ return data;
431
+ });
432
+ }
433
+
434
+ // CodeBuddy: .mcp.json + pre-approval in .codebuddy/settings.json
435
+ if (targetCodebuddy) {
436
+ safeMergeJson(claudePath, (data) => {
437
+ data.mcpServers = data.mcpServers || {};
438
+ data.mcpServers.biomcp = {
439
+ type: "stdio",
440
+ command: normalizedNodePath,
441
+ args: [normalizedBundlePath],
442
+ env: data.mcpServers.biomcp?.env || {},
443
+ };
444
+ return data;
445
+ });
446
+
447
+ safeMergeJson(codebuddySettingsPath, (data) => {
448
+ const approved = new Set(data.enabledMcpjsonServers || []);
449
+ approved.add("biomcp");
450
+ data.enabledMcpjsonServers = Array.from(approved);
451
+ return data;
452
+ });
453
+ }
454
+
455
+ // WorkBuddy: .workbuddy/mcp.json
456
+ if (targetWorkbuddy) {
457
+ safeMergeJson(workbuddyPath, (data) => {
458
+ data.mcpServers = data.mcpServers || {};
459
+ data.mcpServers.biomcp = {
460
+ type: "stdio",
461
+ command: normalizedNodePath,
462
+ args: [normalizedBundlePath],
463
+ env: data.mcpServers.biomcp?.env || {},
464
+ };
465
+ return data;
466
+ });
467
+ }
468
+
469
+ // 8. Run pre-flight verification via biomcp doctor --json
470
+ console.log("[bioresearcher-onboard] Running pre-flight verification via biomcp doctor...");
471
+ const cliPath = path.join(RUNTIME_ROOT, "node_modules", "biomcp", "dist", "cli.js");
472
+ const doctorRes = spawnSync(nodeBin, [cliPath, "doctor", "--json"], {
473
+ cwd: CWD,
474
+ encoding: "utf8",
475
+ });
476
+
477
+ if (doctorRes.status === 0) {
478
+ try {
479
+ const report = JSON.parse(doctorRes.stdout);
480
+ console.log(`[bioresearcher-onboard] ✓ Pre-flight check PASSED (schema v${report.schema_version})`);
481
+ console.log(` Node: ${report.node.version} (required ${report.node.required})`);
482
+ console.log(` Install mode: ${report.server_context.install_mode}`);
483
+ for (const f of report.features) {
484
+ console.log(` Feature ${f.id}: ${f.running_after_restart ? "ON after restart" : "off"}`);
485
+ }
486
+ } catch {
487
+ console.log("[bioresearcher-onboard] ✓ Doctor exit code 0");
488
+ }
489
+ } else {
490
+ console.warn("[bioresearcher-onboard] Warning: Doctor reported potential blockers:");
491
+ console.log(doctorRes.stdout || doctorRes.stderr);
492
+ }
493
+
494
+ // 9. Git Hygiene: Ensure .bioresearcher-runtime is gitignored
495
+ const gitignorePath = path.resolve(CWD, ".gitignore");
496
+ let gitignoreContent = "";
497
+ if (fs.existsSync(gitignorePath)) {
498
+ gitignoreContent = fs.readFileSync(gitignorePath, "utf8");
499
+ }
500
+ if (!gitignoreContent.includes(".bioresearcher-runtime")) {
501
+ const newline = gitignoreContent.length > 0 && !gitignoreContent.endsWith("\n") ? "\n" : "";
502
+ fs.appendFileSync(gitignorePath, `${newline}# BioMCP project-local runtime\n.bioresearcher-runtime/\n`);
503
+ console.log("[bioresearcher-onboard] Added .bioresearcher-runtime/ to .gitignore");
504
+ }
505
+
506
+ console.log("\n========================================================");
507
+ console.log("✓ BioMCP runtime bootstrapped successfully!");
508
+ console.log(` Runtime directory: .bioresearcher-runtime/`);
509
+ console.log(` Node.js: ${nodeBin}`);
510
+ console.log(` BioMCP bundle: ${bundlePath}`);
511
+ console.log("\n→ NEXT STEP: Restart your agent/client session to activate biomcp tools.");
512
+ console.log("========================================================\n");
package/loader.js DELETED
@@ -1,3 +0,0 @@
1
- // Root loader for OpenCode shallow plugin auto-discovery (.opencode/plugins/bioresearcher.js).
2
- export { default } from "./bioresearcher/index.js";
3
- export * from "./bioresearcher/index.js";