create-byan-agent 2.29.2 → 2.38.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 (48) hide show
  1. package/CHANGELOG.md +361 -0
  2. package/install/bin/create-byan-agent-v2.js +44 -1
  3. package/install/lib/claude-native-setup.js +12 -38
  4. package/install/lib/gdoc-setup.js +210 -0
  5. package/install/lib/mcp-extensions/gdrive.js +27 -2
  6. package/install/lib/platforms/claude-code.js +28 -19
  7. package/install/lib/rtk-integration.js +18 -8
  8. package/install/package.json +1 -1
  9. package/install/packages/platform-config/lib/credentials.js +13 -1
  10. package/install/packages/platform-config/lib/mcp-config.js +71 -5
  11. package/install/setup-gdoc.js +41 -0
  12. package/install/setup-rtk.js +1 -1
  13. package/install/templates/.claude/CLAUDE.md +16 -4
  14. package/install/templates/.claude/hooks/inject-delivery-default.js +46 -0
  15. package/install/templates/.claude/hooks/inject-soul.js +4 -3
  16. package/install/templates/.claude/hooks/inject-tao.js +65 -25
  17. package/install/templates/.claude/hooks/inject-voice-anchor.js +102 -0
  18. package/install/templates/.claude/hooks/leantime-fd-sync.js +12 -1
  19. package/install/templates/.claude/hooks/lib/delivery-contract.js +143 -0
  20. package/install/templates/.claude/hooks/lib/punt-detect.js +143 -0
  21. package/install/templates/.claude/hooks/punt-guard.js +126 -0
  22. package/install/templates/.claude/hooks/strict-stop-guard.js +29 -6
  23. package/install/templates/.claude/rules/portable-core.md +81 -0
  24. package/install/templates/.claude/settings.json +13 -1
  25. package/install/templates/.claude/skills/byan-hermes-dispatch/SKILL.md +16 -2
  26. package/install/templates/_byan/_config/delivery-default.json +22 -0
  27. package/install/templates/_byan/mcp/byan-mcp-server/channel-entry.js +46 -0
  28. package/install/templates/_byan/mcp/byan-mcp-server/lib/channel-poll.js +128 -0
  29. package/install/templates/_byan/mcp/byan-mcp-server/lib/channel-server.js +234 -0
  30. package/install/templates/_byan/mcp/byan-mcp-server/lib/completeness-evidence.js +159 -0
  31. package/install/templates/_byan/mcp/byan-mcp-server/lib/gdoc-client.js +203 -0
  32. package/install/templates/_byan/mcp/byan-mcp-server/lib/gdoc-content.js +203 -0
  33. package/install/templates/_byan/mcp/byan-mcp-server/lib/leantime-fd-core.js +60 -2
  34. package/install/templates/_byan/mcp/byan-mcp-server/lib/leantime-sync.js +4 -1
  35. package/install/templates/_byan/mcp/byan-mcp-server/lib/precommit-gate.js +68 -2
  36. package/install/templates/_byan/mcp/byan-mcp-server/lib/resolve-config.js +15 -2
  37. package/install/templates/_byan/mcp/byan-mcp-server/lib/strict-mode.js +78 -1
  38. package/install/templates/_byan/mcp/byan-mcp-server/lib/sync-rules.js +1 -1
  39. package/install/templates/_byan/mcp/byan-mcp-server/package.json +2 -0
  40. package/install/templates/_byan/mcp/byan-mcp-server/server.js +70 -0
  41. package/install/templates/_byan/mcp/byan-mcp-server/skill-bundles-manifest.json +1 -1
  42. package/install/templates/dist/skill-bundles/byan-hermes-dispatch.zip +0 -0
  43. package/install/templates/docs/google-docs-publish.md +121 -0
  44. package/install/templates/docs/leantime-integration.md +11 -1
  45. package/node_modules/byan-platform-config/lib/credentials.js +13 -1
  46. package/node_modules/byan-platform-config/lib/mcp-config.js +71 -5
  47. package/package.json +3 -1
  48. package/install/templates/.mcp.json.tmpl +0 -8
@@ -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
  };
@@ -11,9 +11,19 @@ const path = require('path');
11
11
  const os = require('os');
12
12
  const fileUtils = require('../utils/file-utils');
13
13
  const logger = require('../utils/logger');
14
+ // Single source of truth for the byan + byan-channel entry shape: the same pure
15
+ // merge the canonical .mcp.json writer uses (READ-MERGE-WRITE, relative paths,
16
+ // no secret, byan-channel inert). installDirectMCP delegates to it instead of
17
+ // hand-building a divergent entry with an absolute path that overwrote siblings.
18
+ const { mcpConfig } = require('byan-platform-config');
14
19
 
15
20
  const PLATFORM_NAME = 'Claude Code';
16
- const MCP_SERVER_FILENAME = 'byan-mcp-server.js';
21
+ // Relative path discipline: the byan server entry args[0] is repo-relative
22
+ // (mcpConfig.MCP_SERVER_REL_PATH), NEVER an absolute path. The old
23
+ // MCP_SERVER_FILENAME ('byan-mcp-server.js') was both the wrong filename and
24
+ // joined into an absolute path; both are gone. Kept only as the existence probe
25
+ // target below is the real server file under the relative path.
26
+ const MCP_SERVER_REL_PATH = mcpConfig.MCP_SERVER_REL_PATH;
17
27
 
18
28
  /**
19
29
  * Get config path for current platform
@@ -137,41 +147,40 @@ async function installViaClaudeAgent(projectRoot, agents, config) {
137
147
  */
138
148
  async function installDirectMCP(projectRoot, agents, config) {
139
149
  const configPath = getConfigPath();
140
- const mcpServerPath = path.join(projectRoot, MCP_SERVER_FILENAME);
141
-
150
+ // Existence probe against the REAL server file at its repo-relative path
151
+ // (resolved to absolute only for the fs check). The args written into the
152
+ // config below stay relative — see mcpConfig.MCP_SERVER_REL_PATH.
153
+ const mcpServerPath = path.join(projectRoot, MCP_SERVER_REL_PATH);
154
+
142
155
  // Check if MCP server exists
143
156
  if (!await fileUtils.exists(mcpServerPath)) {
144
157
  logger.warn(`MCP server not found at: ${mcpServerPath}`);
145
158
  logger.warn('Run @bmad-agent-claude to generate MCP server first.');
146
-
159
+
147
160
  return {
148
161
  success: false,
149
162
  installed: 0,
150
163
  method: 'direct-mcp-failed'
151
164
  };
152
165
  }
153
-
166
+
154
167
  // Backup existing config
155
168
  const backupPath = `${configPath}.backup`;
156
169
  await fileUtils.copy(configPath, backupPath);
157
170
  logger.info(`Backed up config to: ${backupPath}`);
158
-
159
- // Read and update config
171
+
172
+ // READ-MERGE-WRITE through the shared pure merge so this writer is byte-for-byte
173
+ // identical to the canonical .mcp.json writer: relative server path, no secret,
174
+ // existing mcpServers.* preserved, and the inert byan-channel entry added.
175
+ // apiUrl is irrelevant to the entry shape (the server resolves its own config),
176
+ // but the merge signature accepts it; a localhost placeholder keeps it valid.
160
177
  const existingConfig = await fileUtils.readJson(configPath);
161
- existingConfig.mcpServers = existingConfig.mcpServers || {};
162
-
163
- existingConfig.mcpServers.byan = {
164
- command: 'node',
165
- args: [mcpServerPath],
166
- env: {
167
- PROJECT_ROOT: projectRoot
168
- }
169
- };
170
-
178
+ const merged = mcpConfig.mergeByanEntry(existingConfig, { apiUrl: 'http://localhost:3737' });
179
+
171
180
  // Write updated config
172
- await fileUtils.writeJson(configPath, existingConfig, { spaces: 2 });
181
+ await fileUtils.writeJson(configPath, merged, { spaces: 2 });
173
182
  logger.info(`Updated MCP config: ${configPath}`);
174
-
183
+
175
184
  return {
176
185
  success: true,
177
186
  installed: agents.length,
@@ -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`. BYAN maintains only "pick the available installer, run it
15
- * bounded + visible, locate the binary, ask rtk to wire its hook" a tiny
16
- * surface, bumped via one constant.
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(`${bin} init -g`, { stdio: 'pipe' });
201
- log(`rtk: ready (${installedVia}, v${version || '?'}) — hook wired via '${bin} init -g'. Restart Claude Code to activate.`);
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 '${bin} init -g' failed (${oneLine(err)}) — run it manually, BYAN unaffected.`);
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;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-byan-agent",
3
- "version": "2.29.2",
3
+ "version": "2.37.1",
4
4
  "description": "BYAN - Intelligent AI agent installer with multi-platform native support (Claude Code, Codex/OpenCode)",
5
5
  "bin": {
6
6
  "create-byan-agent": "bin/create-byan-agent-v2.js"
@@ -18,7 +18,19 @@ const path = require('path');
18
18
  const fs = require('fs-extra');
19
19
 
20
20
  // Keys the credentials file understands. Any other key passed in is ignored.
21
- const KNOWN_KEYS = ['BYAN_API_URL', 'BYAN_API_TOKEN', 'LEANTIME_API_URL', 'LEANTIME_API_TOKEN'];
21
+ // The Google Docs publish keys (byan_publish) live here too so a per-user
22
+ // service-account setup persists alongside the byan_web / Leantime config; the
23
+ // MCP server reads them via resolve-config. GOOGLE_APPLICATION_CREDENTIALS is a
24
+ // PATH to the SA JSON (not the key itself) -- the key file stays separate, 0600.
25
+ const KNOWN_KEYS = [
26
+ 'BYAN_API_URL',
27
+ 'BYAN_API_TOKEN',
28
+ 'LEANTIME_API_URL',
29
+ 'LEANTIME_API_TOKEN',
30
+ 'GOOGLE_APPLICATION_CREDENTIALS',
31
+ 'GDOC_TEMPLATE_ID',
32
+ 'GDOC_LOGO_PNG_URL',
33
+ ];
22
34
 
23
35
  function credentialsDir(homedir = os.homedir()) {
24
36
  return path.join(homedir, '.byan');
@@ -27,6 +27,13 @@ const path = require('path');
27
27
  const fs = require('fs-extra');
28
28
 
29
29
  const MCP_SERVER_REL_PATH = '_byan/mcp/byan-mcp-server/server.js';
30
+ // Channel entrypoint: a SEPARATE MCP server (research preview) spawned by Claude
31
+ // Code via --dangerously-load-development-channels. Same relative-path discipline
32
+ // as the main server (relative to projectRoot, never absolute) so the entry is
33
+ // portable across machines/OSes and stays valid when the repo is moved or shipped
34
+ // via npm. The entry is INERT by default: registering it in .mcp.json does NOT
35
+ // enable the channel (that needs the explicit --dangerously-load flag at launch).
36
+ const MCP_CHANNEL_REL_PATH = '_byan/mcp/byan-mcp-server/channel-entry.js';
30
37
  const TOKEN_PLACEHOLDER = '${BYAN_API_TOKEN}';
31
38
  const LEANTIME_URL_PLACEHOLDER = '${LEANTIME_API_URL}';
32
39
  const LEANTIME_TOKEN_PLACEHOLDER = '${LEANTIME_API_TOKEN}';
@@ -87,15 +94,66 @@ function mergeByanEntry(existingConfig, { apiUrl, token } = {}) {
87
94
  delete env.BYAN_API_URL;
88
95
  delete env.BYAN_API_TOKEN;
89
96
 
97
+ // The canonical RELATIVE path is forced on `args` AFTER ...existing so it always
98
+ // wins -- this REPAIRS a stale entry that carried an absolute path (a pre-2.37.x
99
+ // install): relative survives a moved / npm-shipped repo, absolute does not, and
100
+ // Claude Code spawns the server with cwd=projectRoot so the relative path resolves
101
+ // identically. `command` is only F1-orthogonal (the interpreter, not a path), so a
102
+ // user-chosen command is preserved; we default to 'node' only when absent. Other
103
+ // pre-existing keys are still preserved.
90
104
  const entry = {
91
- command: 'node',
92
- args: [MCP_SERVER_REL_PATH],
93
105
  ...existing,
106
+ command: existing.command || 'node',
107
+ args: [MCP_SERVER_REL_PATH],
94
108
  };
95
109
  if (Object.keys(env).length > 0) entry.env = env;
96
110
  else delete entry.env;
97
111
 
98
112
  cfg.mcpServers.byan = entry;
113
+
114
+ // The channel entry is written alongside byan from the SAME merge so there is
115
+ // a single source of truth for the byan MCP registration. It is inert.
116
+ return mergeChannelEntry(cfg);
117
+ }
118
+
119
+ /**
120
+ * Pure merge — no I/O. Adds the `byan-channel` MCP server entry alongside byan.
121
+ *
122
+ * This entry is a Claude Code research-preview channel (CC v2.1.80+). It is
123
+ * INERT by default: present in .mcp.json but only loaded when the user launches
124
+ * with `claude --dangerously-load-development-channels server:byan-channel`.
125
+ * Registering it does NOT auto-activate anything — this function writes NO
126
+ * channelsEnabled / allowedChannelPlugins / --dangerously-load flag.
127
+ *
128
+ * Same portability discipline as the byan entry: a RELATIVE path (never
129
+ * absolute), no secret in env (the channel resolves its own config at boot via
130
+ * resolve-config.js: env -> ~/.byan/credentials.json -> defaults). An existing
131
+ * byan-channel entry is preserved (command/args/env) so the merge is idempotent.
132
+ *
133
+ * @param {object} existingConfig — current parsed config
134
+ * @returns {object} new merged config
135
+ */
136
+ function mergeChannelEntry(existingConfig) {
137
+ const cfg = existingConfig && typeof existingConfig === 'object' ? { ...existingConfig } : {};
138
+ cfg.mcpServers = { ...(cfg.mcpServers || {}) };
139
+
140
+ const existing = cfg.mcpServers['byan-channel'] || {};
141
+
142
+ // The canonical RELATIVE path + the empty (secret-free) env are forced AFTER
143
+ // ...existing so they always win, normalizing a stale entry (absolute path, or
144
+ // a stray env) while preserving any other pre-existing key. env stays empty: the
145
+ // channel resolves BYAN_API_URL/TOKEN itself via resolve-config.js -- writing a
146
+ // secret here would land it in tracked git, and the channel has no legitimate
147
+ // env contract of its own (Leantime refs live on the byan entry). `command` is
148
+ // F1-orthogonal so a user-chosen interpreter is preserved, 'node' only as the
149
+ // default. Idempotent: re-running yields the same entry.
150
+ cfg.mcpServers['byan-channel'] = {
151
+ ...existing,
152
+ command: existing.command || 'node',
153
+ args: [MCP_CHANNEL_REL_PATH],
154
+ env: {},
155
+ };
156
+
99
157
  return cfg;
100
158
  }
101
159
 
@@ -141,14 +199,20 @@ function mergeLeantimeRefs(existingConfig) {
141
199
  env.LEANTIME_API_URL = LEANTIME_URL_PLACEHOLDER;
142
200
  env.LEANTIME_API_TOKEN = LEANTIME_TOKEN_PLACEHOLDER;
143
201
 
202
+ // Same discipline as mergeByanEntry: args forced to the canonical RELATIVE path
203
+ // (normalizes a stale absolute one), command preserved (F1-orthogonal), env
204
+ // carries the merged Leantime refs.
144
205
  cfg.mcpServers.byan = {
145
- command: 'node',
146
- args: [MCP_SERVER_REL_PATH],
147
206
  ...existing,
207
+ command: existing.command || 'node',
208
+ args: [MCP_SERVER_REL_PATH],
148
209
  env,
149
210
  };
150
211
 
151
- return cfg;
212
+ // Keep the channel entry coherent with the byan entry: ensureLeantimeRefs runs
213
+ // after ensureMcpConfig in the installer, but routing through mergeChannelEntry
214
+ // here makes the byan-channel registration robust to call order (idempotent).
215
+ return mergeChannelEntry(cfg);
152
216
  }
153
217
 
154
218
  /**
@@ -252,12 +316,14 @@ module.exports = {
252
316
  ensureMcpConfig,
253
317
  readMcpConfig,
254
318
  mergeByanEntry,
319
+ mergeChannelEntry,
255
320
  mergeLeantimeRefs,
256
321
  ensureLeantimeRefs,
257
322
  addMcpEntry,
258
323
  removeMcpEntry,
259
324
  looksLikeSecret,
260
325
  MCP_SERVER_REL_PATH,
326
+ MCP_CHANNEL_REL_PATH,
261
327
  TOKEN_PLACEHOLDER,
262
328
  LEANTIME_URL_PLACEHOLDER,
263
329
  LEANTIME_TOKEN_PLACEHOLDER,
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * setup-gdoc -- explicit opt-in setup for the byan_publish service-account key.
6
+ *
7
+ * Run on demand: `npm run setup-gdoc` (or `node install/setup-gdoc.js`). Walks
8
+ * the user through creating a Google service account + key, imports the JSON to
9
+ * ~/.byan/google-sa.json (0600), and persists the publish config into
10
+ * ~/.byan/credentials.json. It NEVER throws and exits 0 : the SA setup is
11
+ * optional and must not fail the surrounding flow. The real logic lives in
12
+ * lib/gdoc-setup.js and is unit-tested.
13
+ */
14
+
15
+ const chalk = require('chalk');
16
+ const { setupGdocPublish } = require('./lib/gdoc-setup');
17
+
18
+ async function main() {
19
+ console.log(chalk.cyan('\nbyan_publish -- clé service account (Google Docs headless)'));
20
+ console.log(chalk.gray(' Open source : tu fournis ta clé ; rien de secret ne ship.\n'));
21
+
22
+ const result = await setupGdocPublish({ log: (...a) => console.log(...a) });
23
+
24
+ if (result.configured) {
25
+ console.log(chalk.green(`\n Configuré. Clé : ${result.path}`));
26
+ console.log(chalk.gray(' Installe aussi googleapis dans le MCP server : (cd _byan/mcp/byan-mcp-server && npm install googleapis google-auth-library)'));
27
+ } else {
28
+ console.log(chalk.yellow(`\n Non configuré (${result.skipReason || 'ignoré'}). BYAN reste fonctionnel.`));
29
+ }
30
+ process.exit(0);
31
+ }
32
+
33
+ if (require.main === module) {
34
+ main().catch((err) => {
35
+ // Last-resort guard : an optional setup must not crash with a non-zero exit.
36
+ console.log(chalk.yellow(` setup-gdoc ignoré : ${err.message}`));
37
+ process.exit(0);
38
+ });
39
+ }
40
+
41
+ module.exports = { main };
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * Run on demand: `npm run setup-rtk` (or `node install/setup-rtk.js`). It delegates
8
8
  * the install to rtk's own canonical installer (brew / cargo / official script) and
9
- * the Claude Code hook wiring to rtk's own `rtk init -g`. It NEVER throws: a missing
9
+ * the Claude Code hook wiring to rtk's own `rtk init -g --auto-patch`. It NEVER throws: a missing
10
10
  * installer or a failed step is reported and exits 0 (RTK is optional; BYAN works
11
11
  * without it). The real logic lives in lib/rtk-integration.js and is unit-tested.
12
12
  */
@@ -62,8 +62,9 @@ Doctrine d'equipe complete (template role-in-team, analogie orchestre, principes
62
62
  - Doctrine d'equipe: voir @.claude/rules/team-doctrine.md
63
63
  - Methodologie: voir @.claude/rules/merise-agile.md
64
64
  - Systeme de confiance epistemique: voir @.claude/rules/elo-trust.md
65
- - Protocol fact-check scientifique: voir @.claude/rules/fact-check.md
66
- - Mode strict anti-downgrade: voir @.claude/rules/strict-mode.md
65
+ - Protocol fact-check scientifique: voir .claude/rules/fact-check.md (charge a la demande via le skill byan-fact-check)
66
+ - Mode strict anti-downgrade: voir .claude/rules/strict-mode.md (charge a la demande via le skill byan-strict)
67
+ - Architecture portable (noyau portable, projection native): voir .claude/rules/portable-core.md (charge a la demande)
67
68
  - Systeme API byan_web: voir @.claude/rules/byan-api.md
68
69
 
69
70
  ## API byan_web
@@ -109,7 +110,7 @@ Protocole : lock du scope -> build complet -> self-verify >= 3 passes -> complet
109
110
  - Filet final : `.githooks/pre-commit` bloque le commit si une session strict est engagee mais non completee
110
111
  - Persistance : sessions poussees vers l'API byan_web (autorite ; local = miroir/fallback offline) via `lib/strict-sync.js` ; migration `033` + `routes/strict-sessions.js` cote byan_web
111
112
 
112
- Detail complet : voir @.claude/rules/strict-mode.md
113
+ Detail complet (hors contexte par defaut, charge a la demande via le skill byan-strict) : voir .claude/rules/strict-mode.md
113
114
 
114
115
  <!-- BYAN-AUTOBENCH:BEGIN (Generated by byan-sync-rules from _byan/_config/autobench.yaml. Do not hand-edit.) -->
115
116
  ## BYAN Auto-Benchmark
@@ -120,5 +121,16 @@ both gates hold (>= 2 non-substitutable options diverging on >= 1 weighted
120
121
  criterion). Emit the marker verbatim before the table:
121
122
  `<!-- BYAN-BENCH:done g1=<#options> g2=<#divergent-criteria> scope=<internal|external> conf=<assertive|lean> -->`.
122
123
  A confirm, a destructive prompt, or an obvious default is not a fork — emit
123
- `<!-- BYAN-BENCH:skip reason=.. -->` instead. Full doctrine: see @.claude/rules/benchmark.md
124
+ `<!-- BYAN-BENCH:skip reason=.. -->` instead. Full doctrine (loaded on demand): see .claude/rules/benchmark.md
124
125
  <!-- BYAN-AUTOBENCH:END -->
126
+
127
+ ## Compact instructions
128
+
129
+ Quand tu compactes cette conversation, PRESERVE en priorite :
130
+ - le contrat de livraison par defaut (F1) : grade=PROD, scope=MAXIMAL, etalon de cout=AI-2026 (temps-agent x10, jamais temps-humain-a-la-main). Pas de proposition d'MVP/livrable-court/decoupage-pour-ne-pas-bloquer-le-lourd sauf opt-out explicite du message courant. Source : `_byan/_config/delivery-default.json` ; reinjecte chaque tour par `inject-delivery-default`.
131
+ - 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`).
132
+ - 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).
133
+ - 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.
134
+ - les derniers commits et tout travail non committe en cours.
135
+
136
+ 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).