create-byan-agent 2.28.0 → 2.29.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/CHANGELOG.md CHANGED
@@ -9,6 +9,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [2.29.0] - 2026-06-23
13
+
14
+ ### Added - Native opt-in RTK token optimizer (rtk-ai/rtk)
15
+
16
+ - The yanstaller now offers RTK ("Rust Token Killer", Apache-2.0) during install:
17
+ a single zero-dep binary that compresses dev-command output before the LLM
18
+ context (-60/90% tokens) and wires into Claude Code via its own hook.
19
+ - Integration is thin and delegating (`install/lib/rtk-integration.js` +
20
+ `native-helper.js`): the install is handed to rtk's own canonical installer
21
+ (brew / `cargo --tag v0.42.4` / the pinned `install.sh`, which passes
22
+ `RTK_VERSION` so the downloaded binary is pinned too), and the hook wiring to
23
+ rtk's own `rtk init -g`. No bespoke per-OS download/checksum logic.
24
+ - Opt-in and safe: the prompt defaults to NO and discloses the install mechanism
25
+ and the global hook; gated on a TTY + an available installer; opt out with
26
+ `BYAN_SKIP_RTK=1`. Every failure path is a graceful no-op that leaves the BYAN
27
+ install intact. Retry anytime with `npm run setup-rtk`. 25 unit tests.
28
+
29
+ ### Added - Open Knowledge Format (OKF v0.1) adoption for the knowledge base
30
+
31
+ - BYAN's knowledge is now interoperable with the Open Knowledge Format
32
+ (GoogleCloudPlatform/knowledge-catalog) — markdown + YAML frontmatter, vendor-
33
+ neutral, zero runtime deps. Only the FORMAT is adopted; the GCP reference agent
34
+ (Python + BigQuery/Gemini) is deliberately left out (not vendored).
35
+ - `lib/okf-format.js` (parse/serialize/validate frontmatter + BYAN type mapping)
36
+ and `lib/okf-bundle.js` (pure, idempotent converter). `byan-okf build` emits a
37
+ normalized OKF bundle to the gitignored `_byan-output/okf-bundle/` (NON-
38
+ destructive — it leaves `_byan/connaissance` untouched); `byan-okf check`
39
+ validates a bundle. 24 unit tests; a real build over the 43 knowledge files
40
+ yields 41 valid OKF entries. The optional GCP enrichment bridge is parked as a
41
+ phase-2 follow-on.
42
+
12
43
  ## [2.28.0] - 2026-06-23
13
44
 
14
45
  ### Added - Advisory model-tiering lint for native workflows
@@ -16,6 +16,7 @@ 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 { setupLeantimeIntegration, validateLeantimeReachability } = require('../lib/byan-leantime-integration');
19
+ const { setupRtkIntegration, shouldOfferRtk } = require('../lib/rtk-integration');
19
20
  const { setupClaudeNative } = require('../lib/claude-native-setup');
20
21
  const { setupCodexNative } = require('../lib/codex-native-setup');
21
22
  const { setupMcpExtensions } = require('../lib/mcp-extensions');
@@ -1369,6 +1370,33 @@ async function install(options = {}) {
1369
1370
  }
1370
1371
  }
1371
1372
 
1373
+ if (needsClaude && shouldOfferRtk()) {
1374
+ console.log();
1375
+ console.log(chalk.cyan('RTK token optimizer (optional — Rust binary, cuts dev-command tokens 60-90%)'));
1376
+ try {
1377
+ const { proceed } = await inquirer.prompt([
1378
+ {
1379
+ type: 'confirm',
1380
+ name: 'proceed',
1381
+ message: 'Install rtk now? Runs its official installer (brew/cargo, or a pinned curl|sh) and adds a GLOBAL Claude Code hook via `rtk init -g`.',
1382
+ default: false,
1383
+ },
1384
+ ]);
1385
+ if (proceed) {
1386
+ const r = setupRtkIntegration({ log: (m) => console.log(chalk.gray(' ' + m)) });
1387
+ if (r.synced) {
1388
+ console.log(chalk.green(` ✓ rtk ready (${r.installedVia}, v${r.version || '?'}) — restart Claude Code to activate`));
1389
+ } else {
1390
+ console.log(chalk.yellow(` ⚠ rtk not wired (${r.reason}) — BYAN unaffected`));
1391
+ }
1392
+ } else {
1393
+ console.log(chalk.gray(' rtk skipped — run `npm run setup-rtk` anytime to enable.'));
1394
+ }
1395
+ } catch (error) {
1396
+ console.log(chalk.yellow(` ⚠ rtk setup skipped: ${error.message}`));
1397
+ }
1398
+ }
1399
+
1372
1400
  if (needsClaude) {
1373
1401
  console.log();
1374
1402
  console.log(chalk.cyan('Leantime board sync (optional — self-hosted)'));
@@ -0,0 +1,50 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * native-helper — dependency-light helpers for installing OPTIONAL native
5
+ * components (rtk today; turbo-whisper/parakeet can adopt it later to retire
6
+ * their bespoke per-OS branches, shrinking the maintenance surface).
7
+ *
8
+ * Everything is PURE + injectable (the command runner is a parameter) so callers
9
+ * stay unit-testable without shelling out for real. WHY: the install path runs on
10
+ * every user machine across OSes — it must be exercised by tests, not hope.
11
+ */
12
+
13
+ const { execSync } = require('child_process');
14
+
15
+ /**
16
+ * commandExists(cmd) -> boolean. Is an executable resolvable on PATH?
17
+ * Uses POSIX `command -v` (and `where` on Windows). The runner is injectable.
18
+ */
19
+ function commandExists(cmd, { run = execSync, platform = process.platform } = {}) {
20
+ const probe = platform === 'win32' ? `where ${cmd}` : `command -v ${cmd}`;
21
+ try {
22
+ run(probe, { stdio: 'pipe' });
23
+ return true;
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
28
+
29
+ /**
30
+ * detectPlatform() -> 'mac' | 'windows' | 'linux'. Coarse bucket for choosing an
31
+ * install strategy. Keeps the platform string in ONE place.
32
+ */
33
+ function detectPlatform(platform = process.platform) {
34
+ if (platform === 'darwin') return 'mac';
35
+ if (platform === 'win32') return 'windows';
36
+ return 'linux';
37
+ }
38
+
39
+ /**
40
+ * firstAvailable(candidates) -> the first candidate whose `tool` resolves on
41
+ * PATH, or null. `candidates` is an ordered preference list of
42
+ * { id, tool, ... }. This is the generic "pick the installer the machine has"
43
+ * primitive; the concrete strategy list stays with each component.
44
+ */
45
+ function firstAvailable(candidates, { has = commandExists } = {}) {
46
+ if (!Array.isArray(candidates)) return null;
47
+ return candidates.find((c) => c && c.tool && has(c.tool)) || null;
48
+ }
49
+
50
+ module.exports = { commandExists, detectPlatform, firstAvailable };
@@ -0,0 +1,182 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * rtk-integration — optional NATIVE token-optimizer integration (rtk-ai/rtk).
5
+ *
6
+ * RTK ("Rust Token Killer", Apache-2.0) is a single zero-dependency binary that
7
+ * filters/compresses dev-command output before it reaches the LLM context
8
+ * (-60/90% tokens). It wires into Claude Code through its OWN hook installer.
9
+ *
10
+ * MAINTAINABILITY by design (the whole point of choosing this shape):
11
+ * - We do NOT reimplement per-OS download / checksum / version pinning. We
12
+ * DELEGATE the install to rtk's own canonical installer (brew / cargo / the
13
+ * official install.sh), and DELEGATE the Claude Code hook wiring to rtk's own
14
+ * `rtk init -g`. BYAN maintains only "pick the available installer, run it,
15
+ * verify, ask rtk to wire its hook" — a tiny surface, bumped via one constant.
16
+ * - SUPPLY-CHAIN: we pin to a TAG, never a moving branch. cargo builds the
17
+ * tagged source (`--tag`), and install.sh is fetched from the IMMUTABLE tag ref
18
+ * (and that script itself checksum-verifies the binary it downloads). So the
19
+ * fetched artifacts are reproducible, not "whatever master is today".
20
+ * - Everything is graceful: a missing installer or a failed step is a no-op that
21
+ * NEVER breaks the BYAN install (returns { ok:true, synced:false, reason }).
22
+ * - The command runner + the PATH probe are injectable, so the whole flow is
23
+ * unit-tested without shelling out (see install/__tests__/rtk-integration.test.js).
24
+ *
25
+ * Mirrors the ENTRY-POINT shape of byan-web-integration.js / byan-leantime-
26
+ * integration.js (one `setup*Integration`); the return contract is { ok, synced,
27
+ * reason, ... } because persistence is owned by rtk's own `rtk init -g`, not by
28
+ * byan-platform-config.
29
+ */
30
+
31
+ const { execSync } = require('child_process');
32
+ const { commandExists, firstAvailable } = require('./native-helper');
33
+
34
+ // Pinned install target. Bumping rtk = change these two constants only. We pin a
35
+ // TAG so the install is reproducible and supply-chain-bounded (see header).
36
+ const RTK_VERSION = '0.42.4';
37
+ const RTK_TAG = `v${RTK_VERSION}`;
38
+ const RTK_REPO = 'https://github.com/rtk-ai/rtk';
39
+ const RTK_INSTALL_SH = `https://raw.githubusercontent.com/rtk-ai/rtk/refs/tags/${RTK_TAG}/install.sh`;
40
+
41
+ /**
42
+ * Ordered install strategies — each DELEGATES to rtk's own canonical installer,
43
+ * pinned to RTK_TAG where the mechanism allows. Preference: brew (cleanest
44
+ * upgrades, vetted formula) > cargo (any Rust dev, pinned --tag) > the official
45
+ * install.sh at the immutable tag ref (universal fallback, self-checksumming).
46
+ */
47
+ function installStrategies() {
48
+ return [
49
+ { id: 'brew', tool: 'brew', cmd: 'brew install rtk' },
50
+ { id: 'cargo', tool: 'cargo', cmd: `cargo install --git ${RTK_REPO} --tag ${RTK_TAG}` },
51
+ // Pass RTK_VERSION into the script so it pins the BINARY too (the script
52
+ // builds releases/download/${VERSION}/...; without it, it resolves
53
+ // releases/latest). Pinning the URL alone is not enough — this pins both.
54
+ { id: 'script', tool: 'curl', cmd: `curl -fsSL ${RTK_INSTALL_SH} | RTK_VERSION=${RTK_TAG} sh` },
55
+ ];
56
+ }
57
+
58
+ function oneLine(err) {
59
+ return String((err && err.message) || err).split('\n')[0];
60
+ }
61
+
62
+ /**
63
+ * rtkStatus() -> { installed, version }. Never throws. Parses `rtk --version`
64
+ * ("rtk 0.42.4"). An installed-but-unparseable build yields { installed:true,
65
+ * version:null } (a real runtime possibility, handled downstream).
66
+ */
67
+ function rtkStatus({ run = execSync } = {}) {
68
+ try {
69
+ const out = String(run('rtk --version', { stdio: 'pipe' }) || '');
70
+ const m = out.match(/rtk\s+v?([0-9]+\.[0-9]+\.[0-9]+)/i);
71
+ return { installed: true, version: m ? m[1] : null };
72
+ } catch {
73
+ return { installed: false, version: null };
74
+ }
75
+ }
76
+
77
+ /**
78
+ * pickStrategy() -> the first install strategy whose tool is on PATH, or null.
79
+ */
80
+ function pickStrategy({ has = commandExists } = {}) {
81
+ return firstAvailable(installStrategies(), { has });
82
+ }
83
+
84
+ /**
85
+ * doctor() -> a status report for diagnostics / a `byan` doctor surface. `pinned`
86
+ * is the version BYAN installs/targets — NOT an enforced floor on a pre-existing
87
+ * rtk (we do not break a user's own older rtk).
88
+ */
89
+ function doctor({ run = execSync } = {}) {
90
+ const s = rtkStatus({ run });
91
+ return {
92
+ component: 'rtk',
93
+ installed: s.installed,
94
+ version: s.version,
95
+ pinned: RTK_VERSION,
96
+ repo: RTK_REPO,
97
+ hookCommand: 'rtk init -g',
98
+ };
99
+ }
100
+
101
+ // Delegate the Claude Code hook wiring to rtk's OWN command (idempotent). This is
102
+ // the maintainability win: no manual settings.json merge to keep in sync.
103
+ function wireHook({ run, log, installedVia, version }) {
104
+ try {
105
+ run('rtk init -g', { stdio: 'pipe' });
106
+ log(`rtk: ready (${installedVia}, v${version || '?'}) — hook wired via 'rtk init -g'. Restart Claude Code to activate.`);
107
+ return { ok: true, synced: true, reason: 'wired', installed: true, installedVia, version, hook: true };
108
+ } catch (err) {
109
+ log(`rtk: installed (${installedVia}) but 'rtk init -g' failed (${oneLine(err)}) — run it manually, BYAN unaffected.`);
110
+ return { ok: true, synced: false, reason: 'hook-failed', installed: true, installedVia, version, hook: false };
111
+ }
112
+ }
113
+
114
+ /**
115
+ * setupRtkIntegration(options) -> { ok, synced, reason, ... }. The single entry.
116
+ *
117
+ * Flow: if rtk is already present, just (re)wire the hook (idempotent). Otherwise
118
+ * pick an available installer, delegate the install to rtk, verify, then wire the
119
+ * hook. Any missing tool / failed step degrades to a logged no-op — it NEVER
120
+ * throws and NEVER fails the surrounding BYAN install. `ok` is therefore always
121
+ * true; `synced` says whether rtk ended up wired.
122
+ *
123
+ * @param {object} [o]
124
+ * @param {Function} [o.run] command runner (default execSync) — injected in tests
125
+ * @param {Function} [o.has] PATH probe (default commandExists) — injected in tests
126
+ * @param {Function} [o.log] one-line breadcrumb sink (default no-op)
127
+ */
128
+ function setupRtkIntegration({ run = execSync, has = commandExists, log = () => {} } = {}) {
129
+ const before = rtkStatus({ run });
130
+ if (before.installed) {
131
+ return wireHook({ run, log, installedVia: 'already-present', version: before.version });
132
+ }
133
+
134
+ const strat = pickStrategy({ has });
135
+ if (!strat) {
136
+ log('rtk: no installer found (brew / cargo / curl) — skipped. BYAN install unaffected.');
137
+ return { ok: true, synced: false, reason: 'no-installer', installed: false, hook: false };
138
+ }
139
+
140
+ // Breadcrumb BEFORE the (silent, possibly slow) delegated install so the user
141
+ // is not left staring at a frozen prompt while rtk's installer runs.
142
+ log(`rtk: installing via ${strat.id} (pinned ${RTK_TAG}; this can take a moment)...`);
143
+ try {
144
+ run(strat.cmd, { stdio: 'pipe' });
145
+ } catch (err) {
146
+ log(`rtk: install via ${strat.id} failed (${oneLine(err)}) — skipped gracefully.`);
147
+ return { ok: true, synced: false, reason: `install-failed:${strat.id}`, installed: false, hook: false };
148
+ }
149
+
150
+ const after = rtkStatus({ run });
151
+ if (!after.installed) {
152
+ log(`rtk: install via ${strat.id} ran but 'rtk --version' did not confirm — skipped.`);
153
+ return { ok: true, synced: false, reason: 'install-unverified', installed: false, hook: false };
154
+ }
155
+
156
+ return wireHook({ run, log, installedVia: strat.id, version: after.version });
157
+ }
158
+
159
+ /**
160
+ * shouldOfferRtk() -> boolean. The installer asks this BEFORE prompting the user:
161
+ * offer rtk only when (a) interactive (a TTY), (b) not opted out
162
+ * (BYAN_SKIP_RTK=1), and (c) an installer is actually on PATH — so we never prompt
163
+ * for something we cannot deliver, and never block a non-interactive/CI install.
164
+ */
165
+ function shouldOfferRtk({ env = process.env, isTTY = !!(process.stdin && process.stdin.isTTY), has = commandExists } = {}) {
166
+ if (env && env.BYAN_SKIP_RTK === '1') return false;
167
+ if (!isTTY) return false;
168
+ return Boolean(pickStrategy({ has }));
169
+ }
170
+
171
+ module.exports = {
172
+ setupRtkIntegration,
173
+ rtkStatus,
174
+ pickStrategy,
175
+ installStrategies,
176
+ doctor,
177
+ shouldOfferRtk,
178
+ RTK_VERSION,
179
+ RTK_TAG,
180
+ RTK_INSTALL_SH,
181
+ RTK_REPO,
182
+ };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-byan-agent",
3
- "version": "2.28.0",
3
+ "version": "2.29.0",
4
4
  "description": "BYAN - Intelligent AI agent installer with multi-platform native support (Claude Code, Codex/OpenCode)",
5
5
  "bin": {
6
6
  "create-byan-agent": "bin/create-byan-agent-v2.js"
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * setup-rtk — explicit opt-in installer for the RTK token optimizer (rtk-ai/rtk).
6
+ *
7
+ * Run on demand: `npm run setup-rtk` (or `node install/setup-rtk.js`). It delegates
8
+ * the install to rtk's own canonical installer (brew / cargo / official script) and
9
+ * the Claude Code hook wiring to rtk's own `rtk init -g`. It NEVER throws: a missing
10
+ * installer or a failed step is reported and exits 0 (RTK is optional; BYAN works
11
+ * without it). The real logic lives in lib/rtk-integration.js and is unit-tested.
12
+ */
13
+
14
+ const chalk = require('chalk');
15
+ const { setupRtkIntegration, doctor } = require('./lib/rtk-integration');
16
+
17
+ function main() {
18
+ console.log(chalk.cyan('\nRTK token optimizer (rtk-ai/rtk) — optional native component'));
19
+ console.log(chalk.gray(' Cuts dev-command tokens 60-90%; wires its own Claude Code hook.\n'));
20
+
21
+ const result = setupRtkIntegration({ log: (m) => console.log(chalk.gray(' ' + m)) });
22
+
23
+ if (result.synced) {
24
+ console.log(chalk.green(`\n rtk ready (${result.installedVia}, v${result.version || '?'}). Restart Claude Code to activate.`));
25
+ } else if (result.reason === 'no-installer') {
26
+ console.log(chalk.yellow('\n No installer found (brew / cargo / curl). Install one, then re-run `npm run setup-rtk`.'));
27
+ } else {
28
+ console.log(chalk.yellow(`\n rtk not wired (${result.reason}). BYAN is unaffected; you can retry later.`));
29
+ }
30
+
31
+ const d = doctor();
32
+ console.log(chalk.gray(` doctor: installed=${d.installed} version=${d.version || '-'} (pinned ${d.pinned})`));
33
+ // Optional component -> always a clean exit; never fail the surrounding flow.
34
+ process.exit(0);
35
+ }
36
+
37
+ if (require.main === module) {
38
+ main();
39
+ }
40
+
41
+ module.exports = { main };
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+ // byan-okf — produce / validate an Open Knowledge Format bundle from BYAN's
3
+ // knowledge base (_byan/connaissance).
4
+ //
5
+ // byan-okf build [--root <dir>] [--out <dir>] build a bundle (non-destructive)
6
+ // byan-okf check [<dir>] [--root <dir>] validate every .md is OKF
7
+ //
8
+ // `build` is NON-DESTRUCTIVE: it reads _byan/connaissance and writes a normalized
9
+ // OKF bundle (frontmatter + index.md + log.md) to the OUT dir (default
10
+ // _byan-output/okf-bundle/, which is gitignored). It never mutates the source.
11
+ // The conversion logic is in lib/okf-bundle.js (pure, unit-tested).
12
+
13
+ import fs from 'node:fs';
14
+ import path from 'node:path';
15
+ import { buildBundle } from '../lib/okf-bundle.js';
16
+ import { parseFrontmatter, validateOkf, OKF_RESERVED } from '../lib/okf-format.js';
17
+
18
+ function parseArgs(argv) {
19
+ const a = { _: [] };
20
+ for (let i = 2; i < argv.length; i++) {
21
+ const x = argv[i];
22
+ if (x === '--root') a.root = argv[++i];
23
+ else if (x === '--out') a.out = argv[++i];
24
+ else a._.push(x);
25
+ }
26
+ return a;
27
+ }
28
+
29
+ function walkMd(dir, base = dir, acc = []) {
30
+ let entries;
31
+ try {
32
+ entries = fs.readdirSync(dir, { withFileTypes: true });
33
+ } catch {
34
+ return acc;
35
+ }
36
+ for (const e of entries) {
37
+ const full = path.join(dir, e.name);
38
+ if (e.isDirectory()) walkMd(full, base, acc);
39
+ else if (e.isFile() && e.name.endsWith('.md')) acc.push({ relPath: path.relative(base, full), text: fs.readFileSync(full, 'utf8') });
40
+ }
41
+ return acc;
42
+ }
43
+
44
+ function cmdBuild(args) {
45
+ const root = args.root || process.cwd();
46
+ const src = path.join(root, '_byan', 'connaissance');
47
+ const out = args.out || path.join(root, '_byan-output', 'okf-bundle');
48
+ const files = walkMd(src);
49
+ if (!files.length) {
50
+ process.stdout.write(`[byan-okf] no markdown found under ${src} — nothing to build\n`);
51
+ return 0;
52
+ }
53
+ const timestamp = new Date().toISOString();
54
+ const { entries, index, log, errors } = buildBundle(files, { timestamp });
55
+ fs.mkdirSync(out, { recursive: true });
56
+ for (const e of entries) {
57
+ const dest = path.join(out, e.path);
58
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
59
+ fs.writeFileSync(dest, e.serialized);
60
+ }
61
+ fs.writeFileSync(path.join(out, 'index.md'), index);
62
+ fs.writeFileSync(path.join(out, 'log.md'), log);
63
+ const warned = entries.filter((e) => e.validation.warnings.length).length;
64
+ process.stdout.write(`[byan-okf] wrote ${entries.length} OKF entries + index.md + log.md to ${out} (${warned} with warnings, ${errors.length} invalid)\n`);
65
+ return errors.length ? 1 : 0;
66
+ }
67
+
68
+ function cmdCheck(args) {
69
+ const root = args.root || process.cwd();
70
+ const dir = args._[1] || path.join(root, '_byan-output', 'okf-bundle');
71
+ const files = walkMd(dir);
72
+ let bad = 0;
73
+ for (const f of files) {
74
+ if (OKF_RESERVED.includes(path.basename(f.relPath))) continue;
75
+ const { data } = parseFrontmatter(f.text);
76
+ const v = validateOkf(data);
77
+ if (!v.ok) {
78
+ bad += 1;
79
+ process.stderr.write(`[byan-okf] INVALID ${f.relPath}: ${v.errors.join('; ')}\n`);
80
+ }
81
+ }
82
+ if (bad) {
83
+ process.stderr.write(`[byan-okf] ${bad} invalid OKF file(s) under ${dir}\n`);
84
+ return 1;
85
+ }
86
+ process.stdout.write(`[byan-okf] OK - ${files.length} markdown file(s) are valid OKF under ${dir}\n`);
87
+ return 0;
88
+ }
89
+
90
+ const args = parseArgs(process.argv);
91
+ const cmd = args._[0] || 'build';
92
+ let code = 0;
93
+ if (cmd === 'build') code = cmdBuild(args);
94
+ else if (cmd === 'check') code = cmdCheck(args);
95
+ else {
96
+ process.stderr.write(`[byan-okf] unknown command '${cmd}' (use: build | check)\n`);
97
+ code = 2;
98
+ }
99
+ process.exit(code);
@@ -0,0 +1,118 @@
1
+ // okf-bundle — pure converter that turns BYAN knowledge files into an Open
2
+ // Knowledge Format bundle (normalized frontmatter + index.md + log.md).
3
+ //
4
+ // PURE (no fs): the bin (byan-okf.js) does the I/O and hands content in/out.
5
+ // buildEntry is a FIXPOINT on its own output — feeding a normalized file back in
6
+ // keeps its fields verbatim (no duplication, no churn). Note: the FIRST build of a
7
+ // frontmatter-less source stamps a fresh timestamp, but once a file carries one,
8
+ // rebuilds preserve it. Unknown/extension keys are always preserved (OKF spec).
9
+
10
+ import path from 'node:path';
11
+ import {
12
+ parseFrontmatter,
13
+ serializeFrontmatter,
14
+ validateOkf,
15
+ byanTypeFor,
16
+ deriveTitle,
17
+ deriveDescription,
18
+ OKF_VERSION,
19
+ OKF_RESERVED,
20
+ } from './okf-format.js';
21
+
22
+ const PREFERRED_ORDER = ['type', 'title', 'description', 'resource', 'tags', 'timestamp'];
23
+
24
+ function basenameTitle(relPath) {
25
+ return path.basename(String(relPath || '')).replace(/\.md$/, '');
26
+ }
27
+
28
+ /**
29
+ * buildEntry(relPath, text, { timestamp }) -> { path, data, body, serialized,
30
+ * validation }. Fills the OKF frontmatter from existing values FIRST (idempotent),
31
+ * deriving type/title/description only when absent. Existing + unknown keys are
32
+ * preserved; keys are emitted in a stable, readable order.
33
+ */
34
+ export function buildEntry(relPath, text, { timestamp } = {}) {
35
+ const { data: existing, body } = parseFrontmatter(text);
36
+ const data = { ...existing };
37
+
38
+ if (!data.type) data.type = byanTypeFor(relPath);
39
+ if (!data.title) data.title = deriveTitle(body) || basenameTitle(relPath);
40
+ if (data.description === undefined) {
41
+ const d = deriveDescription(body);
42
+ if (d) data.description = d;
43
+ }
44
+ if (!data.timestamp && timestamp) data.timestamp = timestamp;
45
+ // A YAML-coerced Date (unquoted source timestamp) -> canonical ISO string, so
46
+ // the emitted bundle always carries a string timestamp.
47
+ if (data.timestamp instanceof Date) data.timestamp = data.timestamp.toISOString();
48
+
49
+ const ordered = {};
50
+ for (const k of PREFERRED_ORDER) if (data[k] !== undefined) ordered[k] = data[k];
51
+ for (const k of Object.keys(data)) if (ordered[k] === undefined) ordered[k] = data[k];
52
+
53
+ return {
54
+ path: relPath,
55
+ data: ordered,
56
+ body,
57
+ serialized: serializeFrontmatter(ordered, body),
58
+ validation: validateOkf(ordered),
59
+ };
60
+ }
61
+
62
+ /**
63
+ * buildIndex(entries) -> the bundle-root index.md content. Carries the
64
+ * `okf_version` frontmatter (the one place the spec allows it) and groups entries
65
+ * by type with relative links — the progressive-disclosure listing.
66
+ */
67
+ export function buildIndex(entries) {
68
+ const byType = new Map();
69
+ for (const e of entries) {
70
+ const t = (e.data && e.data.type) || 'Knowledge';
71
+ if (!byType.has(t)) byType.set(t, []);
72
+ byType.get(t).push(e);
73
+ }
74
+ const lines = [
75
+ '# BYAN Knowledge',
76
+ '',
77
+ 'Open Knowledge Format bundle generated from `_byan/connaissance`.',
78
+ '',
79
+ ];
80
+ for (const t of [...byType.keys()].sort()) {
81
+ lines.push(`## ${t}`, '');
82
+ for (const e of byType.get(t).slice().sort((a, b) => a.path.localeCompare(b.path))) {
83
+ const title = (e.data && e.data.title) || e.path;
84
+ const desc = e.data && e.data.description ? ` — ${e.data.description}` : '';
85
+ lines.push(`- [${title}](${e.path})${desc}`);
86
+ }
87
+ lines.push('');
88
+ }
89
+ return serializeFrontmatter({ okf_version: OKF_VERSION }, lines.join('\n'));
90
+ }
91
+
92
+ /**
93
+ * buildLog(entries, timestamp) -> log.md content. Reserved file, no frontmatter,
94
+ * plain update history.
95
+ */
96
+ export function buildLog(entries, timestamp) {
97
+ const stamp = timestamp || '(unstamped)';
98
+ return `# Update Log\n\n## ${stamp}\n\nGenerated OKF bundle from _byan/connaissance: ${entries.length} entries.\n`;
99
+ }
100
+
101
+ /**
102
+ * buildBundle(files, { timestamp }) -> { entries, index, log, errors }. `files`
103
+ * is [{ relPath, text }]. Reserved names (index.md/log.md) and non-markdown are
104
+ * skipped. `errors` lists entries that fail OKF validation (should be none, since
105
+ * we always set `type`).
106
+ */
107
+ export function buildBundle(files, { timestamp } = {}) {
108
+ const entries = (files || [])
109
+ .filter((f) => f && typeof f.relPath === 'string' && /\.md$/.test(f.relPath))
110
+ .filter((f) => !OKF_RESERVED.includes(path.basename(f.relPath)))
111
+ .map((f) => buildEntry(f.relPath, f.text, { timestamp }));
112
+ const index = buildIndex(entries);
113
+ const log = buildLog(entries, timestamp);
114
+ const errors = entries
115
+ .filter((e) => !e.validation.ok)
116
+ .map((e) => ({ path: e.path, errors: e.validation.errors }));
117
+ return { entries, index, log, errors };
118
+ }
@@ -0,0 +1,136 @@
1
+ // okf-format — Open Knowledge Format v0.1 primitives (parse / serialize /
2
+ // validate) + the BYAN knowledge type mapping.
3
+ //
4
+ // OKF (GoogleCloudPlatform/knowledge-catalog) represents knowledge as plain
5
+ // markdown + YAML frontmatter, vendor-neutral and dependency-free for consumers.
6
+ // We ADOPT the FORMAT for _byan/connaissance (the GCP reference AGENT, which needs
7
+ // BigQuery/Gemini, is deliberately NOT vendored — see docs). This module is the
8
+ // single source of truth for the frontmatter contract; the converter
9
+ // (okf-bundle.js) builds on it.
10
+ //
11
+ // Spec v0.1: required `type` (free string); recommended `title`, `description`,
12
+ // `resource`, `tags` (list), `timestamp` (ISO-8601); unknown keys MUST be
13
+ // preserved. Reserved filenames: index.md (listing, okf_version at bundle root)
14
+ // and log.md (history).
15
+
16
+ import yaml from 'js-yaml';
17
+
18
+ export const OKF_VERSION = '0.1';
19
+ export const OKF_RESERVED = Object.freeze(['index.md', 'log.md']);
20
+
21
+ const FM_RE = /^?---\n([\s\S]*?)\n---\n?/;
22
+ const ISO_RE = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?)?$/;
23
+
24
+ /**
25
+ * parseFrontmatter(text) -> { data, body }. A leading `---\n...\n---` block is
26
+ * YAML-parsed into `data`; the rest is `body`. No block -> { data:{}, body:text }.
27
+ * Malformed YAML degrades to {} rather than throwing (a knowledge file is never
28
+ * worth crashing the tool over).
29
+ */
30
+ export function parseFrontmatter(text) {
31
+ const s = String(text == null ? '' : text);
32
+ const m = s.match(FM_RE);
33
+ if (!m) return { data: {}, body: s.replace(/^/, '') };
34
+ let data;
35
+ try {
36
+ data = yaml.load(m[1]);
37
+ } catch {
38
+ data = {};
39
+ }
40
+ if (!data || typeof data !== 'object' || Array.isArray(data)) data = {};
41
+ // Strip the blank line(s) the convention puts between `---` and the body, so
42
+ // body starts at real content and round-trips cleanly with serializeFrontmatter.
43
+ return { data, body: s.slice(m[0].length).replace(/^\n+/, '') };
44
+ }
45
+
46
+ /**
47
+ * serializeFrontmatter(data, body) -> a markdown string with a clean YAML
48
+ * frontmatter block followed by the body. Round-trips with parseFrontmatter.
49
+ */
50
+ export function serializeFrontmatter(data, body) {
51
+ const fm = yaml.dump(data || {}, { lineWidth: -1, noRefs: true, sortKeys: false }).trimEnd();
52
+ const b = String(body == null ? '' : body).replace(/^\n+/, '');
53
+ return `---\n${fm}\n---\n\n${b}`.replace(/\n*$/, '\n');
54
+ }
55
+
56
+ /**
57
+ * isIsoTimestamp(v) -> true for an ISO-8601 date/datetime string OR a Date.
58
+ * js-yaml coerces an UNQUOTED YAML timestamp (`timestamp: 2026-06-23T10:00:00Z`)
59
+ * into a JS Date, so a valid hand-authored OKF file must not be rejected for it.
60
+ */
61
+ export function isIsoTimestamp(v) {
62
+ if (v instanceof Date) return !Number.isNaN(v.getTime());
63
+ return typeof v === 'string' && ISO_RE.test(v);
64
+ }
65
+
66
+ /**
67
+ * validateOkf(data) -> { ok, errors, warnings }. Enforces the v0.1 contract:
68
+ * `type` is required; title/description must be strings if present; tags must be
69
+ * a string list; timestamp must be ISO-8601. Missing recommended fields are
70
+ * WARNINGS, not errors (the spec only hard-requires `type`).
71
+ */
72
+ export function validateOkf(data) {
73
+ const errors = [];
74
+ const warnings = [];
75
+ const d = data && typeof data === 'object' ? data : {};
76
+
77
+ if (typeof d.type !== 'string' || !d.type.trim()) {
78
+ errors.push('missing required field: type (non-empty string)');
79
+ }
80
+ for (const k of ['title', 'description', 'resource']) {
81
+ if (d[k] !== undefined && typeof d[k] !== 'string') errors.push(`${k} must be a string`);
82
+ }
83
+ if (d.tags !== undefined && (!Array.isArray(d.tags) || d.tags.some((t) => typeof t !== 'string'))) {
84
+ errors.push('tags must be a list of strings');
85
+ }
86
+ if (d.timestamp !== undefined && !isIsoTimestamp(d.timestamp)) {
87
+ errors.push('timestamp must be ISO-8601');
88
+ }
89
+ for (const k of ['title', 'description', 'timestamp']) {
90
+ if (d[k] === undefined) warnings.push(`recommended field absent: ${k}`);
91
+ }
92
+ return { ok: errors.length === 0, errors, warnings };
93
+ }
94
+
95
+ /**
96
+ * byanTypeFor(relPath) -> a sensible OKF `type` for a _byan/connaissance file.
97
+ * Types are free strings in OKF; this keeps BYAN's vocabulary consistent.
98
+ */
99
+ export function byanTypeFor(relPath) {
100
+ const p = String(relPath || '').replace(/\\/g, '/');
101
+ const base = p.split('/').pop();
102
+ if (base === 'sources.md') return 'Reference: Source Registry';
103
+ if (base === 'blacklisted-sources.md') return 'Reference: Blacklist';
104
+ if (base === 'mantras-sources.md') return 'Reference: Mantra Sources';
105
+ if (base === 'axioms.md') return 'Axiom';
106
+ // Match by path SEGMENT (not '/testarch/') so it works for relative paths
107
+ // ('testarch/...') the converter passes AND absolute ones.
108
+ const segs = p.split('/');
109
+ if (segs.includes('testarch')) return 'Knowledge: Test Architecture';
110
+ if (segs.includes('excalidraw')) return 'Knowledge: Excalidraw';
111
+ return 'Knowledge';
112
+ }
113
+
114
+ /** deriveTitle(body) -> the first `# ` heading text, or null. */
115
+ export function deriveTitle(body) {
116
+ const m = String(body || '').match(/^#\s+(.+?)\s*$/m);
117
+ return m ? m[1].trim() : null;
118
+ }
119
+
120
+ /**
121
+ * deriveDescription(body) -> the first prose sentence (skipping headings, the
122
+ * bold `**key:**` metadata lines, blockquotes and rules), capped at 200 chars,
123
+ * or null when the body has no prose. Best-effort only (description is a
124
+ * RECOMMENDED field): the sentence split is naive (an abbreviation like "e.g."
125
+ * truncates early) and a leading bullet is taken verbatim — acceptable for a
126
+ * derived default a human can override in the frontmatter.
127
+ */
128
+ export function deriveDescription(body) {
129
+ for (const raw of String(body || '').split('\n')) {
130
+ const t = raw.trim();
131
+ if (!t || t.startsWith('#') || t.startsWith('---') || t.startsWith('**') || t.startsWith('>') || t.startsWith('|')) continue;
132
+ const sentence = t.split(/(?<=[.!?])\s/)[0];
133
+ return sentence.length > 200 ? `${sentence.slice(0, 197)}...` : sentence;
134
+ }
135
+ return null;
136
+ }
@@ -490,7 +490,7 @@ try {
490
490
 
491
491
  **Plan:** `BYAN-V2-MANUAL-TEST-PLAN.md`
492
492
 
493
- 1. Run demo-byan-v2-simple.js
493
+ 1. Run examples/demo-byan-v2-simple.js
494
494
  2. Verify 12 questions asked
495
495
  3. Check profile generated
496
496
  4. Validate against GitHub Copilot CLI requirements
@@ -570,7 +570,7 @@ byan.setTemplate('my-custom-template');
570
570
  ### Tests
571
571
  - `__tests__/byan-v2/orchestrator/` - Tests unitaires
572
572
  - `__tests__/byan-v2/integration/` - Tests intégration
573
- - `demo-byan-v2-simple.js` - Démo complète
573
+ - `examples/demo-byan-v2-simple.js` - Démo complète
574
574
 
575
575
  ---
576
576
 
@@ -271,7 +271,7 @@ Emojis not allowed in technical sections
271
271
  ### Integration Tests
272
272
 
273
273
  **Scenarios:**
274
- 1. Validate demo agent (demo-byan-v2-simple.js output)
274
+ 1. Validate demo agent (examples/demo-byan-v2-simple.js output)
275
275
  2. Validate all BYAN agents in _byan/agents/
276
276
  3. Detect invalid agents (negative tests)
277
277
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-byan-agent",
3
- "version": "2.28.0",
3
+ "version": "2.29.0",
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 (Claude Code, Codex). Merise Agile + TDD + 71 Mantras. ~54% LLM cost savings.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -17,6 +17,7 @@
17
17
  "test:watch": "jest --watch",
18
18
  "test:e2e": "jest install/__tests__/post-install.e2e.test.js",
19
19
  "setup-turbo-whisper": "node install/setup-turbo-whisper.js",
20
+ "setup-rtk": "node install/setup-rtk.js",
20
21
  "byan": "echo \"BYAN agent installed. Open Claude Code (or Codex) in this project.\"",
21
22
  "version": "node scripts/sync-install-version.js && git add install/package.json",
22
23
  "app:dev": "npm --prefix app run dev",
@@ -61,6 +62,7 @@
61
62
  "install/packages/platform-config/package.json",
62
63
  "install/setup-turbo-whisper.js",
63
64
  "install/setup-parakeet.js",
65
+ "install/setup-rtk.js",
64
66
  "install/install.sh",
65
67
  "install/package.json",
66
68
  "update-byan-agent/bin/",