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
package/CHANGELOG.md CHANGED
@@ -9,6 +9,256 @@ 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
+
234
+ ## [2.29.2] - 2026-06-23
235
+
236
+ ### Fixed - RTK optional install: fail-proof, off-PATH, shell-aware
237
+
238
+ The 2.29.x RTK installer could hang silently and mis-report a successful build.
239
+
240
+ - **Bounded + visible.** The delegated install now runs with a per-strategy
241
+ timeout (brew/script 5min, cargo 20min; override via `BYAN_RTK_TIMEOUT_MS`,
242
+ positive integers only) and inherited stdio, so progress streams live instead
243
+ of a frozen line. Inherited stdio also sidesteps execSync's 1MB maxBuffer cap a
244
+ verbose build would blow.
245
+ - **Off-PATH resolution.** `cargo install` drops the binary in `~/.cargo/bin`,
246
+ which a Debian non-login PATH does not include — the install succeeded but
247
+ `rtk --version` reported "unverified". The installer now resolves rtk across
248
+ known dirs (PATH, `~/.cargo/bin`, `~/.local/bin`, `/usr/local/bin`, brew prefix,
249
+ `CARGO_HOME`), verifies + wires the hook by the resolved path, and prints a
250
+ shell-correct PATH hint (`fish_add_path` on fish, `export` on bash/zsh,
251
+ `$env:PATH` on Windows).
252
+ - **Prebuilt preferred.** Strategy order is now brew > script > cargo: the
253
+ prebuilt-binary script (pinned to the immutable tag ref, which SHA-256-verifies
254
+ the binary per refs/tags/v0.42.4/install.sh) is fast and PATH-stable; cargo
255
+ (from-source, off-PATH, slow) becomes the last-resort fallback.
256
+ - **Stays graceful.** Hardened the no-throw contract: a HOME-less environment
257
+ (`os.homedir()` throwing) no longer propagates out of `setup-rtk.js`.
258
+
259
+ Reviewed by an adversarial workflow (correctness + supply-chain + mantras) plus a
260
+ compliance pass; 2375/2375 tests green (49 dedicated to RTK).
261
+
12
262
  ## [2.29.1] - 2026-06-23
13
263
 
14
264
  ### Fixed - Republish (the 2.29.0 npm tarball was missing)
@@ -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,16 +1379,19 @@ 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
  ]);
1385
1386
  if (proceed) {
1387
+ console.log(chalk.gray(' Installing... progress streams below. The cargo fallback compiles from source (can take minutes); Ctrl+C is safe — BYAN is already installed.'));
1386
1388
  const r = setupRtkIntegration({ log: (m) => console.log(chalk.gray(' ' + m)) });
1387
1389
  if (r.synced) {
1388
1390
  console.log(chalk.green(` ✓ rtk ready (${r.installedVia}, v${r.version || '?'}) — restart Claude Code to activate`));
1391
+ if (r.pathHint) console.log(chalk.yellow(` ⚠ rtk is not on your PATH — add it: ${r.pathHint}`));
1389
1392
  } else {
1390
- console.log(chalk.yellow(` ⚠ rtk not wired (${r.reason}) — BYAN unaffected`));
1393
+ console.log(chalk.yellow(` ⚠ rtk not wired (${r.reason}) — BYAN unaffected; re-run \`npm run setup-rtk\` anytime`));
1394
+ if (r.pathHint) console.log(chalk.yellow(` ⚠ rtk found off-PATH — add it then re-run: ${r.pathHint}`));
1391
1395
  }
1392
1396
  } else {
1393
1397
  console.log(chalk.gray(' rtk skipped — run `npm run setup-rtk` anytime to enable.'));
@@ -1397,6 +1401,33 @@ async function install(options = {}) {
1397
1401
  }
1398
1402
  }
1399
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
+
1400
1431
  if (needsClaude) {
1401
1432
  console.log();
1402
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: 'Configurer l\'écran de consentement OAuth (External, mode Test)',
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
  };