create-byan-agent 2.29.2 → 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 +222 -0
- package/install/bin/create-byan-agent-v2.js +29 -1
- package/install/lib/gdoc-setup.js +210 -0
- package/install/lib/mcp-extensions/gdrive.js +27 -2
- package/install/lib/rtk-integration.js +18 -8
- 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 +1 -1
- 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
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,228 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
## [2.35.0] - 2026-06-26
|
|
13
|
+
|
|
14
|
+
### Added - Portable core / native projection doctrine + degradation litmus
|
|
15
|
+
|
|
16
|
+
BYAN's memory/identity is now governed by an explicit architecture doctrine: a
|
|
17
|
+
portable in-repo core, with native Claude features as opportunistic write-through
|
|
18
|
+
accelerators rather than dependencies.
|
|
19
|
+
|
|
20
|
+
- **F1 -- doctrine.** New `.claude/rules/portable-core.md` (+ install template)
|
|
21
|
+
states the boundary: source of truth lives under `_byan/` (+ byan_web); native
|
|
22
|
+
features (prompt caching, `@-import` memory files, hooks, subagent isolation)
|
|
23
|
+
are write-through accelerators; the native AutoMem
|
|
24
|
+
(`~/.claude/projects/<hash>/memory/`) is explicitly out-of-perimeter
|
|
25
|
+
(per-machine, not shippable, not a BYAN source). Carries a feature -> adapter
|
|
26
|
+
-> degraded-path table. Mirrored to `AGENTS.md` (Codex) and pointed to from
|
|
27
|
+
`.claude/CLAUDE.md` WITHOUT an `@-import` (token budget).
|
|
28
|
+
- **F2 -- degradation litmus.** New `.claude/__tests__/portable-core.test.js`
|
|
29
|
+
makes independence mechanical: identity (soul/tao/soul-memory) reconstructs from
|
|
30
|
+
portable `_byan/` artifacts alone, and no critical read path depends on the
|
|
31
|
+
native AutoMem (regression guard).
|
|
32
|
+
|
|
33
|
+
Origin: a memory-integration audit found BYAN runs a parallel memory stack that
|
|
34
|
+
does not touch the native AutoMem -- largely justified by npm portability, but
|
|
35
|
+
undocumented and drift-prone. This codifies the boundary instead of syncing two
|
|
36
|
+
stores (F3 anti-drift guard cut, YAGNI).
|
|
37
|
+
|
|
38
|
+
Files: `.claude/rules/portable-core.md` (+ template), `AGENTS.md`,
|
|
39
|
+
`.claude/CLAUDE.md` (+ template), `.claude/__tests__/portable-core.test.js`.
|
|
40
|
+
root jest 2441/2441, MCP node --test 689/689. Strict mode: scope 591e6e49.
|
|
41
|
+
|
|
42
|
+
## [2.34.0] - 2026-06-25
|
|
43
|
+
|
|
44
|
+
### Changed - Context engineering: do more with less (compaction + subagent isolation)
|
|
45
|
+
|
|
46
|
+
Two context-engineering moves from Anthropic's guidance, applied without touching
|
|
47
|
+
BYAN's persistent identity.
|
|
48
|
+
|
|
49
|
+
- **G2 -- compaction directive.** A `## Compact instructions` section in
|
|
50
|
+
`.claude/CLAUDE.md` tells the compaction what to preserve when a long session is
|
|
51
|
+
summarized: the active FD state (phase, backlog, verdicts -- `_byan-output/fd-state.json`),
|
|
52
|
+
the active Strict Mode session, BYAN's soul/tao voice, and recent commits. It
|
|
53
|
+
complements the `pre-compact-save` PreCompact hook (which writes a file snapshot):
|
|
54
|
+
one says what to keep in-context, the other persists a snapshot to disk.
|
|
55
|
+
- **G3 -- subagent isolation doctrine in hermes.** The dispatcher's worktree path
|
|
56
|
+
already capped the subagent's return; the `mcp-worker` path did not, and the
|
|
57
|
+
isolation principle was unnamed. Both spawn paths now cap the return to a
|
|
58
|
+
distilled summary (< 200 words / ~1-2k tokens) -- verbose tool output and
|
|
59
|
+
intermediate reasoning stay in the subagent's own context. A new "Subagent
|
|
60
|
+
isolation (token leverage)" section + a hard rule codify it (Anthropic: a
|
|
61
|
+
subagent may burn ~9k tokens internally yet return ~1-2k).
|
|
62
|
+
|
|
63
|
+
Files: `.claude/CLAUDE.md` (+ template), `.claude/skills/byan-hermes-dispatch/SKILL.md`
|
|
64
|
+
(+ template), skill-bundles manifest + ZIP rebuilt, `.claude/__tests__/claude-md-context-budget.test.js`.
|
|
65
|
+
root jest 2432/2432, MCP node --test 689/689. Source: Anthropic "Effective context engineering for AI agents".
|
|
66
|
+
|
|
67
|
+
## [2.33.0] - 2026-06-25
|
|
68
|
+
|
|
69
|
+
### Changed - Keep BYAN's voice alive on long sessions (tao persistence)
|
|
70
|
+
|
|
71
|
+
Hardens the 2.32.0 tao cache-alignment so BYAN's voice does not fade on a long
|
|
72
|
+
session, without re-introducing the per-turn cost.
|
|
73
|
+
|
|
74
|
+
- **F1 -- heart-survival pinned.** A test (`.claude/__tests__/soul-hooks.test.js`)
|
|
75
|
+
now fails if `inject-tao.js` stops being wired under SessionStart with an
|
|
76
|
+
all-sources matcher (so it keeps re-firing on `source: "compact"`) or stops
|
|
77
|
+
emitting the full tao. Claude Code re-fires SessionStart after each compaction
|
|
78
|
+
(per its docs), re-injecting the full tao then; this test guards that floor
|
|
79
|
+
against a silent refactor.
|
|
80
|
+
- **F2 -- periodic refresh.** `inject-voice-anchor.js` re-injects the FULL tao
|
|
81
|
+
every N turns (N via `BYAN_TAO_REFRESH_EVERY`, default 12); the other turns keep
|
|
82
|
+
the compact anchor. A per-turn counter under `_byan-output/` (gitignored), reset
|
|
83
|
+
at SessionStart by `inject-tao.js`, drives the cadence -- so the voice is
|
|
84
|
+
refreshed close to the live edge at least every N turns, between compactions.
|
|
85
|
+
Amortized cost stays well under the pre-2.32.0 per-turn tao.
|
|
86
|
+
- **Honest floor.** The periodic refresh is best-effort: it needs a writable
|
|
87
|
+
counter, and each turn is a separate process. If `_byan-output/` is not writable
|
|
88
|
+
the hook degrades to the anchor (exit 0, no crash) and the SessionStart /
|
|
89
|
+
compaction re-injection remains the floor. A test pins that degradation.
|
|
90
|
+
|
|
91
|
+
Built under BYAN Strict Mode; reviewed by bmad-compliance (one CHANGES round on the
|
|
92
|
+
FS-degradation honesty, then approve). root jest 2430/2430, MCP node --test 689/689.
|
|
93
|
+
|
|
94
|
+
## [2.32.0] - 2026-06-25
|
|
95
|
+
|
|
96
|
+
### Changed - Token cost reduction for BYAN's persistent identity (cache + dedup)
|
|
97
|
+
|
|
98
|
+
Cuts the per-turn token cost of BYAN's persistent identity payload without making
|
|
99
|
+
any of it conditional: tao, mantras, skeptic, ELO and fact-check stay applied each
|
|
100
|
+
turn. Two levers attack the transport/representation cost, not the presence.
|
|
101
|
+
|
|
102
|
+
- **Cache-align tao injection.** The full tao was re-injected each turn via a
|
|
103
|
+
UserPromptSubmit hook (~14.9 KB per turn, re-billed at the growing edge of the
|
|
104
|
+
conversation). It now loads once at SessionStart (`inject-tao.js` -> SessionStart,
|
|
105
|
+
the cacheable prefix), and a new `inject-voice-anchor.js` injects a compact
|
|
106
|
+
~95-token voice anchor each turn (register + signatures + tutoiement + zero-emoji
|
|
107
|
+
+ IA-16). Per-turn tao transport drops 14898 -> 383 chars (97.4% lower); the full
|
|
108
|
+
tao stays present, moved to the session prefix.
|
|
109
|
+
- **De-duplicate the persistent doctrine.** `strict-mode.md`, `benchmark.md` and
|
|
110
|
+
`fact-check.md` were `@`-imported by `CLAUDE.md`, force-loading the full files
|
|
111
|
+
into every turn (~5964 tokens). The `@`-imports become lean plain pointers. The
|
|
112
|
+
behavioral summary stays inline in `CLAUDE.md` and the enforcement lives in the
|
|
113
|
+
hooks (`strict-*-guard`, `autobench-stop-guard`, `fact-check-*`); the full rule
|
|
114
|
+
files stay reachable on demand via their skills. The benchmark pointer is
|
|
115
|
+
regenerated from the `sync-rules` renderer. `elo-trust` and `team-doctrine` keep
|
|
116
|
+
their `@`-import.
|
|
117
|
+
|
|
118
|
+
A third idea (output-side filtering) was closed as already covered by RTK + Claude
|
|
119
|
+
Code's native large-output truncation -- a documented redundancy, not a cut.
|
|
120
|
+
|
|
121
|
+
Files: `.claude/hooks/inject-tao.js` (now SessionStart), `.claude/hooks/inject-voice-anchor.js`
|
|
122
|
+
(new), `.claude/settings.json`, `.claude/CLAUDE.md`, `_byan/mcp/byan-mcp-server/lib/sync-rules.js`
|
|
123
|
+
(renderer), `_byan/mcp/byan-mcp-server/lib/template-sync.js` (ships the new hook), `AGENTS.md`
|
|
124
|
+
(regenerated), plus the template mirrors. Built under BYAN Strict Mode; reviewed by bmad-compliance
|
|
125
|
+
(approve, identity line held); root jest 2420/2420, MCP node --test 689/689, zero emoji. Token
|
|
126
|
+
figures are char/4 estimates; `/context` is the exact counter-test.
|
|
127
|
+
|
|
128
|
+
## [2.31.0] - 2026-06-24
|
|
129
|
+
|
|
130
|
+
### Added - Install-time service-account key setup for byan_publish
|
|
131
|
+
|
|
132
|
+
BYAN is open-source, so no Google key ships in the npm package: each user
|
|
133
|
+
provides their own service-account key, on their own machine. This release adds
|
|
134
|
+
the install-time setup that wires that key for `byan_publish` (the headless
|
|
135
|
+
Google Docs publisher from 2.30.0).
|
|
136
|
+
|
|
137
|
+
- `install/lib/gdoc-setup.js` (new) : `setupGdocPublish` guides the user to the
|
|
138
|
+
Google Cloud console, imports the downloaded SA JSON into
|
|
139
|
+
`~/.byan/google-sa.json` (mode `0600`, dir `0700`), validates it
|
|
140
|
+
(`client_email` + `private_key`), then persists the path via `writeCredentials`
|
|
141
|
+
(`GOOGLE_APPLICATION_CREDENTIALS` + optional `GDOC_TEMPLATE_ID` /
|
|
142
|
+
`GDOC_LOGO_PNG_URL`). Every side-effecting dep (prompt / fs / writeCredentials)
|
|
143
|
+
is injected; the function stays graceful (a missing, invalid, or unreadable key
|
|
144
|
+
degrades to `configured:false` rather than throwing). Only the key path is
|
|
145
|
+
persisted -- the secret stays on disk in `~/.byan/`, out of the repo.
|
|
146
|
+
- `install/setup-gdoc.js` (new) + `npm run setup-gdoc` : run the setup on demand,
|
|
147
|
+
outside the installer.
|
|
148
|
+
- `install/bin/create-byan-agent-v2.js` : opt-in block (prompt default no, mirrors
|
|
149
|
+
the RTK block; gated by `shouldOfferGdoc` -- TTY only, skipped on
|
|
150
|
+
`BYAN_SKIP_GDOC=1`).
|
|
151
|
+
- `@byan/platform-config` : `KNOWN_KEYS` extended with
|
|
152
|
+
`GOOGLE_APPLICATION_CREDENTIALS`, `GDOC_TEMPLATE_ID`, `GDOC_LOGO_PNG_URL` so
|
|
153
|
+
`writeCredentials` persists them (covered by test).
|
|
154
|
+
- Guide : `docs/google-docs-publish.md` documents the open-source per-user key
|
|
155
|
+
model and the `npm run setup-gdoc` path (mirrored to `install/templates/docs/`).
|
|
156
|
+
|
|
157
|
+
Built under BYAN Strict Mode (scope locked, self-verified) ; secrets and
|
|
158
|
+
never-throws self-reviewed PASS (independent compliance pass to re-run before
|
|
159
|
+
publish, transient API outage at review time). Root jest 2389/0.
|
|
160
|
+
|
|
161
|
+
## [2.30.0] - 2026-06-24
|
|
162
|
+
|
|
163
|
+
### Added - byan_publish : Google Docs brandés, headless (service account)
|
|
164
|
+
|
|
165
|
+
New MCP tool `byan_publish` : a byan-owned, headless Google Docs publisher. A
|
|
166
|
+
service-account JWT (durable, no OAuth, no browser, no 7-day refresh-token expiry)
|
|
167
|
+
creates a branded Google Doc from a content object and returns its URL, optionally
|
|
168
|
+
sharing it. Distinct from the gw OAuth connector.
|
|
169
|
+
|
|
170
|
+
- `lib/gdoc-content.js` (pure) : content -> Docs `batchUpdate` requests. Template
|
|
171
|
+
mode (`replaceAllText` over a branded template) or programmatic mode (insert +
|
|
172
|
+
the AcadéNice palette : marine `#0e2656`, teal `#24947a`, turquoise `#4cccb8`).
|
|
173
|
+
- `lib/gdoc-client.js` : service-account auth (google-auth-library JWT, scopes
|
|
174
|
+
`documents` + `drive.file`) + create-or-copy + `batchUpdate` +
|
|
175
|
+
`permissions.create`. googleapis is lazy-loaded so the server boots without it ;
|
|
176
|
+
every failure path returns `{ ok:false, reason }` (no-credentials /
|
|
177
|
+
bad-credentials / invalid-content / dep-missing / api-error) rather than
|
|
178
|
+
throwing. The SA key is read from a path (`GOOGLE_APPLICATION_CREDENTIALS`, via
|
|
179
|
+
resolve-config) and stays on disk -- not serialized into any output.
|
|
180
|
+
- `byan_publish` registered in server.js, kept OUT of `REMOTE_SAFE_TOOLS`
|
|
181
|
+
(network+auth, stdio-only). New deps : `googleapis` + `google-auth-library`.
|
|
182
|
+
- Guide : `docs/google-docs-publish.md` (SA key recipe + usage + branding/template
|
|
183
|
+
+ the standalone-vs-Workspace ownership model).
|
|
184
|
+
|
|
185
|
+
The single manual step is creating the SA key (Google Cloud IAM). RNCP/eval
|
|
186
|
+
content plugs in later as a thin adapter. Built under BYAN Strict Mode (scope
|
|
187
|
+
locked, self-verified) ; reviewed by bmad-compliance (security : secrets +
|
|
188
|
+
no-throw PASS, narrow scopes, fact-check floor L1). MCP suite 687/687, root jest
|
|
189
|
+
2378/0.
|
|
190
|
+
|
|
191
|
+
## [2.29.4] - 2026-06-24
|
|
192
|
+
|
|
193
|
+
### Changed - Google Workspace (gdrive) install: durable OAuth "Internal"
|
|
194
|
+
|
|
195
|
+
The installer's Google Workspace setup guide now points to an OAuth consent
|
|
196
|
+
screen in "Internal" mode instead of "External / Testing". "External + Testing"
|
|
197
|
+
expires the refresh token in ~7 days for scopes beyond openid/email/profile (all
|
|
198
|
+
of gw's Drive/Docs/... scopes qualify); "Internal" removes that expiry and skips
|
|
199
|
+
Google app verification (source: developers.google.com/identity/protocols/oauth2).
|
|
200
|
+
The result: ONE durable OAuth client is byan's single Google credential, and the
|
|
201
|
+
claude.ai Drive connector becomes redundant.
|
|
202
|
+
|
|
203
|
+
- `install/lib/mcp-extensions/gdrive.js`: `SETUP_LINKS` + `printSetupGuide` guide
|
|
204
|
+
to Internal, with the durability rationale, the org-Workspace prerequisite, and
|
|
205
|
+
the honest limit (one browser login at setup, org-only). A durability reminder
|
|
206
|
+
fires on the reuse path too.
|
|
207
|
+
- The package `google-workspace-mcp` does not support service accounts (its
|
|
208
|
+
README), so "Internal" is the durable path that keeps its 95+ tools. A
|
|
209
|
+
service-account route (for fully headless publishing) would be a separate,
|
|
210
|
+
byan-owned Google client — out of scope here.
|
|
211
|
+
- New guide: `docs/google-workspace-setup.md` (one-time Internal recipe + the
|
|
212
|
+
mutualization rationale).
|
|
213
|
+
|
|
214
|
+
Reviewed by bmad-compliance (auth domain, fact-check floor L1). 2378 tests green.
|
|
215
|
+
|
|
216
|
+
## [2.29.3] - 2026-06-24
|
|
217
|
+
|
|
218
|
+
### Fixed - RTK hook now actually installed (--auto-patch)
|
|
219
|
+
|
|
220
|
+
2.29.2 wired only the RTK instruction layer, not the transparent hook. `wireHook`
|
|
221
|
+
ran `rtk init -g` with stdio piped (no TTY), but bare `rtk init -g` PROMPTS before
|
|
222
|
+
patching `settings.json`, so the prompt was skipped and rtk wrote `RTK.md` + the
|
|
223
|
+
`@RTK.md` reference WITHOUT the PreToolUse hook — `rtk init --show` reported
|
|
224
|
+
"Hook: not found". `wireHook` now calls `rtk init -g --auto-patch`, which patches
|
|
225
|
+
`settings.json` non-interactively, so the command-rewriting hook is installed. An
|
|
226
|
+
older rtk lacking the flag degrades gracefully (reason `hook-failed`; the install
|
|
227
|
+
still succeeds). `doctor()` and the installer consent prompt were updated to match.
|
|
228
|
+
Verified live on a Debian/zsh host: `rtk init --show` reports "Hook: configured"
|
|
229
|
+
after the fix.
|
|
230
|
+
|
|
231
|
+
If you installed 2.29.x before this and RTK feels inactive, re-run
|
|
232
|
+
`rtk init -g --auto-patch`.
|
|
233
|
+
|
|
12
234
|
## [2.29.2] - 2026-06-23
|
|
13
235
|
|
|
14
236
|
### Fixed - RTK optional install: fail-proof, off-PATH, shell-aware
|
|
@@ -17,6 +17,7 @@ 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
19
|
const { setupRtkIntegration, shouldOfferRtk } = require('../lib/rtk-integration');
|
|
20
|
+
const { setupGdocPublish, shouldOfferGdoc } = require('../lib/gdoc-setup');
|
|
20
21
|
const { setupClaudeNative } = require('../lib/claude-native-setup');
|
|
21
22
|
const { setupCodexNative } = require('../lib/codex-native-setup');
|
|
22
23
|
const { setupMcpExtensions } = require('../lib/mcp-extensions');
|
|
@@ -1378,7 +1379,7 @@ async function install(options = {}) {
|
|
|
1378
1379
|
{
|
|
1379
1380
|
type: 'confirm',
|
|
1380
1381
|
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
|
+
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 --auto-patch`.',
|
|
1382
1383
|
default: false,
|
|
1383
1384
|
},
|
|
1384
1385
|
]);
|
|
@@ -1400,6 +1401,33 @@ async function install(options = {}) {
|
|
|
1400
1401
|
}
|
|
1401
1402
|
}
|
|
1402
1403
|
|
|
1404
|
+
if (needsClaude && shouldOfferGdoc()) {
|
|
1405
|
+
console.log();
|
|
1406
|
+
console.log(chalk.cyan('byan_publish — clé service account Google Docs (optionnel, headless)'));
|
|
1407
|
+
try {
|
|
1408
|
+
const { proceed } = await inquirer.prompt([
|
|
1409
|
+
{
|
|
1410
|
+
type: 'confirm',
|
|
1411
|
+
name: 'proceed',
|
|
1412
|
+
message: 'Configurer ta clé service account maintenant (chacun la sienne ; rien de secret ne ship) ?',
|
|
1413
|
+
default: false,
|
|
1414
|
+
},
|
|
1415
|
+
]);
|
|
1416
|
+
if (proceed) {
|
|
1417
|
+
const r = await setupGdocPublish({ log: (...a) => console.log(...a) });
|
|
1418
|
+
if (r.configured) {
|
|
1419
|
+
console.log(chalk.green(` ✓ byan_publish prêt (clé : ${r.path})`));
|
|
1420
|
+
} else {
|
|
1421
|
+
console.log(chalk.yellow(` ⚠ byan_publish non configuré (${r.skipReason}) — \`npm run setup-gdoc\` à tout moment`));
|
|
1422
|
+
}
|
|
1423
|
+
} else {
|
|
1424
|
+
console.log(chalk.gray(' byan_publish ignoré — `npm run setup-gdoc` à tout moment pour activer.'));
|
|
1425
|
+
}
|
|
1426
|
+
} catch (error) {
|
|
1427
|
+
console.log(chalk.yellow(` ⚠ byan_publish setup skipped: ${error.message}`));
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1403
1431
|
if (needsClaude) {
|
|
1404
1432
|
console.log();
|
|
1405
1433
|
console.log(chalk.cyan('Leantime board sync (optional — self-hosted)'));
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* gdoc-setup -- install-time setup of the byan_publish service-account key.
|
|
5
|
+
*
|
|
6
|
+
* BYAN is open source : NO key ships. Each user provides THEIR OWN Google service
|
|
7
|
+
* account at install time. This module walks the user through creating the SA +
|
|
8
|
+
* key in the Google Cloud console, imports the downloaded JSON to
|
|
9
|
+
* ~/.byan/google-sa.json (chmod 0600), and persists the publish config
|
|
10
|
+
* (GOOGLE_APPLICATION_CREDENTIALS + optional GDOC_TEMPLATE_ID / GDOC_LOGO_PNG_URL)
|
|
11
|
+
* into ~/.byan/credentials.json via byan-platform-config's writeCredentials. The
|
|
12
|
+
* byan MCP server then reads them through resolve-config.
|
|
13
|
+
*
|
|
14
|
+
* Mirrors the shape of rtk-integration.js : graceful (NEVER throws, ok stays
|
|
15
|
+
* true so a failed step never breaks the BYAN install), and every side-effecting
|
|
16
|
+
* dep (prompt / fs / writeCredentials) is injectable, so the whole flow is
|
|
17
|
+
* unit-tested without touching a real key, the network, or the user HOME.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const os = require('os');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const fs = require('fs-extra');
|
|
23
|
+
const chalk = require('chalk');
|
|
24
|
+
const inquirer = require('inquirer');
|
|
25
|
+
const { writeCredentials } = require('byan-platform-config');
|
|
26
|
+
|
|
27
|
+
const SA_FILENAME = 'google-sa.json';
|
|
28
|
+
|
|
29
|
+
const SETUP_LINKS = [
|
|
30
|
+
{
|
|
31
|
+
step: "Projet Google Cloud (idéalement rattaché à ton org Workspace)",
|
|
32
|
+
url: 'https://console.cloud.google.com/projectcreate',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
step: 'Activer les APIs Google Docs + Google Drive',
|
|
36
|
+
url: 'https://console.cloud.google.com/apis/library',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
step: 'Créer un service account',
|
|
40
|
+
url: 'https://console.cloud.google.com/iam-admin/serviceaccounts',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
step: 'Sur le service account -> Keys -> Add key -> Create new key -> JSON -> télécharger',
|
|
44
|
+
url: 'https://console.cloud.google.com/iam-admin/serviceaccounts',
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
function saDestPath(homedir = os.homedir()) {
|
|
49
|
+
return path.join(homedir, '.byan', SA_FILENAME);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A plausible service-account key : an object carrying a client_email and a
|
|
54
|
+
* private_key. Shape-only -- we never validate the key cryptographically.
|
|
55
|
+
*/
|
|
56
|
+
function validateServiceAccount(parsed) {
|
|
57
|
+
return Boolean(
|
|
58
|
+
parsed &&
|
|
59
|
+
typeof parsed === 'object' &&
|
|
60
|
+
typeof parsed.client_email === 'string' &&
|
|
61
|
+
parsed.client_email.trim() &&
|
|
62
|
+
typeof parsed.private_key === 'string' &&
|
|
63
|
+
parsed.private_key.includes('PRIVATE KEY')
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* shouldOfferGdoc() -> offer the setup only when interactive and not opted out
|
|
69
|
+
* (BYAN_SKIP_GDOC=1). Never prompts in CI / non-TTY.
|
|
70
|
+
*/
|
|
71
|
+
function shouldOfferGdoc({ env = process.env, isTTY = !!(process.stdin && process.stdin.isTTY) } = {}) {
|
|
72
|
+
if (env && env.BYAN_SKIP_GDOC === '1') return false;
|
|
73
|
+
return Boolean(isTTY);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function printSetupGuide(log) {
|
|
77
|
+
log();
|
|
78
|
+
log(chalk.cyan('Clé service account pour byan_publish (Google Docs headless) :'));
|
|
79
|
+
log(chalk.gray(' Open source -> tu fournis TA clé ; rien de secret ne ship. ~2 min, une fois.'));
|
|
80
|
+
SETUP_LINKS.forEach((s, i) => {
|
|
81
|
+
log(chalk.gray(` ${i + 1}. ${s.step}`));
|
|
82
|
+
log(chalk.gray(` -> ${s.url}`));
|
|
83
|
+
});
|
|
84
|
+
log();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* setupGdocPublish(deps) -> { ok, configured, ... }. Never throws ; ok stays
|
|
89
|
+
* true on every path. configured=true only when a valid key was imported and
|
|
90
|
+
* persisted.
|
|
91
|
+
*
|
|
92
|
+
* @param {object} [deps]
|
|
93
|
+
* @param {Function} [deps.prompt] inquirer.prompt (injected in tests)
|
|
94
|
+
* @param {Function} [deps.log] line sink
|
|
95
|
+
* @param {string} [deps.homedir]
|
|
96
|
+
* @param {object} [deps.fsImpl] fs-extra-like (readJson/ensureDir/writeFile/chmod)
|
|
97
|
+
* @param {Function} [deps.persist] writeCredentials (injected in tests)
|
|
98
|
+
* @param {boolean} [deps.quiet]
|
|
99
|
+
*/
|
|
100
|
+
async function setupGdocPublish(deps = {}) {
|
|
101
|
+
const prompt = deps.prompt || inquirer.prompt;
|
|
102
|
+
const homedir = deps.homedir || os.homedir();
|
|
103
|
+
const fsImpl = deps.fsImpl || fs;
|
|
104
|
+
const persist = deps.persist || writeCredentials;
|
|
105
|
+
const log = deps.quiet ? () => {} : deps.log || ((...a) => console.log(...a));
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
printSetupGuide(log);
|
|
109
|
+
|
|
110
|
+
const { hasKey } = await prompt([
|
|
111
|
+
{
|
|
112
|
+
type: 'confirm',
|
|
113
|
+
name: 'hasKey',
|
|
114
|
+
message: 'Tu as téléchargé le JSON de la clé service account ?',
|
|
115
|
+
default: false,
|
|
116
|
+
},
|
|
117
|
+
]);
|
|
118
|
+
if (!hasKey) {
|
|
119
|
+
return {
|
|
120
|
+
ok: true,
|
|
121
|
+
configured: false,
|
|
122
|
+
skipReason: "Setup reporté -- relance `npm run setup-gdoc` une fois le JSON téléchargé.",
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const { jsonPath } = await prompt([
|
|
127
|
+
{
|
|
128
|
+
type: 'input',
|
|
129
|
+
name: 'jsonPath',
|
|
130
|
+
message: 'Chemin local vers le JSON de la clé SA :',
|
|
131
|
+
validate: (v) => (v && v.trim().length > 0) || 'Chemin requis',
|
|
132
|
+
},
|
|
133
|
+
]);
|
|
134
|
+
|
|
135
|
+
let parsed;
|
|
136
|
+
try {
|
|
137
|
+
parsed = await fsImpl.readJson(path.resolve(jsonPath.trim()));
|
|
138
|
+
} catch (e) {
|
|
139
|
+
return { ok: true, configured: false, skipReason: `JSON illisible : ${e.message}` };
|
|
140
|
+
}
|
|
141
|
+
if (!validateServiceAccount(parsed)) {
|
|
142
|
+
return {
|
|
143
|
+
ok: true,
|
|
144
|
+
configured: false,
|
|
145
|
+
skipReason:
|
|
146
|
+
'Ce fichier ne ressemble pas à une clé service account (client_email + private_key requis).',
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Store the key OUTSIDE any repo, in ~/.byan/, at 0600 (created restrictively).
|
|
151
|
+
const dir = path.join(homedir, '.byan');
|
|
152
|
+
const dest = saDestPath(homedir);
|
|
153
|
+
await fsImpl.ensureDir(dir);
|
|
154
|
+
try {
|
|
155
|
+
await fsImpl.chmod(dir, 0o700);
|
|
156
|
+
} catch {
|
|
157
|
+
// POSIX modes unsupported -> proceed without the dir tightening.
|
|
158
|
+
}
|
|
159
|
+
await fsImpl.writeFile(dest, JSON.stringify(parsed, null, 2) + '\n', { mode: 0o600 });
|
|
160
|
+
try {
|
|
161
|
+
await fsImpl.chmod(dest, 0o600);
|
|
162
|
+
} catch {
|
|
163
|
+
// see above
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const { templateId } = await prompt([
|
|
167
|
+
{
|
|
168
|
+
type: 'input',
|
|
169
|
+
name: 'templateId',
|
|
170
|
+
message: 'ID d\'un Google Doc template brandé (optionnel, Entrée pour passer) :',
|
|
171
|
+
default: '',
|
|
172
|
+
},
|
|
173
|
+
]);
|
|
174
|
+
const { logoUrl } = await prompt([
|
|
175
|
+
{
|
|
176
|
+
type: 'input',
|
|
177
|
+
name: 'logoUrl',
|
|
178
|
+
message: 'URL PNG du logo (optionnel, Entrée pour passer) :',
|
|
179
|
+
default: '',
|
|
180
|
+
},
|
|
181
|
+
]);
|
|
182
|
+
|
|
183
|
+
const values = { GOOGLE_APPLICATION_CREDENTIALS: dest };
|
|
184
|
+
if (templateId && templateId.trim()) values.GDOC_TEMPLATE_ID = templateId.trim();
|
|
185
|
+
if (logoUrl && logoUrl.trim()) values.GDOC_LOGO_PNG_URL = logoUrl.trim();
|
|
186
|
+
|
|
187
|
+
const res = await persist(values, { homedir });
|
|
188
|
+
log(chalk.green(` clé SA -> ${dest} (0600)`));
|
|
189
|
+
log(chalk.gray(` persisté dans ~/.byan/credentials.json : ${(res.written || []).join(', ')}`));
|
|
190
|
+
return {
|
|
191
|
+
ok: true,
|
|
192
|
+
configured: true,
|
|
193
|
+
path: dest,
|
|
194
|
+
persisted: res.written || [],
|
|
195
|
+
clientEmail: parsed.client_email,
|
|
196
|
+
};
|
|
197
|
+
} catch (err) {
|
|
198
|
+
return { ok: true, configured: false, skipReason: `setup gdoc échoué : ${err.message}` };
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
module.exports = {
|
|
203
|
+
setupGdocPublish,
|
|
204
|
+
shouldOfferGdoc,
|
|
205
|
+
validateServiceAccount,
|
|
206
|
+
printSetupGuide,
|
|
207
|
+
saDestPath,
|
|
208
|
+
SETUP_LINKS,
|
|
209
|
+
SA_FILENAME,
|
|
210
|
+
};
|
|
@@ -16,6 +16,17 @@
|
|
|
16
16
|
*
|
|
17
17
|
* No credential ever touches the project tree. The .mcp.json entry only
|
|
18
18
|
* declares command/args — every secret stays in ~/.google-mcp/.
|
|
19
|
+
*
|
|
20
|
+
* DURABLE + MUTUALIZED AUTH (the design choice): we guide the user to an OAuth
|
|
21
|
+
* consent screen in "Internal" mode (Workspace-org only). "External + Testing"
|
|
22
|
+
* expires the refresh token in ~7 days for scopes beyond openid/email/profile
|
|
23
|
+
* (all of gw's Drive/Docs/... scopes qualify) ; "Internal" does not, and skips
|
|
24
|
+
* Google app verification (source: developers.google.com/identity/protocols/oauth2).
|
|
25
|
+
* This OAuth client is THE single Google credential of byan — the claude.ai Drive
|
|
26
|
+
* connector becomes redundant. NOTE: this package (pm990320/google-workspace-mcp)
|
|
27
|
+
* does NOT support service accounts (README: "Service account authentication is
|
|
28
|
+
* not currently supported"), so a service-account JSON is not an option here ;
|
|
29
|
+
* "Internal" is the durable path that keeps the package's 95+ tools.
|
|
19
30
|
*/
|
|
20
31
|
|
|
21
32
|
'use strict';
|
|
@@ -33,7 +44,7 @@ const CREDENTIALS_PATH = path.join(CONFIG_DIR, 'credentials.json');
|
|
|
33
44
|
|
|
34
45
|
const SETUP_LINKS = [
|
|
35
46
|
{
|
|
36
|
-
step: 'Créer un projet Google Cloud',
|
|
47
|
+
step: 'Créer un projet Google Cloud RATTACHÉ à ton organisation Workspace (ex : acadenice.fr) — pas un compte Gmail perso',
|
|
37
48
|
url: 'https://console.cloud.google.com/projectcreate',
|
|
38
49
|
},
|
|
39
50
|
{
|
|
@@ -41,7 +52,7 @@ const SETUP_LINKS = [
|
|
|
41
52
|
url: 'https://console.cloud.google.com/apis/library',
|
|
42
53
|
},
|
|
43
54
|
{
|
|
44
|
-
step: '
|
|
55
|
+
step: 'Écran de consentement OAuth : User type = "Internal" (durable : pas d\'expiration 7 j, pas de vérification Google)',
|
|
45
56
|
url: 'https://console.cloud.google.com/apis/credentials/consent',
|
|
46
57
|
},
|
|
47
58
|
{
|
|
@@ -93,6 +104,16 @@ function printSetupGuide(log) {
|
|
|
93
104
|
log(chalk.gray(` 6. Renommer ce fichier en : credentials.json`));
|
|
94
105
|
log(chalk.gray(` 7. Le placer dans : ${CREDENTIALS_PATH}`));
|
|
95
106
|
log();
|
|
107
|
+
// The single thing that makes the credential durable. "External + Testing"
|
|
108
|
+
// expires the refresh token in ~7 days for scopes beyond openid/email/profile
|
|
109
|
+
// (all gw scopes qualify) ; "Internal" does not, and skips Google app
|
|
110
|
+
// verification. Source: developers.google.com/identity/protocols/oauth2.
|
|
111
|
+
log(chalk.yellow(' Durabilité — LE point qui compte :'));
|
|
112
|
+
log(chalk.yellow(' Écran de consentement en "Internal" → credential pérenne (token qui ne meurt pas).'));
|
|
113
|
+
log(chalk.yellow(' "External + Testing" → refresh token expiré sous ~7 jours (scopes hors openid/email/profile, donc tous ceux de gw). À éviter.'));
|
|
114
|
+
log(chalk.gray(' Limite "Internal" : seuls les comptes de ton org Workspace peuvent autoriser, et il faut UN login navigateur au premier setup.'));
|
|
115
|
+
log(chalk.gray(' Ce client OAuth devient LE credential Google unique de byan (le connecteur claude.ai Drive devient redondant).'));
|
|
116
|
+
log();
|
|
96
117
|
}
|
|
97
118
|
|
|
98
119
|
async function importCredentialsFromPath(srcPath, log) {
|
|
@@ -174,6 +195,8 @@ async function setup({ quiet } = {}) {
|
|
|
174
195
|
},
|
|
175
196
|
]);
|
|
176
197
|
if (reuse) {
|
|
198
|
+
log(chalk.gray(' Rappel durabilité : si ce client OAuth est en "External + Testing", son token meurt sous ~7 j (scopes Workspace gw concernés).'));
|
|
199
|
+
log(chalk.gray(' Pour un credential pérenne, l\'écran de consentement doit être en "Internal" (org Workspace).'));
|
|
177
200
|
return { configured: true, message: 'reused existing credentials' };
|
|
178
201
|
}
|
|
179
202
|
} else {
|
|
@@ -250,7 +273,9 @@ module.exports = {
|
|
|
250
273
|
buildMcpEntry,
|
|
251
274
|
// exposed for tests
|
|
252
275
|
buildEntry,
|
|
276
|
+
printSetupGuide,
|
|
253
277
|
CONFIG_DIR,
|
|
254
278
|
CREDENTIALS_PATH,
|
|
255
279
|
PACKAGE_NAME,
|
|
280
|
+
SETUP_LINKS,
|
|
256
281
|
};
|
|
@@ -11,9 +11,10 @@
|
|
|
11
11
|
* - We do NOT reimplement per-OS download / checksum / version pinning. We
|
|
12
12
|
* DELEGATE the install to rtk's own canonical installer (brew / the official
|
|
13
13
|
* install.sh / cargo), and DELEGATE the Claude Code hook wiring to rtk's own
|
|
14
|
-
* `rtk init -g
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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.
|
|
17
18
|
* - SUPPLY-CHAIN: we pin to a TAG, never a moving branch. cargo builds the
|
|
18
19
|
* tagged source (`--tag`), and install.sh is fetched from the IMMUTABLE tag ref.
|
|
19
20
|
* That script verifies a SHA-256 of the downloaded binary against a published
|
|
@@ -38,7 +39,7 @@
|
|
|
38
39
|
*
|
|
39
40
|
* Mirrors the ENTRY-POINT shape of byan-web-integration.js / byan-leantime-
|
|
40
41
|
* integration.js (one `setup*Integration`); the return contract is { ok, synced,
|
|
41
|
-
* 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
|
|
42
43
|
* byan-platform-config.
|
|
43
44
|
*/
|
|
44
45
|
|
|
@@ -187,18 +188,27 @@ function doctor({ run = execSync, has = commandExists, resolve = resolveBinary,
|
|
|
187
188
|
bin: s.bin,
|
|
188
189
|
pinned: RTK_VERSION,
|
|
189
190
|
repo: RTK_REPO,
|
|
190
|
-
hookCommand: 'rtk init -g',
|
|
191
|
+
hookCommand: 'rtk init -g --auto-patch',
|
|
191
192
|
};
|
|
192
193
|
}
|
|
193
194
|
|
|
194
195
|
// Delegate the Claude Code hook wiring to rtk's OWN command (idempotent). This is
|
|
195
196
|
// the maintainability win: no manual settings.json merge to keep in sync. Wires
|
|
196
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`.)
|
|
197
206
|
function wireHook({ run, log, installedVia, version, bin = 'rtk', env = process.env, platform = process.platform }) {
|
|
198
207
|
const offPath = bin !== 'rtk';
|
|
208
|
+
const initCmd = `${bin} init -g --auto-patch`;
|
|
199
209
|
try {
|
|
200
|
-
run(
|
|
201
|
-
log(`rtk: ready (${installedVia}, v${version || '?'}) — hook wired via '${
|
|
210
|
+
run(initCmd, { stdio: 'pipe' });
|
|
211
|
+
log(`rtk: ready (${installedVia}, v${version || '?'}) — hook wired via '${initCmd}'. Restart Claude Code to activate.`);
|
|
202
212
|
const r = { ok: true, synced: true, reason: 'wired', installed: true, installedVia, version, hook: true, bin };
|
|
203
213
|
if (offPath) {
|
|
204
214
|
r.pathHint = pathHintFor(bin, { env, platform });
|
|
@@ -206,7 +216,7 @@ function wireHook({ run, log, installedVia, version, bin = 'rtk', env = process.
|
|
|
206
216
|
}
|
|
207
217
|
return r;
|
|
208
218
|
} catch (err) {
|
|
209
|
-
log(`rtk: installed (${installedVia}) but '${
|
|
219
|
+
log(`rtk: installed (${installedVia}) but '${initCmd}' failed (${oneLine(err)}) — run it manually, BYAN unaffected.`);
|
|
210
220
|
const r = { ok: true, synced: false, reason: 'hook-failed', installed: true, installedVia, version, hook: false, bin };
|
|
211
221
|
if (offPath) r.pathHint = pathHintFor(bin, { env, platform });
|
|
212
222
|
return r;
|
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');
|