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.
- package/CHANGELOG.md +361 -0
- package/install/bin/create-byan-agent-v2.js +44 -1
- package/install/lib/claude-native-setup.js +12 -38
- package/install/lib/gdoc-setup.js +210 -0
- package/install/lib/mcp-extensions/gdrive.js +27 -2
- package/install/lib/platforms/claude-code.js +28 -19
- 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/packages/platform-config/lib/mcp-config.js +71 -5
- package/install/setup-gdoc.js +41 -0
- package/install/setup-rtk.js +1 -1
- package/install/templates/.claude/CLAUDE.md +16 -4
- package/install/templates/.claude/hooks/inject-delivery-default.js +46 -0
- 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/hooks/leantime-fd-sync.js +12 -1
- package/install/templates/.claude/hooks/lib/delivery-contract.js +143 -0
- package/install/templates/.claude/hooks/lib/punt-detect.js +143 -0
- package/install/templates/.claude/hooks/punt-guard.js +126 -0
- package/install/templates/.claude/hooks/strict-stop-guard.js +29 -6
- package/install/templates/.claude/rules/portable-core.md +81 -0
- package/install/templates/.claude/settings.json +13 -1
- package/install/templates/.claude/skills/byan-hermes-dispatch/SKILL.md +16 -2
- package/install/templates/_byan/_config/delivery-default.json +22 -0
- package/install/templates/_byan/mcp/byan-mcp-server/channel-entry.js +46 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/channel-poll.js +128 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/channel-server.js +234 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/completeness-evidence.js +159 -0
- 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/leantime-fd-core.js +60 -2
- package/install/templates/_byan/mcp/byan-mcp-server/lib/leantime-sync.js +4 -1
- package/install/templates/_byan/mcp/byan-mcp-server/lib/precommit-gate.js +68 -2
- package/install/templates/_byan/mcp/byan-mcp-server/lib/resolve-config.js +15 -2
- package/install/templates/_byan/mcp/byan-mcp-server/lib/strict-mode.js +78 -1
- 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/install/templates/docs/leantime-integration.md +11 -1
- package/node_modules/byan-platform-config/lib/credentials.js +13 -1
- package/node_modules/byan-platform-config/lib/mcp-config.js +71 -5
- package/package.json +3 -1
- 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: '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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,
|
|
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
|
|
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');
|
|
@@ -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
|
-
|
|
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 };
|
package/install/setup-rtk.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Run on demand: `npm run setup-rtk` (or `node install/setup-rtk.js`). It delegates
|
|
8
8
|
* the install to rtk's own canonical installer (brew / cargo / official script) and
|
|
9
|
-
* the Claude Code hook wiring to rtk's own `rtk init -g`. It NEVER throws: a missing
|
|
9
|
+
* the Claude Code hook wiring to rtk's own `rtk init -g --auto-patch`. It NEVER throws: a missing
|
|
10
10
|
* installer or a failed step is reported and exits 0 (RTK is optional; BYAN works
|
|
11
11
|
* without it). The real logic lives in lib/rtk-integration.js and is unit-tested.
|
|
12
12
|
*/
|
|
@@ -62,8 +62,9 @@ Doctrine d'equipe complete (template role-in-team, analogie orchestre, principes
|
|
|
62
62
|
- Doctrine d'equipe: voir @.claude/rules/team-doctrine.md
|
|
63
63
|
- Methodologie: voir @.claude/rules/merise-agile.md
|
|
64
64
|
- Systeme de confiance epistemique: voir @.claude/rules/elo-trust.md
|
|
65
|
-
- Protocol fact-check scientifique: voir
|
|
66
|
-
- Mode strict anti-downgrade: voir
|
|
65
|
+
- Protocol fact-check scientifique: voir .claude/rules/fact-check.md (charge a la demande via le skill byan-fact-check)
|
|
66
|
+
- Mode strict anti-downgrade: voir .claude/rules/strict-mode.md (charge a la demande via le skill byan-strict)
|
|
67
|
+
- Architecture portable (noyau portable, projection native): voir .claude/rules/portable-core.md (charge a la demande)
|
|
67
68
|
- Systeme API byan_web: voir @.claude/rules/byan-api.md
|
|
68
69
|
|
|
69
70
|
## API byan_web
|
|
@@ -109,7 +110,7 @@ Protocole : lock du scope -> build complet -> self-verify >= 3 passes -> complet
|
|
|
109
110
|
- Filet final : `.githooks/pre-commit` bloque le commit si une session strict est engagee mais non completee
|
|
110
111
|
- Persistance : sessions poussees vers l'API byan_web (autorite ; local = miroir/fallback offline) via `lib/strict-sync.js` ; migration `033` + `routes/strict-sessions.js` cote byan_web
|
|
111
112
|
|
|
112
|
-
Detail complet : voir
|
|
113
|
+
Detail complet (hors contexte par defaut, charge a la demande via le skill byan-strict) : voir .claude/rules/strict-mode.md
|
|
113
114
|
|
|
114
115
|
<!-- BYAN-AUTOBENCH:BEGIN (Generated by byan-sync-rules from _byan/_config/autobench.yaml. Do not hand-edit.) -->
|
|
115
116
|
## BYAN Auto-Benchmark
|
|
@@ -120,5 +121,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
|
|
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).
|