create-byan-agent 2.11.0 → 2.11.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.
- package/install/templates/_byan/mcp/byan-mcp-server/server.js +122 -9
- package/node_modules/byan-platform-config/README.md +107 -0
- package/node_modules/byan-platform-config/index.js +16 -0
- package/node_modules/byan-platform-config/lib/env-config.js +128 -0
- package/node_modules/byan-platform-config/lib/mcp-config.js +95 -0
- package/node_modules/byan-platform-config/lib/token-prompt.js +61 -0
- package/node_modules/byan-platform-config/lib/url-utils.js +27 -0
- package/node_modules/byan-platform-config/lib/validate.js +44 -0
- package/node_modules/byan-platform-config/package.json +42 -0
- package/node_modules/fs-extra/LICENSE +15 -0
- package/node_modules/fs-extra/README.md +294 -0
- package/node_modules/fs-extra/lib/copy/copy-sync.js +176 -0
- package/node_modules/fs-extra/lib/copy/copy.js +180 -0
- package/node_modules/fs-extra/lib/copy/index.js +7 -0
- package/node_modules/fs-extra/lib/empty/index.js +39 -0
- package/node_modules/fs-extra/lib/ensure/file.js +66 -0
- package/node_modules/fs-extra/lib/ensure/index.js +23 -0
- package/node_modules/fs-extra/lib/ensure/link.js +64 -0
- package/node_modules/fs-extra/lib/ensure/symlink-paths.js +101 -0
- package/node_modules/fs-extra/lib/ensure/symlink-type.js +34 -0
- package/node_modules/fs-extra/lib/ensure/symlink.js +92 -0
- package/node_modules/fs-extra/lib/esm.mjs +68 -0
- package/node_modules/fs-extra/lib/fs/index.js +146 -0
- package/node_modules/fs-extra/lib/index.js +16 -0
- package/node_modules/fs-extra/lib/json/index.js +16 -0
- package/node_modules/fs-extra/lib/json/jsonfile.js +11 -0
- package/node_modules/fs-extra/lib/json/output-json-sync.js +12 -0
- package/node_modules/fs-extra/lib/json/output-json.js +12 -0
- package/node_modules/fs-extra/lib/mkdirs/index.js +14 -0
- package/node_modules/fs-extra/lib/mkdirs/make-dir.js +27 -0
- package/node_modules/fs-extra/lib/mkdirs/utils.js +21 -0
- package/node_modules/fs-extra/lib/move/index.js +7 -0
- package/node_modules/fs-extra/lib/move/move-sync.js +55 -0
- package/node_modules/fs-extra/lib/move/move.js +59 -0
- package/node_modules/fs-extra/lib/output-file/index.js +31 -0
- package/node_modules/fs-extra/lib/path-exists/index.js +12 -0
- package/node_modules/fs-extra/lib/remove/index.js +17 -0
- package/node_modules/fs-extra/lib/util/async.js +29 -0
- package/node_modules/fs-extra/lib/util/stat.js +159 -0
- package/node_modules/fs-extra/lib/util/utimes.js +36 -0
- package/node_modules/fs-extra/package.json +71 -0
- package/node_modules/graceful-fs/LICENSE +15 -0
- package/node_modules/graceful-fs/README.md +143 -0
- package/node_modules/graceful-fs/clone.js +23 -0
- package/node_modules/graceful-fs/graceful-fs.js +448 -0
- package/node_modules/graceful-fs/legacy-streams.js +118 -0
- package/node_modules/graceful-fs/package.json +53 -0
- package/node_modules/graceful-fs/polyfills.js +355 -0
- package/node_modules/jsonfile/LICENSE +15 -0
- package/node_modules/jsonfile/README.md +230 -0
- package/node_modules/jsonfile/index.js +88 -0
- package/node_modules/jsonfile/package.json +40 -0
- package/node_modules/jsonfile/utils.js +18 -0
- package/node_modules/universalify/LICENSE +20 -0
- package/node_modules/universalify/README.md +76 -0
- package/node_modules/universalify/index.js +24 -0
- package/node_modules/universalify/package.json +34 -0
- package/package.json +4 -1
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import fsSync from 'node:fs';
|
|
3
|
+
import fsPromises from 'node:fs/promises';
|
|
4
|
+
import nodePath from 'node:path';
|
|
2
5
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
6
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
7
|
import {
|
|
@@ -94,6 +97,81 @@ async function apiRequest(path, options = {}) {
|
|
|
94
97
|
return body;
|
|
95
98
|
}
|
|
96
99
|
|
|
100
|
+
// Default filters — skip common build/vcs artifacts that pollute payload.
|
|
101
|
+
const DEFAULT_SKIP_DIRS = new Set([
|
|
102
|
+
'.git', 'node_modules', 'dist', 'build', '.next', 'coverage',
|
|
103
|
+
'__pycache__', '.venv', 'venv', '.pytest_cache', '.mypy_cache',
|
|
104
|
+
'target', 'out', '.turbo', '.cache', '.DS_Store',
|
|
105
|
+
]);
|
|
106
|
+
const DEFAULT_SKIP_FILE_PATTERNS = [
|
|
107
|
+
/\.log$/i, /\.sqlite$/i, /\.sqlite-journal$/i, /\.sqlite-wal$/i,
|
|
108
|
+
/\.lock$/i, /\.pid$/i,
|
|
109
|
+
];
|
|
110
|
+
// Heuristic: treat as binary if content has NUL byte in first 8KB.
|
|
111
|
+
function looksBinary(buf) {
|
|
112
|
+
const sample = buf.subarray(0, Math.min(buf.length, 8192));
|
|
113
|
+
for (const b of sample) if (b === 0) return true;
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Hard limits — match W1's API guards so we fail fast client-side.
|
|
118
|
+
const MAX_FILES = 10000;
|
|
119
|
+
const MAX_TOTAL_BYTES = 100 * 1024 * 1024; // 100 MB
|
|
120
|
+
|
|
121
|
+
async function buildFilesPayload(absRoot, opts = {}) {
|
|
122
|
+
const skipDirs = opts.skipDirs || DEFAULT_SKIP_DIRS;
|
|
123
|
+
const skipPatterns = opts.skipPatterns || DEFAULT_SKIP_FILE_PATTERNS;
|
|
124
|
+
const maxFiles = opts.maxFiles || MAX_FILES;
|
|
125
|
+
const maxBytes = opts.maxBytes || MAX_TOTAL_BYTES;
|
|
126
|
+
|
|
127
|
+
const stat = await fsPromises.stat(absRoot);
|
|
128
|
+
if (!stat.isDirectory()) {
|
|
129
|
+
throw new Error(`Path is not a directory: ${absRoot}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const files = [];
|
|
133
|
+
let totalBytes = 0;
|
|
134
|
+
|
|
135
|
+
async function walk(dir) {
|
|
136
|
+
const entries = await fsPromises.readdir(dir, { withFileTypes: true });
|
|
137
|
+
for (const entry of entries) {
|
|
138
|
+
const full = nodePath.join(dir, entry.name);
|
|
139
|
+
if (entry.isDirectory()) {
|
|
140
|
+
if (skipDirs.has(entry.name)) continue;
|
|
141
|
+
await walk(full);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (!entry.isFile()) continue;
|
|
145
|
+
if (skipPatterns.some((re) => re.test(entry.name))) continue;
|
|
146
|
+
|
|
147
|
+
const rel = nodePath.relative(absRoot, full).split(nodePath.sep).join('/');
|
|
148
|
+
const buf = await fsPromises.readFile(full);
|
|
149
|
+
|
|
150
|
+
totalBytes += buf.length;
|
|
151
|
+
if (files.length + 1 > maxFiles) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
`Too many files (>${maxFiles}). Add to skipDirs or increase maxFiles.`
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
if (totalBytes > maxBytes) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
`Total size exceeds ${(maxBytes / 1024 / 1024).toFixed(0)}MB. ` +
|
|
159
|
+
`Prune node_modules/dist/build dirs or increase maxBytes.`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (looksBinary(buf)) {
|
|
164
|
+
files.push({ path: rel, content: buf.toString('base64'), encoding: 'base64' });
|
|
165
|
+
} else {
|
|
166
|
+
files.push({ path: rel, content: buf.toString('utf8'), encoding: 'utf8' });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
await walk(absRoot);
|
|
172
|
+
return { files, count: files.length, totalBytes };
|
|
173
|
+
}
|
|
174
|
+
|
|
97
175
|
const tools = [
|
|
98
176
|
{
|
|
99
177
|
name: 'byan_ping',
|
|
@@ -124,13 +202,13 @@ const tools = [
|
|
|
124
202
|
{
|
|
125
203
|
name: 'byan_import_project',
|
|
126
204
|
description:
|
|
127
|
-
'Import a local project directory into byan_web.
|
|
205
|
+
'Import a local project directory into byan_web. Reads files from the local filesystem (client-side) and uploads them as a payload; works whether byan_web is local or remote. Skips .git, node_modules, dist, build, coverage, *.log, *.sqlite. Limits: 10000 files, 100MB total. Requires auth.',
|
|
128
206
|
inputSchema: {
|
|
129
207
|
type: 'object',
|
|
130
208
|
properties: {
|
|
131
209
|
path: {
|
|
132
210
|
type: 'string',
|
|
133
|
-
description: 'Absolute path to the project directory to
|
|
211
|
+
description: 'Absolute path to the project directory on THIS machine (the MCP client). The API does not need filesystem access to this path.',
|
|
134
212
|
},
|
|
135
213
|
name: { type: 'string', description: 'Optional project name override.' },
|
|
136
214
|
type: {
|
|
@@ -138,6 +216,14 @@ const tools = [
|
|
|
138
216
|
enum: ['dev', 'training'],
|
|
139
217
|
description: 'Project type. Default: dev.',
|
|
140
218
|
},
|
|
219
|
+
maxFiles: {
|
|
220
|
+
type: 'number',
|
|
221
|
+
description: 'Override max file count (default 10000).',
|
|
222
|
+
},
|
|
223
|
+
maxBytes: {
|
|
224
|
+
type: 'number',
|
|
225
|
+
description: 'Override max total bytes (default 104857600 = 100MB).',
|
|
226
|
+
},
|
|
141
227
|
},
|
|
142
228
|
required: ['path'],
|
|
143
229
|
additionalProperties: false,
|
|
@@ -820,10 +906,14 @@ const tools = [
|
|
|
820
906
|
{
|
|
821
907
|
name: 'byan_api_import_scan',
|
|
822
908
|
description:
|
|
823
|
-
'Scan a local directory and report what would be imported.
|
|
909
|
+
'Scan a local directory and report what would be imported into byan_web. Reads files from the local filesystem (client-side) and uploads them as a payload; works whether byan_web is local or remote. Skips .git, node_modules, dist, build, coverage, *.log, *.sqlite. Limits: 10000 files, 100MB total. Requires auth.',
|
|
824
910
|
inputSchema: {
|
|
825
911
|
type: 'object',
|
|
826
|
-
properties: {
|
|
912
|
+
properties: {
|
|
913
|
+
path: { type: 'string', description: 'Absolute path to the directory on THIS machine (the MCP client). The API does not need filesystem access to this path.' },
|
|
914
|
+
maxFiles: { type: 'number', description: 'Override max file count (default 10000).' },
|
|
915
|
+
maxBytes: { type: 'number', description: 'Override max total bytes (default 104857600 = 100MB).' },
|
|
916
|
+
},
|
|
827
917
|
required: ['path'],
|
|
828
918
|
additionalProperties: false,
|
|
829
919
|
},
|
|
@@ -831,10 +921,14 @@ const tools = [
|
|
|
831
921
|
{
|
|
832
922
|
name: 'byan_api_import_dry_run',
|
|
833
923
|
description:
|
|
834
|
-
'Dry-run an import from a local directory (no writes).
|
|
924
|
+
'Dry-run an import from a local directory into byan_web (no writes). Reads files from the local filesystem (client-side) and uploads them as a payload; works whether byan_web is local or remote. Skips .git, node_modules, dist, build, coverage, *.log, *.sqlite. Limits: 10000 files, 100MB total. Requires auth.',
|
|
835
925
|
inputSchema: {
|
|
836
926
|
type: 'object',
|
|
837
|
-
properties: {
|
|
927
|
+
properties: {
|
|
928
|
+
path: { type: 'string', description: 'Absolute path to the directory on THIS machine (the MCP client). The API does not need filesystem access to this path.' },
|
|
929
|
+
maxFiles: { type: 'number', description: 'Override max file count (default 10000).' },
|
|
930
|
+
maxBytes: { type: 'number', description: 'Override max total bytes (default 104857600 = 100MB).' },
|
|
931
|
+
},
|
|
838
932
|
required: ['path'],
|
|
839
933
|
additionalProperties: false,
|
|
840
934
|
},
|
|
@@ -899,10 +993,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
899
993
|
if (!BYAN_API_TOKEN) {
|
|
900
994
|
throw new Error('BYAN_API_TOKEN env var is required for this tool.');
|
|
901
995
|
}
|
|
996
|
+
// Always upload files payload — works for both localhost and remote API.
|
|
997
|
+
// The API still accepts { path } for backward compat if caller insists,
|
|
998
|
+
// but the MCP client has no reason to use it (we can always read locally).
|
|
999
|
+
const { files } = await buildFilesPayload(args.path, {
|
|
1000
|
+
...(args.maxFiles ? { maxFiles: args.maxFiles } : {}),
|
|
1001
|
+
...(args.maxBytes ? { maxBytes: args.maxBytes } : {}),
|
|
1002
|
+
});
|
|
902
1003
|
const body = await apiRequest('/api/import/project', {
|
|
903
1004
|
method: 'POST',
|
|
904
1005
|
body: JSON.stringify({
|
|
905
|
-
|
|
1006
|
+
files,
|
|
906
1007
|
...(args.name ? { name: args.name } : {}),
|
|
907
1008
|
...(args.type ? { type: args.type } : {}),
|
|
908
1009
|
}),
|
|
@@ -1298,18 +1399,28 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1298
1399
|
|
|
1299
1400
|
if (name === 'byan_api_import_scan') {
|
|
1300
1401
|
requireToken();
|
|
1402
|
+
// Build files payload from client filesystem — works for remote byan_web.
|
|
1403
|
+
const { files } = await buildFilesPayload(args.path, {
|
|
1404
|
+
...(args.maxFiles ? { maxFiles: args.maxFiles } : {}),
|
|
1405
|
+
...(args.maxBytes ? { maxBytes: args.maxBytes } : {}),
|
|
1406
|
+
});
|
|
1301
1407
|
const body = await apiRequest('/api/import/scan', {
|
|
1302
1408
|
method: 'POST',
|
|
1303
|
-
body: JSON.stringify({
|
|
1409
|
+
body: JSON.stringify({ files }),
|
|
1304
1410
|
});
|
|
1305
1411
|
return { content: [{ type: 'text', text: JSON.stringify(body, null, 2) }] };
|
|
1306
1412
|
}
|
|
1307
1413
|
|
|
1308
1414
|
if (name === 'byan_api_import_dry_run') {
|
|
1309
1415
|
requireToken();
|
|
1416
|
+
// Build files payload from client filesystem — works for remote byan_web.
|
|
1417
|
+
const { files } = await buildFilesPayload(args.path, {
|
|
1418
|
+
...(args.maxFiles ? { maxFiles: args.maxFiles } : {}),
|
|
1419
|
+
...(args.maxBytes ? { maxBytes: args.maxBytes } : {}),
|
|
1420
|
+
});
|
|
1310
1421
|
const body = await apiRequest('/api/import/dry-run', {
|
|
1311
1422
|
method: 'POST',
|
|
1312
|
-
body: JSON.stringify({
|
|
1423
|
+
body: JSON.stringify({ files }),
|
|
1313
1424
|
});
|
|
1314
1425
|
return { content: [{ type: 'text', text: JSON.stringify(body, null, 2) }] };
|
|
1315
1426
|
}
|
|
@@ -1325,3 +1436,5 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1325
1436
|
|
|
1326
1437
|
const transport = new StdioServerTransport();
|
|
1327
1438
|
await server.connect(transport);
|
|
1439
|
+
|
|
1440
|
+
export { buildFilesPayload };
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# byan-platform-config
|
|
2
|
+
|
|
3
|
+
Shared platform config primitives for BYAN. Single source of truth for
|
|
4
|
+
`.mcp.json`, `.env`, `.claude/settings.local.json`, token prompting,
|
|
5
|
+
byan_web reachability validation, and URL normalization.
|
|
6
|
+
|
|
7
|
+
Consumed by both `create-byan-agent` (installer) and `update-byan-agent`
|
|
8
|
+
(update CLI) so the two CLIs stay in sync.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install byan-platform-config
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Public API
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
const {
|
|
20
|
+
mcpConfig,
|
|
21
|
+
envConfig,
|
|
22
|
+
tokenPrompt,
|
|
23
|
+
validate,
|
|
24
|
+
urlUtils,
|
|
25
|
+
} = require('byan-platform-config');
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### mcpConfig — `.mcp.json` management
|
|
29
|
+
|
|
30
|
+
| Function | Signature | Description |
|
|
31
|
+
|----------|-----------|-------------|
|
|
32
|
+
| `ensureMcpConfig` | `(projectRoot, { apiUrl, token }) → Promise<{ path }>` | READ-MERGE-WRITE. Strips `/api` suffix, preserves other mcpServers, preserves existing byan command/args. |
|
|
33
|
+
| `readMcpConfig` | `(projectRoot) → Promise<object\|null>` | Returns parsed `.mcp.json` or `null` if missing/malformed. |
|
|
34
|
+
| `mergeByanEntry` | `(existingConfig, { apiUrl, token }) → object` | Pure merge — no I/O. Returns a new config object. |
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
await mcpConfig.ensureMcpConfig('/path/to/proj', {
|
|
38
|
+
apiUrl: 'http://localhost:3737',
|
|
39
|
+
token: 'byan_abc123',
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### envConfig — `.env` and `settings.local.json`
|
|
44
|
+
|
|
45
|
+
| Function | Signature | Description |
|
|
46
|
+
|----------|-----------|-------------|
|
|
47
|
+
| `updateSettingsLocal` | `(projectRoot, envVars) → Promise<{ path }>` | Merges vars into `.claude/settings.local.json`, preserves unrelated keys. |
|
|
48
|
+
| `updateDotenv` | `(projectRoot, envVars) → Promise<{ path }>` | Appends/updates `.env`, preserves comments and blank lines. |
|
|
49
|
+
| `readEnvToken` | `(projectRoot) → Promise<string\|null>` | Reads `BYAN_API_TOKEN` with fallback chain : `.env` then `settings.local.json`. |
|
|
50
|
+
|
|
51
|
+
```js
|
|
52
|
+
await envConfig.updateDotenv('/path/to/proj', { BYAN_API_TOKEN: 'tok' });
|
|
53
|
+
const tok = await envConfig.readEnvToken('/path/to/proj');
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### tokenPrompt — interactive prompt
|
|
57
|
+
|
|
58
|
+
| Symbol | Signature / Value | Description |
|
|
59
|
+
|--------|-------------------|-------------|
|
|
60
|
+
| `promptForToken` | `() → Promise<{ configured, apiUrl?, token? }>` | Inquirer prompt (confirm + URL + password). |
|
|
61
|
+
| `ENV_KEYS` | `['BYAN_API_TOKEN', 'BYAN_API_URL']` | Canonical env var names. |
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
const res = await tokenPrompt.promptForToken();
|
|
65
|
+
if (res.configured) { /* use res.apiUrl, res.token */ }
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### validate — reachability probe
|
|
69
|
+
|
|
70
|
+
| Function | Signature | Description |
|
|
71
|
+
|----------|-----------|-------------|
|
|
72
|
+
| `validateByanWebReachability` | `({ apiUrl, token?, timeoutMs? }) → Promise<{ reachable, status?, latencyMs?, error? }>` | GET `/api/health`. Errors surface in the result object instead of throwing. Default timeout 5000 ms. |
|
|
73
|
+
|
|
74
|
+
```js
|
|
75
|
+
const r = await validate.validateByanWebReachability({
|
|
76
|
+
apiUrl: 'http://localhost:3737',
|
|
77
|
+
token: 'byan_abc',
|
|
78
|
+
});
|
|
79
|
+
// → { reachable: true, status: 200, latencyMs: 12 }
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### urlUtils — URL normalization
|
|
83
|
+
|
|
84
|
+
| Function | Signature | Description |
|
|
85
|
+
|----------|-----------|-------------|
|
|
86
|
+
| `stripApiSuffix` | `(url) → string` | Strips trailing `/api`, `/api/`, `/api/v1`, etc. |
|
|
87
|
+
| `buildAuthHeader` | `(token) → { Authorization } \| {}` | `ApiKey` scheme for `byan_*` tokens, `Bearer` otherwise. |
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
urlUtils.stripApiSuffix('http://x:1/api'); // → 'http://x:1'
|
|
91
|
+
urlUtils.buildAuthHeader('byan_abc'); // → { Authorization: 'ApiKey byan_abc' }
|
|
92
|
+
urlUtils.buildAuthHeader('eyJ...'); // → { Authorization: 'Bearer eyJ...' }
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Test
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
npm test
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Design invariants
|
|
102
|
+
|
|
103
|
+
- READ-MERGE-WRITE on every file operation — unknown keys are preserved.
|
|
104
|
+
- `ensureMcpConfig` keeps `mcpServers.byan.command` and `.args` if already set.
|
|
105
|
+
- `validateByanWebReachability` resolves instead of rejecting — errors surface in the result object.
|
|
106
|
+
- `stripApiSuffix` is idempotent and leaves URLs without a `/api` suffix untouched.
|
|
107
|
+
- `buildAuthHeader` returns `{}` (not `{ Authorization: undefined }`) when token is missing.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* byan-platform-config — shared platform config primitives.
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for .mcp.json management, .env/settings.local.json
|
|
5
|
+
* manipulation, token prompting, byan_web reachability validation, and URL
|
|
6
|
+
* normalization. Consumed by both install/ (create-byan-agent) and
|
|
7
|
+
* update-byan-agent/ so the two CLIs do not drift.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
module.exports = {
|
|
11
|
+
mcpConfig: require('./lib/mcp-config'),
|
|
12
|
+
envConfig: require('./lib/env-config'),
|
|
13
|
+
tokenPrompt: require('./lib/token-prompt'),
|
|
14
|
+
validate: require('./lib/validate'),
|
|
15
|
+
urlUtils: require('./lib/url-utils'),
|
|
16
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* .env and .claude/settings.local.json management.
|
|
3
|
+
*
|
|
4
|
+
* updateSettingsLocal : merge env vars into .claude/settings.local.json,
|
|
5
|
+
* preserving unrelated keys (permissions, hooks, etc.).
|
|
6
|
+
* updateDotenv : append/update .env lines, preserving comments and
|
|
7
|
+
* blank lines, replacing (not duplicating) existing keys.
|
|
8
|
+
* readEnvToken : fallback chain to read BYAN_API_TOKEN for migrations.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const fs = require('fs-extra');
|
|
13
|
+
|
|
14
|
+
async function readJsonOrEmpty(filePath) {
|
|
15
|
+
if (await fs.pathExists(filePath)) {
|
|
16
|
+
try {
|
|
17
|
+
return await fs.readJson(filePath);
|
|
18
|
+
} catch {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {string} projectRoot
|
|
27
|
+
* @param {Record<string,string>} envVars
|
|
28
|
+
* @returns {Promise<{ path: string }>}
|
|
29
|
+
*/
|
|
30
|
+
async function updateSettingsLocal(projectRoot, envVars) {
|
|
31
|
+
const filePath = path.join(projectRoot, '.claude', 'settings.local.json');
|
|
32
|
+
const current = await readJsonOrEmpty(filePath);
|
|
33
|
+
current.env = { ...(current.env || {}), ...envVars };
|
|
34
|
+
await fs.ensureDir(path.dirname(filePath));
|
|
35
|
+
await fs.writeJson(filePath, current, { spaces: 2 });
|
|
36
|
+
return { path: filePath };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {string} projectRoot
|
|
41
|
+
* @param {Record<string,string>} envVars
|
|
42
|
+
* @returns {Promise<{ path: string }>}
|
|
43
|
+
*/
|
|
44
|
+
async function updateDotenv(projectRoot, envVars) {
|
|
45
|
+
const filePath = path.join(projectRoot, '.env');
|
|
46
|
+
let content = '';
|
|
47
|
+
if (await fs.pathExists(filePath)) {
|
|
48
|
+
content = await fs.readFile(filePath, 'utf8');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const lines = content ? content.split(/\r?\n/) : [];
|
|
52
|
+
const keys = Object.keys(envVars);
|
|
53
|
+
const kept = lines.filter((line) => {
|
|
54
|
+
const trimmed = line.trim();
|
|
55
|
+
if (!trimmed || trimmed.startsWith('#')) return true;
|
|
56
|
+
const eq = trimmed.indexOf('=');
|
|
57
|
+
if (eq < 0) return true;
|
|
58
|
+
const key = trimmed.slice(0, eq).trim();
|
|
59
|
+
return !keys.includes(key);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
while (kept.length && kept[kept.length - 1] === '') kept.pop();
|
|
63
|
+
for (const key of keys) {
|
|
64
|
+
const val = envVars[key] ?? '';
|
|
65
|
+
kept.push(`${key}=${val}`);
|
|
66
|
+
}
|
|
67
|
+
kept.push('');
|
|
68
|
+
|
|
69
|
+
await fs.writeFile(filePath, kept.join('\n'), 'utf8');
|
|
70
|
+
return { path: filePath };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Parses a single `KEY=value` line, respecting surrounding double quotes.
|
|
75
|
+
* Returns null for comment/empty/malformed lines.
|
|
76
|
+
*/
|
|
77
|
+
function parseDotenvLine(line) {
|
|
78
|
+
const trimmed = line.trim();
|
|
79
|
+
if (!trimmed || trimmed.startsWith('#')) return null;
|
|
80
|
+
const eq = trimmed.indexOf('=');
|
|
81
|
+
if (eq < 0) return null;
|
|
82
|
+
const key = trimmed.slice(0, eq).trim();
|
|
83
|
+
let val = trimmed.slice(eq + 1).trim();
|
|
84
|
+
if (val.length >= 2 && val.startsWith('"') && val.endsWith('"')) {
|
|
85
|
+
val = val.slice(1, -1);
|
|
86
|
+
}
|
|
87
|
+
return { key, value: val };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Reads BYAN_API_TOKEN from .env first, falls back to
|
|
92
|
+
* .claude/settings.local.json env.BYAN_API_TOKEN. Returns null if neither
|
|
93
|
+
* contains a non-empty value.
|
|
94
|
+
*
|
|
95
|
+
* @param {string} projectRoot
|
|
96
|
+
* @returns {Promise<string|null>}
|
|
97
|
+
*/
|
|
98
|
+
async function readEnvToken(projectRoot) {
|
|
99
|
+
const dotenvPath = path.join(projectRoot, '.env');
|
|
100
|
+
if (await fs.pathExists(dotenvPath)) {
|
|
101
|
+
const content = await fs.readFile(dotenvPath, 'utf8');
|
|
102
|
+
for (const line of content.split(/\r?\n/)) {
|
|
103
|
+
const parsed = parseDotenvLine(line);
|
|
104
|
+
if (parsed && parsed.key === 'BYAN_API_TOKEN' && parsed.value) {
|
|
105
|
+
return parsed.value;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const settingsPath = path.join(projectRoot, '.claude', 'settings.local.json');
|
|
111
|
+
if (await fs.pathExists(settingsPath)) {
|
|
112
|
+
try {
|
|
113
|
+
const settings = await fs.readJson(settingsPath);
|
|
114
|
+
const tok = settings && settings.env && settings.env.BYAN_API_TOKEN;
|
|
115
|
+
if (typeof tok === 'string' && tok.length > 0) return tok;
|
|
116
|
+
} catch {
|
|
117
|
+
// ignore malformed json
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = {
|
|
125
|
+
updateSettingsLocal,
|
|
126
|
+
updateDotenv,
|
|
127
|
+
readEnvToken,
|
|
128
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* .mcp.json management.
|
|
3
|
+
*
|
|
4
|
+
* READ-MERGE-WRITE semantics : preserves all existing mcpServers.* entries
|
|
5
|
+
* and, if byan entry already exists, preserves its command/args. Only the
|
|
6
|
+
* env.BYAN_API_URL and env.BYAN_API_TOKEN are authoritative from caller.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const fs = require('fs-extra');
|
|
11
|
+
const { stripApiSuffix } = require('./url-utils');
|
|
12
|
+
|
|
13
|
+
const MCP_SERVER_REL_PATH = '_byan/mcp/byan-mcp-server/server.js';
|
|
14
|
+
|
|
15
|
+
async function readJsonOrEmpty(filePath) {
|
|
16
|
+
if (await fs.pathExists(filePath)) {
|
|
17
|
+
try {
|
|
18
|
+
return await fs.readJson(filePath);
|
|
19
|
+
} catch {
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Reads the project's .mcp.json.
|
|
28
|
+
*
|
|
29
|
+
* @param {string} projectRoot
|
|
30
|
+
* @returns {Promise<object|null>} parsed config or null if missing/malformed.
|
|
31
|
+
*/
|
|
32
|
+
async function readMcpConfig(projectRoot) {
|
|
33
|
+
const filePath = path.join(projectRoot, '.mcp.json');
|
|
34
|
+
if (!(await fs.pathExists(filePath))) return null;
|
|
35
|
+
try {
|
|
36
|
+
return await fs.readJson(filePath);
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Pure merge — no I/O. Returns a new config object with byan entry merged.
|
|
44
|
+
* Useful for migrations that inspect the diff before writing.
|
|
45
|
+
*
|
|
46
|
+
* @param {object} existingConfig — current parsed config (may be {} or {mcpServers:{...}})
|
|
47
|
+
* @param {{ apiUrl: string, token?: string }} opts
|
|
48
|
+
* @returns {object} new merged config
|
|
49
|
+
*/
|
|
50
|
+
function mergeByanEntry(existingConfig, { apiUrl, token } = {}) {
|
|
51
|
+
const cfg = existingConfig && typeof existingConfig === 'object' ? { ...existingConfig } : {};
|
|
52
|
+
cfg.mcpServers = { ...(cfg.mcpServers || {}) };
|
|
53
|
+
|
|
54
|
+
const existing = cfg.mcpServers.byan || {};
|
|
55
|
+
const cleanUrl = stripApiSuffix(apiUrl);
|
|
56
|
+
|
|
57
|
+
const env = { ...(existing.env || {}) };
|
|
58
|
+
env.BYAN_API_URL = cleanUrl;
|
|
59
|
+
if (token && typeof token === 'string' && token.length > 0) {
|
|
60
|
+
env.BYAN_API_TOKEN = token;
|
|
61
|
+
} else {
|
|
62
|
+
delete env.BYAN_API_TOKEN;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
cfg.mcpServers.byan = {
|
|
66
|
+
command: 'node',
|
|
67
|
+
args: [MCP_SERVER_REL_PATH],
|
|
68
|
+
...existing,
|
|
69
|
+
env,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return cfg;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Ensures .mcp.json exists with a valid byan entry. READ-MERGE-WRITE.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} projectRoot
|
|
79
|
+
* @param {{ apiUrl: string, token?: string }} opts
|
|
80
|
+
* @returns {Promise<{ path: string }>}
|
|
81
|
+
*/
|
|
82
|
+
async function ensureMcpConfig(projectRoot, { apiUrl, token } = {}) {
|
|
83
|
+
const filePath = path.join(projectRoot, '.mcp.json');
|
|
84
|
+
const current = await readJsonOrEmpty(filePath);
|
|
85
|
+
const merged = mergeByanEntry(current, { apiUrl, token });
|
|
86
|
+
await fs.writeJson(filePath, merged, { spaces: 2 });
|
|
87
|
+
return { path: filePath };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = {
|
|
91
|
+
ensureMcpConfig,
|
|
92
|
+
readMcpConfig,
|
|
93
|
+
mergeByanEntry,
|
|
94
|
+
MCP_SERVER_REL_PATH,
|
|
95
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive prompt for byan_web API URL + JWT/ApiKey token.
|
|
3
|
+
*
|
|
4
|
+
* Extracted verbatim from install's byan-web-integration so the UX does
|
|
5
|
+
* not drift between create-byan-agent and update-byan-agent.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const inquirer = require('inquirer');
|
|
9
|
+
const chalk = require('chalk');
|
|
10
|
+
|
|
11
|
+
const DEFAULT_API_URL = 'http://localhost:3737';
|
|
12
|
+
const ENV_KEYS = ['BYAN_API_TOKEN', 'BYAN_API_URL'];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @returns {Promise<{ configured: boolean, apiUrl?: string, token?: string }>}
|
|
16
|
+
*/
|
|
17
|
+
async function promptForToken() {
|
|
18
|
+
const { wantsToken } = await inquirer.prompt([
|
|
19
|
+
{
|
|
20
|
+
type: 'confirm',
|
|
21
|
+
name: 'wantsToken',
|
|
22
|
+
message:
|
|
23
|
+
'Connect this project to your byan_web instance ? ' +
|
|
24
|
+
chalk.yellow('(service payant — requires a paid subscription to generate a token)'),
|
|
25
|
+
default: false,
|
|
26
|
+
},
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
if (!wantsToken) return { configured: false };
|
|
30
|
+
|
|
31
|
+
const answers = await inquirer.prompt([
|
|
32
|
+
{
|
|
33
|
+
type: 'input',
|
|
34
|
+
name: 'apiUrl',
|
|
35
|
+
message: 'byan_web API URL:',
|
|
36
|
+
default: DEFAULT_API_URL,
|
|
37
|
+
validate: (v) =>
|
|
38
|
+
/^https?:\/\//.test(v.trim()) || 'Must start with http:// or https://',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
type: 'password',
|
|
42
|
+
name: 'token',
|
|
43
|
+
message: 'byan_web JWT token (from POST /api/auth/login):',
|
|
44
|
+
mask: '*',
|
|
45
|
+
validate: (v) =>
|
|
46
|
+
(typeof v === 'string' && v.trim().length > 0) || 'Token cannot be empty',
|
|
47
|
+
},
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
configured: true,
|
|
52
|
+
apiUrl: answers.apiUrl.trim(),
|
|
53
|
+
token: answers.token.trim(),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = {
|
|
58
|
+
promptForToken,
|
|
59
|
+
ENV_KEYS,
|
|
60
|
+
DEFAULT_API_URL,
|
|
61
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL utilities for BYAN platform config.
|
|
3
|
+
*
|
|
4
|
+
* stripApiSuffix : normalize apiUrl so the MCP server.js doesn't double the
|
|
5
|
+
* /api prefix (see install B5 bugfix).
|
|
6
|
+
* buildAuthHeader : ApiKey scheme for user-generated byan_ tokens,
|
|
7
|
+
* Bearer scheme for JWT tokens.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
function stripApiSuffix(url) {
|
|
11
|
+
if (typeof url !== 'string' || url.length === 0) return url;
|
|
12
|
+
// Strip trailing /api, /api/, /api/v1, /api/v1/ etc. — keep protocol/host intact.
|
|
13
|
+
return url.replace(/\/api(?:\/v\d+)?\/?$/, '');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function buildAuthHeader(token) {
|
|
17
|
+
if (!token || typeof token !== 'string' || token.length === 0) {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
const scheme = token.startsWith('byan_') ? 'ApiKey' : 'Bearer';
|
|
21
|
+
return { Authorization: `${scheme} ${token}` };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = {
|
|
25
|
+
stripApiSuffix,
|
|
26
|
+
buildAuthHeader,
|
|
27
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* byan_web reachability probe.
|
|
3
|
+
*
|
|
4
|
+
* Never throws — always resolves with a plain result object. Uses
|
|
5
|
+
* AbortController to enforce a hard timeout (default 5s).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { stripApiSuffix, buildAuthHeader } = require('./url-utils');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {{ apiUrl: string, token?: string, timeoutMs?: number }} opts
|
|
12
|
+
* @returns {Promise<{ reachable: boolean, status?: number, latencyMs?: number, error?: string }>}
|
|
13
|
+
*/
|
|
14
|
+
async function validateByanWebReachability({ apiUrl, token, timeoutMs = 5000 }) {
|
|
15
|
+
const controller = new AbortController();
|
|
16
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
17
|
+
|
|
18
|
+
const headers = buildAuthHeader(token);
|
|
19
|
+
const url = `${stripApiSuffix(apiUrl)}/api/health`;
|
|
20
|
+
const t0 = Date.now();
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const res = await fetch(url, { method: 'GET', headers, signal: controller.signal });
|
|
24
|
+
const latencyMs = Date.now() - t0;
|
|
25
|
+
clearTimeout(timer);
|
|
26
|
+
|
|
27
|
+
if (res.status >= 200 && res.status < 400) {
|
|
28
|
+
return { reachable: true, status: res.status, latencyMs };
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
reachable: true,
|
|
32
|
+
status: res.status,
|
|
33
|
+
latencyMs,
|
|
34
|
+
error: `HTTP ${res.status}`,
|
|
35
|
+
};
|
|
36
|
+
} catch (err) {
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
return { reachable: false, error: err.message || String(err) };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = {
|
|
43
|
+
validateByanWebReachability,
|
|
44
|
+
};
|