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.
- package/CHANGELOG.md +250 -0
- package/install/bin/create-byan-agent-v2.js +33 -2
- package/install/lib/gdoc-setup.js +210 -0
- package/install/lib/mcp-extensions/gdrive.js +27 -2
- package/install/lib/native-helper.js +68 -1
- package/install/lib/rtk-integration.js +193 -57
- package/install/package.json +1 -1
- package/install/packages/platform-config/lib/credentials.js +13 -1
- package/install/setup-gdoc.js +41 -0
- package/install/setup-rtk.js +7 -3
- package/install/templates/.claude/CLAUDE.md +15 -4
- package/install/templates/.claude/hooks/inject-soul.js +4 -3
- package/install/templates/.claude/hooks/inject-tao.js +65 -25
- package/install/templates/.claude/hooks/inject-voice-anchor.js +102 -0
- package/install/templates/.claude/rules/portable-core.md +81 -0
- package/install/templates/.claude/settings.json +5 -1
- package/install/templates/.claude/skills/byan-hermes-dispatch/SKILL.md +16 -2
- package/install/templates/_byan/mcp/byan-mcp-server/lib/gdoc-client.js +203 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/gdoc-content.js +203 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/resolve-config.js +15 -2
- package/install/templates/_byan/mcp/byan-mcp-server/lib/sync-rules.js +1 -1
- package/install/templates/_byan/mcp/byan-mcp-server/package.json +2 -0
- package/install/templates/_byan/mcp/byan-mcp-server/server.js +70 -0
- package/install/templates/_byan/mcp/byan-mcp-server/skill-bundles-manifest.json +1 -1
- package/install/templates/dist/skill-bundles/byan-hermes-dispatch.zip +0 -0
- package/install/templates/docs/google-docs-publish.md +121 -0
- package/node_modules/byan-platform-config/lib/credentials.js +13 -1
- 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 /
|
|
13
|
-
*
|
|
14
|
-
* `rtk init -g
|
|
15
|
-
*
|
|
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
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* -
|
|
23
|
-
*
|
|
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
|
|
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.
|
|
44
|
-
*
|
|
45
|
-
*
|
|
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
|
-
|
|
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.
|
|
64
|
-
* ("rtk 0.42.4"). An installed-but-
|
|
65
|
-
*
|
|
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(
|
|
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.
|
|
86
|
-
*
|
|
87
|
-
*
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
106
|
-
log(`rtk: ready (${installedVia}, v${version || '?'}) — hook wired via '
|
|
107
|
-
|
|
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 '
|
|
110
|
-
|
|
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
|
|
118
|
-
* pick an available installer, delegate
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
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]
|
|
125
|
-
* @param {Function} [o.has]
|
|
126
|
-
* @param {Function} [o.
|
|
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({
|
|
129
|
-
|
|
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 /
|
|
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
|
|
141
|
-
//
|
|
142
|
-
|
|
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: '
|
|
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
|
-
|
|
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
|
};
|
package/install/package.json
CHANGED
|
@@ -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
|
-
|
|
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 };
|
package/install/setup-rtk.js
CHANGED
|
@@ -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
|
|
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 /
|
|
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
|
|
66
|
-
- Mode strict anti-downgrade: voir
|
|
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
|
|
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
|
|
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
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|