create-byan-agent 2.29.1 → 2.35.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.
Files changed (28) hide show
  1. package/CHANGELOG.md +250 -0
  2. package/install/bin/create-byan-agent-v2.js +33 -2
  3. package/install/lib/gdoc-setup.js +210 -0
  4. package/install/lib/mcp-extensions/gdrive.js +27 -2
  5. package/install/lib/native-helper.js +68 -1
  6. package/install/lib/rtk-integration.js +193 -57
  7. package/install/package.json +1 -1
  8. package/install/packages/platform-config/lib/credentials.js +13 -1
  9. package/install/setup-gdoc.js +41 -0
  10. package/install/setup-rtk.js +7 -3
  11. package/install/templates/.claude/CLAUDE.md +15 -4
  12. package/install/templates/.claude/hooks/inject-soul.js +4 -3
  13. package/install/templates/.claude/hooks/inject-tao.js +65 -25
  14. package/install/templates/.claude/hooks/inject-voice-anchor.js +102 -0
  15. package/install/templates/.claude/rules/portable-core.md +81 -0
  16. package/install/templates/.claude/settings.json +5 -1
  17. package/install/templates/.claude/skills/byan-hermes-dispatch/SKILL.md +16 -2
  18. package/install/templates/_byan/mcp/byan-mcp-server/lib/gdoc-client.js +203 -0
  19. package/install/templates/_byan/mcp/byan-mcp-server/lib/gdoc-content.js +203 -0
  20. package/install/templates/_byan/mcp/byan-mcp-server/lib/resolve-config.js +15 -2
  21. package/install/templates/_byan/mcp/byan-mcp-server/lib/sync-rules.js +1 -1
  22. package/install/templates/_byan/mcp/byan-mcp-server/package.json +2 -0
  23. package/install/templates/_byan/mcp/byan-mcp-server/server.js +70 -0
  24. package/install/templates/_byan/mcp/byan-mcp-server/skill-bundles-manifest.json +1 -1
  25. package/install/templates/dist/skill-bundles/byan-hermes-dispatch.zip +0 -0
  26. package/install/templates/docs/google-docs-publish.md +121 -0
  27. package/node_modules/byan-platform-config/lib/credentials.js +13 -1
  28. package/package.json +3 -1
@@ -11,6 +11,9 @@
11
11
  */
12
12
 
13
13
  const { execSync } = require('child_process');
14
+ const fs = require('fs');
15
+ const os = require('os');
16
+ const path = require('path');
14
17
 
15
18
  /**
16
19
  * commandExists(cmd) -> boolean. Is an executable resolvable on PATH?
@@ -26,6 +29,70 @@ function commandExists(cmd, { run = execSync, platform = process.platform } = {}
26
29
  }
27
30
  }
28
31
 
32
+ /**
33
+ * safeHomedir() -> the home directory, or '' if it cannot be determined. WHY:
34
+ * os.homedir() THROWS on an env with no $HOME/$USERPROFILE AND no passwd entry
35
+ * for the effective uid (distroless / random-uid containers). An optional install
36
+ * helper must never throw on that — it degrades to "no home dir to scan".
37
+ */
38
+ function safeHomedir() {
39
+ try {
40
+ return os.homedir();
41
+ } catch {
42
+ return '';
43
+ }
44
+ }
45
+
46
+ /**
47
+ * knownBinDirs() -> ordered list of directories where installers commonly drop a
48
+ * binary that is NOT on a non-login shell's PATH. The canonical case: `cargo
49
+ * install` lands in ~/.cargo/bin, which Debian's apt-packaged cargo does NOT add
50
+ * to PATH — so a freshly built binary is present yet invisible to a bare probe.
51
+ * fs/env/home are injectable for tests. `home` defaults via safeHomedir so a
52
+ * HOME-less environment yields a shorter scan instead of a throw.
53
+ */
54
+ function knownBinDirs({ env = process.env, home = safeHomedir(), platform = process.platform } = {}) {
55
+ const dirs = [];
56
+ if (env && env.CARGO_HOME) dirs.push(path.join(env.CARGO_HOME, 'bin'));
57
+ dirs.push(path.join(home, '.cargo', 'bin')); // cargo install
58
+ dirs.push(path.join(home, '.local', 'bin')); // pip/pipx + many curl scripts
59
+ if (platform === 'win32') {
60
+ if (env && env.LOCALAPPDATA) dirs.push(path.join(env.LOCALAPPDATA, 'Programs'));
61
+ } else {
62
+ dirs.push('/usr/local/bin'); // common script target + brew (intel mac)
63
+ dirs.push('/opt/homebrew/bin'); // brew (apple silicon)
64
+ dirs.push('/usr/bin');
65
+ }
66
+ return dirs;
67
+ }
68
+
69
+ /**
70
+ * resolveBinary(name) -> the command to invoke the executable, or null if it
71
+ * cannot be located. Returns the BARE name when it is on PATH (let the shell
72
+ * resolve it), else an ABSOLUTE path found in a knownBinDir. WHY: an optional
73
+ * native install can succeed yet leave the binary off PATH; callers must verify
74
+ * and wire it by its real location, not declare failure. All I/O is injectable.
75
+ */
76
+ function resolveBinary(name, {
77
+ has = commandExists,
78
+ existsSync = fs.existsSync,
79
+ env = process.env,
80
+ home = safeHomedir(),
81
+ platform = process.platform,
82
+ } = {}) {
83
+ if (has(name)) return name;
84
+ const exe = platform === 'win32' ? `${name}.exe` : name;
85
+ for (const dir of knownBinDirs({ env, home, platform })) {
86
+ const full = path.join(dir, exe);
87
+ try {
88
+ if (existsSync(full)) return full;
89
+ } catch {
90
+ // a stat error on one candidate must not abort the scan
91
+ }
92
+ }
93
+ return null;
94
+ }
95
+
29
96
  /**
30
97
  * detectPlatform() -> 'mac' | 'windows' | 'linux'. Coarse bucket for choosing an
31
98
  * install strategy. Keeps the platform string in ONE place.
@@ -47,4 +114,4 @@ function firstAvailable(candidates, { has = commandExists } = {}) {
47
114
  return candidates.find((c) => c && c.tool && has(c.tool)) || null;
48
115
  }
49
116
 
50
- module.exports = { commandExists, detectPlatform, firstAvailable };
117
+ module.exports = { commandExists, detectPlatform, firstAvailable, knownBinDirs, resolveBinary, safeHomedir };
@@ -9,27 +9,43 @@
9
9
  *
10
10
  * MAINTAINABILITY by design (the whole point of choosing this shape):
11
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.
12
+ * DELEGATE the install to rtk's own canonical installer (brew / the official
13
+ * install.sh / cargo), and DELEGATE the Claude Code hook wiring to rtk's own
14
+ * `rtk init -g --auto-patch` (the `--auto-patch` is what makes it non-
15
+ * interactive see wireHook). BYAN maintains only "pick the available
16
+ * installer, run it bounded + visible, locate the binary, ask rtk to wire its
17
+ * hook" — a tiny surface, bumped via one constant.
16
18
  * - 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).
19
+ * tagged source (`--tag`), and install.sh is fetched from the IMMUTABLE tag ref.
20
+ * That script verifies a SHA-256 of the downloaded binary against a published
21
+ * checksums.txt and aborts on mismatch (source: refs/tags/v0.42.4/install.sh,
22
+ * fetched + verified 2026-06-23). So the fetched artifacts are reproducible and
23
+ * integrity-checked, not "whatever master is today".
24
+ * - FAIL-PROOF: the delegated install runs with a per-strategy TIMEOUT (a hung
25
+ * network/build is bounded, not infinite) and with INHERITED stdio (the user
26
+ * SEES the installer's progress instead of staring at a frozen line; this also
27
+ * sidesteps execSync's 1MB maxBuffer cap that a verbose build would blow). Any
28
+ * missing installer / failed / timed-out step is a logged no-op that NEVER
29
+ * breaks the BYAN install (returns { ok:true, synced:false, reason }).
30
+ * - OFF-PATH SAFE: a successful install can leave the binary off PATH (the
31
+ * classic: cargo drops it in ~/.cargo/bin, which Debian's apt-cargo does not
32
+ * add to PATH). We RESOLVE the binary across known install dirs before
33
+ * declaring "unverified", verify + wire it by its real path, and hand the user
34
+ * a one-line PATH hint. The user's own report (rtk installed at ~/.cargo/bin,
35
+ * invisible to a bare probe) is exactly this case.
36
+ * - The command runner + the PATH probe + the resolver are injectable, so the
37
+ * whole flow is unit-tested without shelling out (see
38
+ * install/__tests__/rtk-integration.test.js).
24
39
  *
25
40
  * Mirrors the ENTRY-POINT shape of byan-web-integration.js / byan-leantime-
26
41
  * 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
42
+ * reason, ... } because persistence is owned by rtk's own `rtk init -g --auto-patch`, not by
28
43
  * byan-platform-config.
29
44
  */
30
45
 
31
46
  const { execSync } = require('child_process');
32
- const { commandExists, firstAvailable } = require('./native-helper');
47
+ const path = require('path');
48
+ const { commandExists, resolveBinary, firstAvailable } = require('./native-helper');
33
49
 
34
50
  // Pinned install target. Bumping rtk = change these two constants only. We pin a
35
51
  // TAG so the install is reproducible and supply-chain-bounded (see header).
@@ -38,20 +54,37 @@ const RTK_TAG = `v${RTK_VERSION}`;
38
54
  const RTK_REPO = 'https://github.com/rtk-ai/rtk';
39
55
  const RTK_INSTALL_SH = `https://raw.githubusercontent.com/rtk-ai/rtk/refs/tags/${RTK_TAG}/install.sh`;
40
56
 
57
+ // Per-strategy timeout (ms). A prebuilt-binary path (brew / script) should be
58
+ // quick; cargo COMPILES from source and is legitimately slow, so it gets a wide
59
+ // budget. The bound exists to cap a genuine HANG, not to race a normal build.
60
+ const MIN = 60 * 1000;
61
+ const RTK_TIMEOUT_MS = { brew: 5 * MIN, script: 5 * MIN, cargo: 20 * MIN };
62
+ const RTK_DEFAULT_TIMEOUT_MS = 10 * MIN;
63
+
41
64
  /**
42
65
  * 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).
66
+ * pinned to RTK_TAG where the mechanism allows.
67
+ *
68
+ * Order = brew > script > cargo. WHY this order (not brew > cargo > script):
69
+ * - brew first: vetted formula, cleanest upgrades, prebuilt.
70
+ * - script second: the official install.sh downloads a PREBUILT binary to a
71
+ * PATH-stable location in seconds. It is pinned to the immutable tag ref and
72
+ * verifies the binary's SHA-256 before installing (see the cited check above)
73
+ * — a bounded supply-chain surface.
74
+ * - cargo LAST: it COMPILES from source (minutes) AND drops the binary in
75
+ * ~/.cargo/bin, frequently off a non-login PATH. It stays as a build-from-
76
+ * source fallback for machines without brew/curl, not the default path.
46
77
  */
47
78
  function installStrategies() {
48
79
  return [
49
- { id: 'brew', tool: 'brew', cmd: 'brew install rtk' },
50
- { id: 'cargo', tool: 'cargo', cmd: `cargo install --git ${RTK_REPO} --tag ${RTK_TAG}` },
80
+ { id: 'brew', tool: 'brew', cmd: 'brew install rtk', timeoutMs: RTK_TIMEOUT_MS.brew },
51
81
  // Pass RTK_VERSION into the script so it pins the BINARY too (the script
52
82
  // builds releases/download/${VERSION}/...; without it, it resolves
53
83
  // 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` },
84
+ // The script verifies a SHA-256 of the binary (vs checksums.txt) before
85
+ // installing — verified at refs/tags/v0.42.4/install.sh, 2026-06-23.
86
+ { id: 'script', tool: 'curl', cmd: `curl -fsSL ${RTK_INSTALL_SH} | RTK_VERSION=${RTK_TAG} sh`, timeoutMs: RTK_TIMEOUT_MS.script },
87
+ { id: 'cargo', tool: 'cargo', cmd: `cargo install --git ${RTK_REPO} --tag ${RTK_TAG}`, timeoutMs: RTK_TIMEOUT_MS.cargo },
55
88
  ];
56
89
  }
57
90
 
@@ -59,19 +92,78 @@ function oneLine(err) {
59
92
  return String((err && err.message) || err).split('\n')[0];
60
93
  }
61
94
 
95
+ // A bounded, env-overridable timeout for a strategy. BYAN_RTK_TIMEOUT_MS (ms)
96
+ // lets a user widen the bound (e.g. a slow machine compiling via cargo). Only a
97
+ // POSITIVE integer is honored: Node treats timeout:0 as "no timeout", so a 0
98
+ // override would silently re-introduce the unbounded hang the bound exists to
99
+ // prevent — it falls back to the strategy budget instead.
100
+ function timeoutFor(strat, { env = process.env } = {}) {
101
+ const raw = env && env.BYAN_RTK_TIMEOUT_MS;
102
+ if (raw != null && /^[0-9]+$/.test(String(raw))) {
103
+ const n = Number(raw);
104
+ if (n > 0) return n;
105
+ }
106
+ return (strat && strat.timeoutMs) || RTK_DEFAULT_TIMEOUT_MS;
107
+ }
108
+
109
+ // Did execSync throw because WE killed it on timeout (vs the command failing on
110
+ // its own)? Node sets `killed=true` and code 'ETIMEDOUT' when it kills the child
111
+ // on timeout; a build error sets a non-zero exit status with killed=false. We do
112
+ // NOT match a bare 'SIGTERM' signal: a sub-process self-terminating with SIGTERM
113
+ // for an unrelated reason would otherwise be mislabelled a timeout.
114
+ function isTimeout(err) {
115
+ return Boolean(err && (err.killed === true || err.code === 'ETIMEDOUT'));
116
+ }
117
+
118
+ // A shell/platform-correct "add this dir to PATH" hint. The resolved binary can
119
+ // live off PATH (cargo -> ~/.cargo/bin); the user needs the line that actually
120
+ // works in THEIR shell — fish rejects POSIX `export`, Windows rejects both.
121
+ function pathHintFor(bin, { env = process.env, platform = process.platform } = {}) {
122
+ // Parse the dir with the TARGET platform's rules, not the host's — so a win32
123
+ // path yields the right dir even when this code runs on a POSIX CI host.
124
+ const dir = (platform === 'win32' ? path.win32 : path.posix).dirname(bin);
125
+ if (platform === 'win32') return `$env:PATH = "${dir};$env:PATH"`;
126
+ const shell = String((env && env.SHELL) || '');
127
+ if (/(^|\/)fish$/.test(shell)) return `fish_add_path ${dir}`;
128
+ return `export PATH="${dir}:$PATH"`;
129
+ }
130
+
62
131
  /**
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).
132
+ * rtkStatus() -> { installed, version, bin }. Never throws. Probes `<bin>
133
+ * --version` ("rtk 0.42.4"), default bin "rtk" (on PATH). An installed-but-
134
+ * unparseable build yields { installed:true, version:null }.
66
135
  */
67
- function rtkStatus({ run = execSync } = {}) {
136
+ function rtkStatus({ run = execSync, bin = 'rtk' } = {}) {
68
137
  try {
69
- const out = String(run('rtk --version', { stdio: 'pipe' }) || '');
138
+ const out = String(run(`${bin} --version`, { stdio: 'pipe' }) || '');
70
139
  const m = out.match(/rtk\s+v?([0-9]+\.[0-9]+\.[0-9]+)/i);
71
- return { installed: true, version: m ? m[1] : null };
140
+ return { installed: true, version: m ? m[1] : null, bin };
72
141
  } catch {
73
- return { installed: false, version: null };
142
+ return { installed: false, version: null, bin: null };
143
+ }
144
+ }
145
+
146
+ /**
147
+ * locateRtk() -> a verified { installed, version, bin } by trying the bare name
148
+ * first (on PATH), then resolving across known install dirs (off-PATH). Returns
149
+ * the off-PATH absolute path in `bin` when that is where it was found.
150
+ */
151
+ function locateRtk({ run = execSync, has = commandExists, resolve = resolveBinary, env = process.env } = {}) {
152
+ const onPath = rtkStatus({ run, bin: 'rtk' });
153
+ if (onPath.installed) return onPath;
154
+ // The resolver must never break the never-throws contract; degrade to not-found
155
+ // if it (or a misbehaving injected resolver) throws.
156
+ let resolved = null;
157
+ try {
158
+ resolved = resolve('rtk', { has, env });
159
+ } catch {
160
+ resolved = null;
161
+ }
162
+ if (resolved && resolved !== 'rtk') {
163
+ const s = rtkStatus({ run, bin: resolved });
164
+ if (s.installed) return s;
74
165
  }
166
+ return { installed: false, version: null, bin: null };
75
167
  }
76
168
 
77
169
  /**
@@ -82,78 +174,116 @@ function pickStrategy({ has = commandExists } = {}) {
82
174
  }
83
175
 
84
176
  /**
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).
177
+ * doctor() -> a status report for diagnostics / a `byan` doctor surface. Resolves
178
+ * the binary across known dirs so an off-PATH install is reported as installed
179
+ * (with its real path), not as missing. `pinned` is the version BYAN targets —
180
+ * NOT an enforced floor on a pre-existing rtk.
88
181
  */
89
- function doctor({ run = execSync } = {}) {
90
- const s = rtkStatus({ run });
182
+ function doctor({ run = execSync, has = commandExists, resolve = resolveBinary, env = process.env } = {}) {
183
+ const s = locateRtk({ run, has, resolve, env });
91
184
  return {
92
185
  component: 'rtk',
93
186
  installed: s.installed,
94
187
  version: s.version,
188
+ bin: s.bin,
95
189
  pinned: RTK_VERSION,
96
190
  repo: RTK_REPO,
97
- hookCommand: 'rtk init -g',
191
+ hookCommand: 'rtk init -g --auto-patch',
98
192
  };
99
193
  }
100
194
 
101
195
  // 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 }) {
196
+ // the maintainability win: no manual settings.json merge to keep in sync. Wires
197
+ // via the resolved `bin` so an off-PATH install still gets its hook.
198
+ //
199
+ // `--auto-patch` is REQUIRED: bare `rtk init -g` PROMPTS before patching
200
+ // settings.json, and we run it non-interactively (stdio:'pipe', no TTY) — so the
201
+ // prompt gets no answer and rtk silently writes only the instruction layer
202
+ // (RTK.md + @RTK.md) WITHOUT the PreToolUse hook. `--auto-patch` patches
203
+ // settings.json non-interactively, so the transparent command-rewriting hook is
204
+ // actually installed. (Verified live: `rtk init --show` reports "Hook: not found"
205
+ // after bare `-g` piped, "Hook: ... configured" after `-g --auto-patch`.)
206
+ function wireHook({ run, log, installedVia, version, bin = 'rtk', env = process.env, platform = process.platform }) {
207
+ const offPath = bin !== 'rtk';
208
+ const initCmd = `${bin} init -g --auto-patch`;
104
209
  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 };
210
+ run(initCmd, { stdio: 'pipe' });
211
+ log(`rtk: ready (${installedVia}, v${version || '?'}) — hook wired via '${initCmd}'. Restart Claude Code to activate.`);
212
+ const r = { ok: true, synced: true, reason: 'wired', installed: true, installedVia, version, hook: true, bin };
213
+ if (offPath) {
214
+ r.pathHint = pathHintFor(bin, { env, platform });
215
+ log(`rtk: note — installed at ${bin}, not on your PATH. Add it: ${r.pathHint}`);
216
+ }
217
+ return r;
108
218
  } 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 };
219
+ log(`rtk: installed (${installedVia}) but '${initCmd}' failed (${oneLine(err)}) — run it manually, BYAN unaffected.`);
220
+ const r = { ok: true, synced: false, reason: 'hook-failed', installed: true, installedVia, version, hook: false, bin };
221
+ if (offPath) r.pathHint = pathHintFor(bin, { env, platform });
222
+ return r;
111
223
  }
112
224
  }
113
225
 
114
226
  /**
115
227
  * setupRtkIntegration(options) -> { ok, synced, reason, ... }. The single entry.
116
228
  *
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.
229
+ * Flow: if rtk is already present (on PATH or off-PATH in a known dir), just
230
+ * (re)wire the hook (idempotent). Otherwise pick an available installer, delegate
231
+ * the install (BOUNDED by a timeout + VISIBLE via inherited stdio), LOCATE the
232
+ * binary, verify, then wire the hook. Any missing tool / failed / timed-out step
233
+ * degrades to a logged no-op it NEVER throws and NEVER fails the surrounding
234
+ * BYAN install. `ok` is therefore always true; `synced` says whether rtk ended
235
+ * up wired. An off-PATH install carries a `pathHint`.
122
236
  *
123
237
  * @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)
238
+ * @param {Function} [o.run] command runner (default execSync) — injected in tests
239
+ * @param {Function} [o.has] PATH probe (default commandExists) — injected in tests
240
+ * @param {Function} [o.resolve] binary resolver (default resolveBinary) — injected in tests
241
+ * @param {Function} [o.log] one-line breadcrumb sink (default no-op)
242
+ * @param {object} [o.env] environment (default process.env) — for the timeout override
127
243
  */
128
- function setupRtkIntegration({ run = execSync, has = commandExists, log = () => {} } = {}) {
129
- const before = rtkStatus({ run });
244
+ function setupRtkIntegration({
245
+ run = execSync,
246
+ has = commandExists,
247
+ resolve = resolveBinary,
248
+ log = () => {},
249
+ env = process.env,
250
+ platform = process.platform,
251
+ } = {}) {
252
+ const before = locateRtk({ run, has, resolve, env });
130
253
  if (before.installed) {
131
- return wireHook({ run, log, installedVia: 'already-present', version: before.version });
254
+ return wireHook({ run, log, installedVia: 'already-present', version: before.version, bin: before.bin, env, platform });
132
255
  }
133
256
 
134
257
  const strat = pickStrategy({ has });
135
258
  if (!strat) {
136
- log('rtk: no installer found (brew / cargo / curl) — skipped. BYAN install unaffected.');
259
+ log('rtk: no installer found (brew / curl / cargo) — skipped. BYAN install unaffected.');
137
260
  return { ok: true, synced: false, reason: 'no-installer', installed: false, hook: false };
138
261
  }
139
262
 
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)...`);
263
+ // Breadcrumb BEFORE the delegated install. stdio is INHERITED so the installer's
264
+ // own progress streams to the user (no frozen line), bounded by a timeout.
265
+ const timeoutMs = timeoutFor(strat, { env });
266
+ log(`rtk: installing via ${strat.id} (pinned ${RTK_TAG}; up to ${Math.round(timeoutMs / MIN)}min, Ctrl+C is safe)...`);
143
267
  try {
144
- run(strat.cmd, { stdio: 'pipe' });
268
+ run(strat.cmd, { stdio: 'inherit', timeout: timeoutMs });
145
269
  } catch (err) {
270
+ if (isTimeout(err)) {
271
+ log(`rtk: install via ${strat.id} exceeded ${Math.round(timeoutMs / MIN)}min — stopped (a background build may still continue). Retry with 'npm run setup-rtk' (or widen BYAN_RTK_TIMEOUT_MS). BYAN unaffected.`);
272
+ return { ok: true, synced: false, reason: `install-timeout:${strat.id}`, installed: false, hook: false };
273
+ }
146
274
  log(`rtk: install via ${strat.id} failed (${oneLine(err)}) — skipped gracefully.`);
147
275
  return { ok: true, synced: false, reason: `install-failed:${strat.id}`, installed: false, hook: false };
148
276
  }
149
277
 
150
- const after = rtkStatus({ run });
278
+ // The binary may be installed but OFF PATH (cargo -> ~/.cargo/bin). Resolve it
279
+ // before declaring failure, and wire it by its real location.
280
+ const after = locateRtk({ run, has, resolve, env });
151
281
  if (!after.installed) {
152
282
  log(`rtk: install via ${strat.id} ran but 'rtk --version' did not confirm — skipped.`);
153
283
  return { ok: true, synced: false, reason: 'install-unverified', installed: false, hook: false };
154
284
  }
155
285
 
156
- return wireHook({ run, log, installedVia: strat.id, version: after.version });
286
+ return wireHook({ run, log, installedVia: strat.id, version: after.version, bin: after.bin, env, platform });
157
287
  }
158
288
 
159
289
  /**
@@ -171,12 +301,18 @@ function shouldOfferRtk({ env = process.env, isTTY = !!(process.stdin && process
171
301
  module.exports = {
172
302
  setupRtkIntegration,
173
303
  rtkStatus,
304
+ locateRtk,
174
305
  pickStrategy,
175
306
  installStrategies,
307
+ timeoutFor,
308
+ isTimeout,
309
+ pathHintFor,
176
310
  doctor,
177
311
  shouldOfferRtk,
178
312
  RTK_VERSION,
179
313
  RTK_TAG,
180
314
  RTK_INSTALL_SH,
181
315
  RTK_REPO,
316
+ RTK_TIMEOUT_MS,
317
+ RTK_DEFAULT_TIMEOUT_MS,
182
318
  };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-byan-agent",
3
- "version": "2.29.1",
3
+ "version": "2.35.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"
@@ -18,7 +18,19 @@ const path = require('path');
18
18
  const fs = require('fs-extra');
19
19
 
20
20
  // Keys the credentials file understands. Any other key passed in is ignored.
21
- const KNOWN_KEYS = ['BYAN_API_URL', 'BYAN_API_TOKEN', 'LEANTIME_API_URL', 'LEANTIME_API_TOKEN'];
21
+ // The Google Docs publish keys (byan_publish) live here too so a per-user
22
+ // service-account setup persists alongside the byan_web / Leantime config; the
23
+ // MCP server reads them via resolve-config. GOOGLE_APPLICATION_CREDENTIALS is a
24
+ // PATH to the SA JSON (not the key itself) -- the key file stays separate, 0600.
25
+ const KNOWN_KEYS = [
26
+ 'BYAN_API_URL',
27
+ 'BYAN_API_TOKEN',
28
+ 'LEANTIME_API_URL',
29
+ 'LEANTIME_API_TOKEN',
30
+ 'GOOGLE_APPLICATION_CREDENTIALS',
31
+ 'GDOC_TEMPLATE_ID',
32
+ 'GDOC_LOGO_PNG_URL',
33
+ ];
22
34
 
23
35
  function credentialsDir(homedir = os.homedir()) {
24
36
  return path.join(homedir, '.byan');
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * setup-gdoc -- explicit opt-in setup for the byan_publish service-account key.
6
+ *
7
+ * Run on demand: `npm run setup-gdoc` (or `node install/setup-gdoc.js`). Walks
8
+ * the user through creating a Google service account + key, imports the JSON to
9
+ * ~/.byan/google-sa.json (0600), and persists the publish config into
10
+ * ~/.byan/credentials.json. It NEVER throws and exits 0 : the SA setup is
11
+ * optional and must not fail the surrounding flow. The real logic lives in
12
+ * lib/gdoc-setup.js and is unit-tested.
13
+ */
14
+
15
+ const chalk = require('chalk');
16
+ const { setupGdocPublish } = require('./lib/gdoc-setup');
17
+
18
+ async function main() {
19
+ console.log(chalk.cyan('\nbyan_publish -- clé service account (Google Docs headless)'));
20
+ console.log(chalk.gray(' Open source : tu fournis ta clé ; rien de secret ne ship.\n'));
21
+
22
+ const result = await setupGdocPublish({ log: (...a) => console.log(...a) });
23
+
24
+ if (result.configured) {
25
+ console.log(chalk.green(`\n Configuré. Clé : ${result.path}`));
26
+ console.log(chalk.gray(' Installe aussi googleapis dans le MCP server : (cd _byan/mcp/byan-mcp-server && npm install googleapis google-auth-library)'));
27
+ } else {
28
+ console.log(chalk.yellow(`\n Non configuré (${result.skipReason || 'ignoré'}). BYAN reste fonctionnel.`));
29
+ }
30
+ process.exit(0);
31
+ }
32
+
33
+ if (require.main === module) {
34
+ main().catch((err) => {
35
+ // Last-resort guard : an optional setup must not crash with a non-zero exit.
36
+ console.log(chalk.yellow(` setup-gdoc ignoré : ${err.message}`));
37
+ process.exit(0);
38
+ });
39
+ }
40
+
41
+ module.exports = { main };
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * Run on demand: `npm run setup-rtk` (or `node install/setup-rtk.js`). It delegates
8
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
9
+ * the Claude Code hook wiring to rtk's own `rtk init -g --auto-patch`. It NEVER throws: a missing
10
10
  * installer or a failed step is reported and exits 0 (RTK is optional; BYAN works
11
11
  * without it). The real logic lives in lib/rtk-integration.js and is unit-tested.
12
12
  */
@@ -16,14 +16,18 @@ const { setupRtkIntegration, doctor } = require('./lib/rtk-integration');
16
16
 
17
17
  function main() {
18
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'));
19
+ console.log(chalk.gray(' Cuts dev-command tokens 60-90%; wires its own Claude Code hook.'));
20
+ console.log(chalk.gray(' Progress streams below. The cargo fallback compiles from source (minutes); Ctrl+C is safe.\n'));
20
21
 
21
22
  const result = setupRtkIntegration({ log: (m) => console.log(chalk.gray(' ' + m)) });
22
23
 
23
24
  if (result.synced) {
24
25
  console.log(chalk.green(`\n rtk ready (${result.installedVia}, v${result.version || '?'}). Restart Claude Code to activate.`));
26
+ if (result.pathHint) console.log(chalk.yellow(` rtk is not on your PATH — add it: ${result.pathHint}`));
25
27
  } 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`.'));
28
+ console.log(chalk.yellow('\n No installer found (brew / curl / cargo). Install one, then re-run `npm run setup-rtk`.'));
29
+ } else if (result.pathHint) {
30
+ console.log(chalk.yellow(`\n rtk found off-PATH (${result.reason}) — add it then re-run: ${result.pathHint}`));
27
31
  } else {
28
32
  console.log(chalk.yellow(`\n rtk not wired (${result.reason}). BYAN is unaffected; you can retry later.`));
29
33
  }
@@ -62,8 +62,9 @@ Doctrine d'equipe complete (template role-in-team, analogie orchestre, principes
62
62
  - Doctrine d'equipe: voir @.claude/rules/team-doctrine.md
63
63
  - Methodologie: voir @.claude/rules/merise-agile.md
64
64
  - Systeme de confiance epistemique: voir @.claude/rules/elo-trust.md
65
- - Protocol fact-check scientifique: voir @.claude/rules/fact-check.md
66
- - Mode strict anti-downgrade: voir @.claude/rules/strict-mode.md
65
+ - Protocol fact-check scientifique: voir .claude/rules/fact-check.md (charge a la demande via le skill byan-fact-check)
66
+ - Mode strict anti-downgrade: voir .claude/rules/strict-mode.md (charge a la demande via le skill byan-strict)
67
+ - Architecture portable (noyau portable, projection native): voir .claude/rules/portable-core.md (charge a la demande)
67
68
  - Systeme API byan_web: voir @.claude/rules/byan-api.md
68
69
 
69
70
  ## API byan_web
@@ -109,7 +110,7 @@ Protocole : lock du scope -> build complet -> self-verify >= 3 passes -> complet
109
110
  - Filet final : `.githooks/pre-commit` bloque le commit si une session strict est engagee mais non completee
110
111
  - Persistance : sessions poussees vers l'API byan_web (autorite ; local = miroir/fallback offline) via `lib/strict-sync.js` ; migration `033` + `routes/strict-sessions.js` cote byan_web
111
112
 
112
- Detail complet : voir @.claude/rules/strict-mode.md
113
+ Detail complet (hors contexte par defaut, charge a la demande via le skill byan-strict) : voir .claude/rules/strict-mode.md
113
114
 
114
115
  <!-- BYAN-AUTOBENCH:BEGIN (Generated by byan-sync-rules from _byan/_config/autobench.yaml. Do not hand-edit.) -->
115
116
  ## BYAN Auto-Benchmark
@@ -120,5 +121,15 @@ both gates hold (>= 2 non-substitutable options diverging on >= 1 weighted
120
121
  criterion). Emit the marker verbatim before the table:
121
122
  `<!-- BYAN-BENCH:done g1=<#options> g2=<#divergent-criteria> scope=<internal|external> conf=<assertive|lean> -->`.
122
123
  A confirm, a destructive prompt, or an obvious default is not a fork — emit
123
- `<!-- BYAN-BENCH:skip reason=.. -->` instead. Full doctrine: see @.claude/rules/benchmark.md
124
+ `<!-- BYAN-BENCH:skip reason=.. -->` instead. Full doctrine (loaded on demand): see .claude/rules/benchmark.md
124
125
  <!-- BYAN-AUTOBENCH:END -->
126
+
127
+ ## Compact instructions
128
+
129
+ Quand tu compactes cette conversation, PRESERVE en priorite :
130
+ - l'etat FD BYAN actif s'il existe : phase courante, feature_name, le backlog avec le statut par item, le dernier verdict review/validate (source : `_byan-output/fd-state.json`).
131
+ - la session Strict Mode active s'il y en a une : scope_hash, criteres d'acceptation, nombre de passes, completion (source : `.byan-strict/state.json` + API byan_web).
132
+ - l'identite BYAN : le noyau immuable du soul + la voix tao (registre, signatures, tutoiement, zero emoji). `inject-tao` la reinjecte au SessionStart, mais garde la voix active dans le resume aussi.
133
+ - les derniers commits et tout travail non committe en cours.
134
+
135
+ Jette : les sorties d'outils deja exploitees, les dumps de fichiers verbeux, les sous-etapes resolues. Garde les decisions et les fils non resolus (recall d'abord, precision ensuite).
@@ -2,9 +2,10 @@
2
2
  /**
3
3
  * SessionStart hook — loads BYAN soul + soul-memory and injects them into
4
4
  * the session's initial context via additionalContext. Tao is intentionally
5
- * NOT bundled here: inject-tao.js injects the full tao on every
6
- * UserPromptSubmit (including the first), so duplicating it at SessionStart
7
- * would double-spend ~15 KB per session for no gain.
5
+ * NOT bundled here: inject-tao.js is its own SessionStart hook that injects the
6
+ * full tao once into the cacheable prefix, and inject-voice-anchor.js carries a
7
+ * compact per-turn voice reminder. Keeping them separate avoids double-spending
8
+ * the tao payload while leaving each hook single-purpose and testable.
8
9
  *
9
10
  * Also resets the per-session mid-session-nudge one-shot marker so the
10
11
  * soul-memory-triggers nudge is per-session (not per-lifetime). Without