create-byan-agent 2.13.1 → 2.14.1

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.
@@ -16,9 +16,83 @@ const { generateProjectAgentsDoc } = require('../lib/project-agents-generator');
16
16
  const { launchPhase2Chat, generateDefaultConfig } = require('../lib/phase2-chat');
17
17
  const { setupByanWebIntegration, validateByanWebReachability } = require('../lib/byan-web-integration');
18
18
  const { setupClaudeNative } = require('../lib/claude-native-setup');
19
+ const { setupCodexNative } = require('../lib/codex-native-setup');
19
20
  const { setupStagingConsent } = require('../lib/staging-consent');
21
+ const { getLatestVersion, compareVersions } = require('../lib/utils/version-compare');
20
22
 
21
- const BYAN_VERSION = require('../package.json').version;
23
+ // Version source-of-truth is the root package.json (the one npm publishes).
24
+ // install/package.json used to be read here, but it carries an unrelated
25
+ // internal version that drifted (2.11.0 while the npm package shipped 2.14.0)
26
+ // and silently mis-flagged users as obsolete. Resolve via path so it works
27
+ // both in the published tarball and in the dev repo.
28
+ const BYAN_VERSION = require(path.resolve(__dirname, '..', '..', 'package.json')).version;
29
+
30
+ // Versions strictly below this floor have known install bugs (e.g. v2.9.8 ships
31
+ // the broken node_modules filter that copies an empty MCP server dir). They
32
+ // are blocked outright. Above the floor, an outdated version becomes a soft
33
+ // prompt — the user can choose to continue or upgrade.
34
+ const KNOWN_BAD_VERSION_FLOOR = '2.13.1';
35
+
36
+ async function assertVersionFresh({ skip = false, timeoutMs = 5000 } = {}) {
37
+ if (skip || process.env.BYAN_SKIP_VERSION_CHECK === '1') return;
38
+ let latest;
39
+ try {
40
+ latest = await Promise.race([
41
+ getLatestVersion('create-byan-agent'),
42
+ new Promise((_, rej) => setTimeout(() => rej(new Error('npm-timeout')), timeoutMs)),
43
+ ]);
44
+ } catch (err) {
45
+ console.log(chalk.gray(` (version check skipped: ${err.message})`));
46
+ return;
47
+ }
48
+ if (compareVersions(BYAN_VERSION, latest) >= 0) return;
49
+
50
+ // Hard block when below the known-bad floor — these versions corrupt installs.
51
+ if (compareVersions(BYAN_VERSION, KNOWN_BAD_VERSION_FLOOR) < 0) {
52
+ console.error('');
53
+ console.error(chalk.red.bold(' BYAN installer is obsolete (known install bugs)'));
54
+ console.error('');
55
+ console.error(chalk.yellow(` Installed: ${BYAN_VERSION}`));
56
+ console.error(chalk.yellow(` Latest: ${latest}`));
57
+ console.error(chalk.yellow(` Floor: ${KNOWN_BAD_VERSION_FLOOR} (older versions silently produce empty MCP servers)`));
58
+ console.error('');
59
+ console.error(chalk.cyan(' Upgrade required:'));
60
+ console.error(chalk.bold(' npm i -g create-byan-agent@latest'));
61
+ console.error('');
62
+ console.error(chalk.gray(' Bypass (not recommended): --skip-version-check or BYAN_SKIP_VERSION_CHECK=1'));
63
+ console.error('');
64
+ process.exit(1);
65
+ }
66
+
67
+ // Soft prompt above the floor — propose upgrade interactively.
68
+ console.log('');
69
+ console.log(chalk.yellow.bold(` BYAN ${BYAN_VERSION} -> ${latest} available on npm`));
70
+ console.log(chalk.gray(' Newer release published. Continue with current, or abort to upgrade?'));
71
+ if (!process.stdin.isTTY) {
72
+ console.log(chalk.gray(' (non-TTY context — continuing without prompt)'));
73
+ return;
74
+ }
75
+ const { proceed } = await inquirer.prompt([
76
+ {
77
+ type: 'list',
78
+ name: 'proceed',
79
+ message: 'How do you want to proceed ?',
80
+ choices: [
81
+ { name: `Continue with v${BYAN_VERSION}`, value: 'continue' },
82
+ { name: `Abort and upgrade (npm i -g create-byan-agent@latest)`, value: 'abort' },
83
+ ],
84
+ default: 'continue',
85
+ },
86
+ ]);
87
+ if (proceed === 'abort') {
88
+ console.log('');
89
+ console.log(chalk.cyan(' Run:'));
90
+ console.log(chalk.bold(' npm i -g create-byan-agent@latest'));
91
+ console.log(chalk.gray(' Then re-run `npx create-byan-agent`.'));
92
+ console.log('');
93
+ process.exit(0);
94
+ }
95
+ }
22
96
 
23
97
  // ASCII Art Banner
24
98
  const banner = `
@@ -256,10 +330,12 @@ async function mergePackageJson(templateDir, projectRoot, spinner) {
256
330
  }
257
331
 
258
332
  // Main installer
259
- async function install() {
333
+ async function install(options = {}) {
260
334
  console.clear();
261
335
  console.log(banner);
262
-
336
+
337
+ await assertVersionFresh({ skip: options.skipVersionCheck });
338
+
263
339
  const projectRoot = process.cwd();
264
340
 
265
341
  // Step 1: Detect project type
@@ -1358,6 +1434,21 @@ async function install() {
1358
1434
  }
1359
1435
  }
1360
1436
 
1437
+ if (needsCodex) {
1438
+ console.log();
1439
+ console.log(chalk.cyan('Codex CLI MCP setup (~/.codex/config.toml)'));
1440
+ try {
1441
+ await setupCodexNative(projectRoot);
1442
+ } catch (error) {
1443
+ console.log(chalk.red(` ✘ Codex MCP setup failed: ${error.message}`));
1444
+ console.log(
1445
+ chalk.yellow(
1446
+ ` → Edit ~/.codex/config.toml manually to add the byan MCP entry.`
1447
+ )
1448
+ );
1449
+ }
1450
+ }
1451
+
1361
1452
  if (needsClaude || needsCopilot) {
1362
1453
  console.log();
1363
1454
  console.log(chalk.cyan('byan_web integration (optional — service payant)'));
@@ -1753,6 +1844,7 @@ program
1753
1844
  .name('create-byan-agent')
1754
1845
  .description('Install BYAN v2.2.0 - Builder of YAN with Model Selector and multi-platform support')
1755
1846
  .version(BYAN_VERSION)
1847
+ .option('--skip-version-check', 'Bypass the npm freshness guard (not recommended)')
1756
1848
  .action(install);
1757
1849
 
1758
1850
  // Update Command (Yanstaller v3)
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Codex CLI native setup — wires the BYAN MCP server into ~/.codex/config.toml
3
+ * during `npx create-byan-agent`.
4
+ *
5
+ * Mirrors claude-native-setup.js but targets Codex CLI's user-level TOML config.
6
+ * Per Codex docs (developers.openai.com/codex/mcp), MCP entries support:
7
+ * command, args, env, env_vars, cwd, startup_timeout_sec, tool_timeout_sec,
8
+ * enabled, required, enabled_tools, disabled_tools, supports_parallel_tool_calls.
9
+ *
10
+ * Idempotent: strips any existing [mcp_servers.byan*] sections before writing
11
+ * the fresh block. Other servers in config.toml are preserved untouched.
12
+ *
13
+ * Never persists a token: BYAN_API_TOKEN is left blank if not supplied via env
14
+ * or interactive prompt; the user gets a clear edit instruction.
15
+ */
16
+
17
+ const fs = require('fs-extra');
18
+ const path = require('path');
19
+ const os = require('os');
20
+ const chalk = require('chalk');
21
+
22
+ const SERVER_NAME = 'byan';
23
+
24
+ function getCodexConfigPath() {
25
+ return path.join(os.homedir(), '.codex', 'config.toml');
26
+ }
27
+
28
+ async function detectCodex() {
29
+ return fs.pathExists(path.join(os.homedir(), '.codex'));
30
+ }
31
+
32
+ // TOML literal-string escape: single quotes don't interpret backslashes,
33
+ // so paths and tokens land verbatim. We just refuse values containing "'".
34
+ function tomlLiteral(value) {
35
+ const s = String(value);
36
+ if (s.includes("'")) {
37
+ throw new Error(`Cannot encode value with single quote in TOML literal: ${s}`);
38
+ }
39
+ return `'${s}'`;
40
+ }
41
+
42
+ // Removes every section whose header matches [mcp_servers.<name>] or
43
+ // [mcp_servers.<name>.<sub>]. Lines belonging to such a section are dropped
44
+ // until another header (any header) is encountered.
45
+ function stripServerSections(content, name) {
46
+ const ours = new RegExp(
47
+ `^\\[mcp_servers\\.${name}(?:\\..+)?\\]\\s*$`
48
+ );
49
+ const anyHeader = /^\[.+\]\s*$/;
50
+ const out = [];
51
+ let inOur = false;
52
+ for (const raw of content.split('\n')) {
53
+ const line = raw;
54
+ if (ours.test(line)) {
55
+ inOur = true;
56
+ continue;
57
+ }
58
+ if (anyHeader.test(line)) {
59
+ inOur = false;
60
+ out.push(line);
61
+ continue;
62
+ }
63
+ if (!inOur) out.push(line);
64
+ }
65
+ // Trim trailing blank lines that may pile up after stripping
66
+ return out.join('\n').replace(/\n{3,}$/g, '\n\n').replace(/\s+$/, '\n');
67
+ }
68
+
69
+ function buildByanBlock({ serverPath, apiUrl, apiToken, startupTimeoutSec = 15 }) {
70
+ const lines = [
71
+ '',
72
+ '[mcp_servers.byan]',
73
+ `command = ${tomlLiteral('node')}`,
74
+ `args = [${tomlLiteral(serverPath)}]`,
75
+ `startup_timeout_sec = ${Number.isFinite(startupTimeoutSec) ? startupTimeoutSec : 15}`,
76
+ '',
77
+ '[mcp_servers.byan.env]',
78
+ `BYAN_API_URL = ${tomlLiteral(apiUrl)}`,
79
+ `BYAN_API_TOKEN = ${tomlLiteral(apiToken)}`,
80
+ '',
81
+ ];
82
+ return lines.join('\n');
83
+ }
84
+
85
+ async function patchCodexConfig(projectRoot, options = {}) {
86
+ const configPath = getCodexConfigPath();
87
+ const serverPath = path.join(
88
+ projectRoot,
89
+ '_byan',
90
+ 'mcp',
91
+ 'byan-mcp-server',
92
+ 'server.js'
93
+ );
94
+
95
+ await fs.ensureDir(path.dirname(configPath));
96
+
97
+ let existing = '';
98
+ if (await fs.pathExists(configPath)) {
99
+ existing = await fs.readFile(configPath, 'utf8');
100
+ }
101
+
102
+ const apiUrl =
103
+ options.apiUrl ||
104
+ process.env.BYAN_API_URL ||
105
+ 'http://localhost:3737';
106
+ const apiToken =
107
+ options.apiToken !== undefined
108
+ ? options.apiToken
109
+ : (process.env.BYAN_API_TOKEN || '');
110
+
111
+ const stripped = stripServerSections(existing, SERVER_NAME);
112
+ const block = buildByanBlock({
113
+ serverPath,
114
+ apiUrl,
115
+ apiToken,
116
+ startupTimeoutSec: options.startupTimeoutSec,
117
+ });
118
+
119
+ const merged =
120
+ (stripped.trimEnd().length > 0 ? stripped.trimEnd() + '\n' : '') + block;
121
+
122
+ await fs.writeFile(configPath, merged, 'utf8');
123
+ return { path: configPath, hadExisting: existing.length > 0, tokenSet: apiToken.length > 0 };
124
+ }
125
+
126
+ async function setupCodexNative(projectRoot, options = {}) {
127
+ const log = options.quiet ? () => {} : (...a) => console.log(...a);
128
+
129
+ const present = await detectCodex();
130
+ if (!present && !options.force) {
131
+ log(chalk.gray(' · Codex CLI not detected (~/.codex absent), skipping'));
132
+ return { skipped: true, reason: 'codex-not-detected' };
133
+ }
134
+
135
+ const result = await patchCodexConfig(projectRoot, options);
136
+ log(chalk.green(` ✓ Codex MCP entry written to ${result.path}`));
137
+ if (!result.tokenSet) {
138
+ log(
139
+ chalk.yellow(
140
+ ` ⚠ BYAN_API_TOKEN left empty. Edit ${result.path} and set BYAN_API_TOKEN`
141
+ )
142
+ );
143
+ log(chalk.gray(' (or rerun with BYAN_API_TOKEN=byan_xxx in the env)'));
144
+ }
145
+ log(chalk.gray(' Restart Codex CLI for the new MCP server to load'));
146
+ return result;
147
+ }
148
+
149
+ module.exports = {
150
+ setupCodexNative,
151
+ patchCodexConfig,
152
+ stripServerSections,
153
+ buildByanBlock,
154
+ getCodexConfigPath,
155
+ detectCodex,
156
+ };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-byan-agent",
3
- "version": "2.11.0",
3
+ "version": "2.14.1",
4
4
  "description": "BYAN v2.2.2 - Intelligent AI agent installer with multi-platform native support (GitHub Copilot CLI, Claude Code, Codex/OpenCode)",
5
5
  "bin": {
6
6
  "create-byan-agent": "bin/create-byan-agent-v2.js"
@@ -18,6 +18,18 @@ Invoke this protocol when the user :
18
18
 
19
19
  If the user request is a simple question or chat, stay out of FD — respond normally.
20
20
 
21
+ ## 1.5. Freshness check (silent, once per session)
22
+
23
+ Before responding to the user's first activation message in a session, call the MCP tool `byan_update_check` once. It is read-only and cheap (single npm registry lookup, 5s timeout, no side effects).
24
+
25
+ Behavior depending on the JSON returned :
26
+
27
+ - `updateAvailable === true` : surface a one-line notice to the user, e.g. *"BYAN {installed} is behind {latest} on npm. Run `byan_update_apply` for the upgrade command, or skip and continue."* — then proceed normally with the user's request. Do not block.
28
+ - `updateAvailable === false` and `note` is set (e.g. manifest missing, network error) : stay silent — do not nag.
29
+ - `isCurrent === true` : stay silent.
30
+
31
+ Never call `byan_update_apply` without explicit user consent. That tool returns a shell command — the user must run it themselves outside this conversation. Update is destructive (file overwrites with backup) and stays a deliberate user action.
32
+
21
33
  ## 2. Five-phase protocol
22
34
 
23
35
  ### Phase 1 — BRAINSTORM
@@ -0,0 +1,127 @@
1
+ /**
2
+ * BYAN update lifecycle helper for the MCP server.
3
+ *
4
+ * Read-only check: compares the installed BYAN version (from
5
+ * <projectRoot>/_byan/.manifest.json, falling back to package.json) against
6
+ * the latest published version on the npm registry.
7
+ *
8
+ * Apply path: returns *instructions* (a shell command) rather than executing
9
+ * anything itself. Update is destructive (overwrites files) and must remain
10
+ * an explicit user action, gated through the regular yanstaller pipeline
11
+ * (`npx create-byan-agent update`).
12
+ */
13
+
14
+ import fsPromises from 'node:fs/promises';
15
+ import nodePath from 'node:path';
16
+ import https from 'node:https';
17
+
18
+ export async function checkForUpdate(projectRoot) {
19
+ const installed = await getInstalledVersion(projectRoot);
20
+ let latest;
21
+ let networkError;
22
+ try {
23
+ latest = await getLatestVersion('create-byan-agent');
24
+ } catch (err) {
25
+ networkError = err.message || String(err);
26
+ }
27
+ if (!latest) {
28
+ return {
29
+ installed,
30
+ latest: null,
31
+ updateAvailable: false,
32
+ networkError,
33
+ note: 'Latest version unknown — npm registry unreachable. Skipped silently.',
34
+ };
35
+ }
36
+ if (installed === 'unknown') {
37
+ return {
38
+ installed,
39
+ latest,
40
+ updateAvailable: false,
41
+ note: '_byan/.manifest.json missing — installed version cannot be determined. The project may be a fresh manual setup; consider running `npx create-byan-agent` to write the manifest.',
42
+ };
43
+ }
44
+ const cmp = compareVersions(installed, latest);
45
+ return {
46
+ installed,
47
+ latest,
48
+ updateAvailable: cmp < 0,
49
+ isCurrent: cmp >= 0,
50
+ delta: cmp < 0 ? 'behind' : cmp > 0 ? 'ahead' : 'same',
51
+ };
52
+ }
53
+
54
+ export function formatApplyInstructions({ preview = false, force = false } = {}) {
55
+ const args = ['update'];
56
+ if (preview) args.push('--preview');
57
+ if (force) args.push('--force');
58
+ return {
59
+ command: `npx create-byan-agent ${args.join(' ')}`,
60
+ rationale: preview
61
+ ? 'Preview mode — no files are written. Inspects diff against the latest npm template.'
62
+ : 'Apply update via the yanstaller pipeline. yanstaller backs up the project, diffs vs latest npm template, and merges only non-user-modified files unless --force is set.',
63
+ safety: [
64
+ 'Backup is automatic before any write.',
65
+ 'User-modified files are preserved (unless --force).',
66
+ 'A rollback command (`npx create-byan-agent rollback`) is available afterwards.',
67
+ ],
68
+ next: 'Ask the user to run the command above. Do not execute it from inside this tool.',
69
+ };
70
+ }
71
+
72
+ async function getInstalledVersion(projectRoot) {
73
+ // _byan/.manifest.json is the canonical source — written by yanstaller after
74
+ // every install/update. byan-mcp-server/package.json carries an unrelated
75
+ // local version and would lie if used as fallback, so we don't fall back at
76
+ // all: return 'unknown' and let callers surface that honestly.
77
+ const manifestPath = nodePath.join(projectRoot, '_byan', '.manifest.json');
78
+ try {
79
+ const raw = await fsPromises.readFile(manifestPath, 'utf8');
80
+ const manifest = JSON.parse(raw);
81
+ if (manifest && manifest.version) return manifest.version;
82
+ } catch {}
83
+ return 'unknown';
84
+ }
85
+
86
+ function getLatestVersion(packageName, { timeoutMs = 5000 } = {}) {
87
+ const url = `https://registry.npmjs.org/${packageName}/latest`;
88
+ return new Promise((resolve, reject) => {
89
+ const req = https.get(
90
+ url,
91
+ { headers: { Accept: 'application/json' } },
92
+ (res) => {
93
+ if (res.statusCode !== 200) {
94
+ reject(new Error(`npm registry returned ${res.statusCode} for ${packageName}`));
95
+ res.resume();
96
+ return;
97
+ }
98
+ let data = '';
99
+ res.on('data', (chunk) => { data += chunk; });
100
+ res.on('end', () => {
101
+ try {
102
+ resolve(JSON.parse(data).version);
103
+ } catch (err) {
104
+ reject(new Error(`Failed to parse npm registry response: ${err.message}`));
105
+ }
106
+ });
107
+ }
108
+ );
109
+ req.on('error', (err) => reject(new Error(`Failed to reach npm registry: ${err.message}`)));
110
+ req.setTimeout(timeoutMs, () => {
111
+ req.destroy(new Error(`npm registry request timed out after ${timeoutMs}ms`));
112
+ });
113
+ });
114
+ }
115
+
116
+ function compareVersions(a, b) {
117
+ const pa = String(a).replace(/^v/, '').split('.').map((n) => Number(n) || 0);
118
+ const pb = String(b).replace(/^v/, '').split('.').map((n) => Number(n) || 0);
119
+ const len = Math.max(pa.length, pb.length);
120
+ for (let i = 0; i < len; i++) {
121
+ const na = pa[i] || 0;
122
+ const nb = pb[i] || 0;
123
+ if (na < nb) return -1;
124
+ if (na > nb) return 1;
125
+ }
126
+ return 0;
127
+ }
@@ -45,6 +45,14 @@ import {
45
45
  fcCheck,
46
46
  fcParse,
47
47
  } from './lib/cli.js';
48
+ import { checkForUpdate, formatApplyInstructions } from './lib/update.js';
49
+ import { fileURLToPath } from 'node:url';
50
+
51
+ const __filename = fileURLToPath(import.meta.url);
52
+ const __dirname = nodePath.dirname(__filename);
53
+ // Resolve the host project root: server.js lives at
54
+ // {projectRoot}/_byan/mcp/byan-mcp-server/server.js, so go up three levels.
55
+ const PROJECT_ROOT = nodePath.resolve(__dirname, '..', '..', '..');
48
56
 
49
57
  const BYAN_API_URL = process.env.BYAN_API_URL || 'http://localhost:3737';
50
58
  const BYAN_API_TOKEN = process.env.BYAN_API_TOKEN || '';
@@ -956,6 +964,35 @@ const tools = [
956
964
  additionalProperties: false,
957
965
  },
958
966
  },
967
+ {
968
+ name: 'byan_update_check',
969
+ description:
970
+ 'Check whether the BYAN platform installed in this project is up to date. Read-only. Reads the installed version from _byan/.manifest.json (fallback: package.json), fetches the latest published version from the npm registry (registry.npmjs.org/create-byan-agent), compares them, and returns { installed, latest, updateAvailable, delta }. Network failures are reported (networkError) and treated as "do not block". Use at agent activation to surface updates without nagging.',
971
+ inputSchema: {
972
+ type: 'object',
973
+ properties: {},
974
+ additionalProperties: false,
975
+ },
976
+ },
977
+ {
978
+ name: 'byan_update_apply',
979
+ description:
980
+ 'Returns the exact shell command the user must run to apply a BYAN update via the yanstaller pipeline (backup, diff vs latest npm template, merge non-user-modified files). Does NOT execute anything itself — update is destructive and must remain an explicit user action. Use after byan_update_check reports updateAvailable=true and the user has consented.',
981
+ inputSchema: {
982
+ type: 'object',
983
+ properties: {
984
+ preview: {
985
+ type: 'boolean',
986
+ description: 'If true, returns the --preview command (shows the diff without writing). Default: false.',
987
+ },
988
+ force: {
989
+ type: 'boolean',
990
+ description: 'If true, returns the --force command (overrides user-modified files). Default: false. Use with caution.',
991
+ },
992
+ },
993
+ additionalProperties: false,
994
+ },
995
+ },
959
996
  ];
960
997
 
961
998
  const server = new Server(
@@ -1457,6 +1494,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1457
1494
  return { content: [{ type: 'text', text: JSON.stringify(body, null, 2) }] };
1458
1495
  }
1459
1496
 
1497
+ if (name === 'byan_update_check') {
1498
+ const status = await checkForUpdate(PROJECT_ROOT);
1499
+ return { content: [{ type: 'text', text: JSON.stringify(status, null, 2) }] };
1500
+ }
1501
+
1502
+ if (name === 'byan_update_apply') {
1503
+ const instructions = formatApplyInstructions({
1504
+ preview: args.preview === true,
1505
+ force: args.force === true,
1506
+ });
1507
+ return { content: [{ type: 'text', text: JSON.stringify(instructions, null, 2) }] };
1508
+ }
1509
+
1460
1510
  throw new Error(`Unknown tool: ${name}`);
1461
1511
  } catch (err) {
1462
1512
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-byan-agent",
3
- "version": "2.13.1",
3
+ "version": "2.14.1",
4
4
  "description": "BYAN v2.8 - Intelligent AI agent creator with ELO trust system + scientific fact-check + Hermes universal dispatcher + native Claude Code integration (hooks, skills, MCP server). Multi-platform (Copilot CLI, Claude Code, Codex). Merise Agile + TDD + 64 Mantras. ~54% LLM cost savings.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -17,7 +17,8 @@
17
17
  "test:watch": "jest --watch",
18
18
  "test:e2e": "jest __tests__/e2e-install-update.test.js",
19
19
  "setup-turbo-whisper": "node install/setup-turbo-whisper.js",
20
- "byan": "echo \"BYAN agent installed. Use: copilot and type /agent\""
20
+ "byan": "echo \"BYAN agent installed. Use: copilot and type /agent\"",
21
+ "version": "node scripts/sync-install-version.js && git add install/package.json"
21
22
  },
22
23
  "keywords": [
23
24
  "byan",