create-byan-agent 2.29.2 → 2.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/CHANGELOG.md +222 -0
  2. package/install/bin/create-byan-agent-v2.js +29 -1
  3. package/install/lib/gdoc-setup.js +210 -0
  4. package/install/lib/mcp-extensions/gdrive.js +27 -2
  5. package/install/lib/rtk-integration.js +18 -8
  6. package/install/package.json +1 -1
  7. package/install/packages/platform-config/lib/credentials.js +13 -1
  8. package/install/setup-gdoc.js +41 -0
  9. package/install/setup-rtk.js +1 -1
  10. package/install/templates/.claude/CLAUDE.md +15 -4
  11. package/install/templates/.claude/hooks/inject-soul.js +4 -3
  12. package/install/templates/.claude/hooks/inject-tao.js +65 -25
  13. package/install/templates/.claude/hooks/inject-voice-anchor.js +102 -0
  14. package/install/templates/.claude/rules/portable-core.md +81 -0
  15. package/install/templates/.claude/settings.json +5 -1
  16. package/install/templates/.claude/skills/byan-hermes-dispatch/SKILL.md +16 -2
  17. package/install/templates/_byan/mcp/byan-mcp-server/lib/gdoc-client.js +203 -0
  18. package/install/templates/_byan/mcp/byan-mcp-server/lib/gdoc-content.js +203 -0
  19. package/install/templates/_byan/mcp/byan-mcp-server/lib/resolve-config.js +15 -2
  20. package/install/templates/_byan/mcp/byan-mcp-server/lib/sync-rules.js +1 -1
  21. package/install/templates/_byan/mcp/byan-mcp-server/package.json +2 -0
  22. package/install/templates/_byan/mcp/byan-mcp-server/server.js +70 -0
  23. package/install/templates/_byan/mcp/byan-mcp-server/skill-bundles-manifest.json +1 -1
  24. package/install/templates/dist/skill-bundles/byan-hermes-dispatch.zip +0 -0
  25. package/install/templates/docs/google-docs-publish.md +121 -0
  26. package/node_modules/byan-platform-config/lib/credentials.js +13 -1
  27. package/package.json +3 -1
@@ -0,0 +1,203 @@
1
+ // gdoc-content -- PURE content -> Google Docs API request builders.
2
+ //
3
+ // No network, no googleapis, no I/O : this module turns a plain content object
4
+ // into the deterministic arrays of Docs `batchUpdate` requests that gdoc-client
5
+ // sends. Kept pure so the whole shaping logic is unit-tested without a service
6
+ // account or a network (see test/gdoc-content.test.js).
7
+ //
8
+ // Two modes, mirroring gdoc-client's two paths :
9
+ // - TEMPLATE mode : buildReplaceRequests() -> replaceAllText over a branded
10
+ // template Doc (logo + palette live IN the template ; we only fill text).
11
+ // - NO-TEMPLATE : buildDocumentRequests() -> insert a structured text doc
12
+ // and colour the title + headings from the AcadeNice palette. A logo is
13
+ // inserted at the top when a PNG URL is provided (opts.logoPngUrl /
14
+ // GDOC_LOGO_PNG_URL) ; Docs accepts a raster URL (PNG/JPG/GIF), not SVG, so
15
+ // the URL must point at a PNG. Absent -> the doc is text branded by colour
16
+ // only (graceful, no image request).
17
+
18
+ // AcadeNice palette (from the brand charter). Hex here ; converted to the Docs
19
+ // API rgbColor (0..1 floats) by hexToRgbColor.
20
+ const BRANDING = {
21
+ marine: '#0e2656',
22
+ teal: '#24947a',
23
+ turquoise: '#4cccb8',
24
+ };
25
+
26
+ /**
27
+ * hexToRgbColor('#0e2656') -> { red, green, blue } each in [0,1], the shape the
28
+ * Docs API expects under textStyle.foregroundColor.color.rgbColor. Accepts an
29
+ * optional leading '#' and 3- or 6-digit hex. Throws on malformed input (a bad
30
+ * palette constant is a programming error, surfaced loudly in tests).
31
+ * @param {string} hex
32
+ */
33
+ function hexToRgbColor(hex) {
34
+ if (typeof hex !== 'string') throw new TypeError('hex must be a string');
35
+ let h = hex.trim().replace(/^#/, '');
36
+ if (h.length === 3) h = h.split('').map((c) => c + c).join('');
37
+ if (!/^[0-9a-fA-F]{6}$/.test(h)) throw new Error(`invalid hex colour: ${hex}`);
38
+ const n = parseInt(h, 16);
39
+ return {
40
+ red: ((n >> 16) & 255) / 255,
41
+ green: ((n >> 8) & 255) / 255,
42
+ blue: (n & 255) / 255,
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Normalize + validate the content object. Returns { title, sections, resources,
48
+ * fields }. Throws a clear error when the minimum (a non-empty title) is missing
49
+ * -- the caller turns that into a tool error, never a crash.
50
+ * @param {object} content
51
+ */
52
+ function normalizeContent(content) {
53
+ const c = content || {};
54
+ const title = typeof c.title === 'string' ? c.title.trim() : '';
55
+ if (!title) throw new Error('content.title is required (non-empty string)');
56
+ const sections = Array.isArray(c.sections)
57
+ ? c.sections
58
+ .map((s) => ({
59
+ heading: typeof s?.heading === 'string' ? s.heading.trim() : '',
60
+ body: typeof s?.body === 'string' ? s.body : '',
61
+ }))
62
+ .filter((s) => s.heading || s.body)
63
+ : [];
64
+ const resources = Array.isArray(c.resources)
65
+ ? c.resources
66
+ .map((r) => ({
67
+ label: typeof r?.label === 'string' ? r.label.trim() : '',
68
+ url: typeof r?.url === 'string' ? r.url.trim() : '',
69
+ }))
70
+ .filter((r) => r.label || r.url)
71
+ : [];
72
+ const fields = c.fields && typeof c.fields === 'object' ? c.fields : {};
73
+ return { title, sections, resources, fields };
74
+ }
75
+
76
+ /**
77
+ * TEMPLATE mode. Map the content onto {{PLACEHOLDER}} tokens and emit one
78
+ * replaceAllText request per token. The template Doc carries the branding ; we
79
+ * only substitute text. `fields` lets a caller fill arbitrary {{KEY}} tokens
80
+ * (e.g. {{CANDIDAT}}). Deterministic order (stable for tests).
81
+ * @param {object} content
82
+ */
83
+ function buildReplaceRequests(content) {
84
+ const { title, sections, resources, fields } = normalizeContent(content);
85
+ const map = {
86
+ TITLE: title,
87
+ BLOCS: sections.map((s) => `${s.heading}\n${s.body}`.trim()).join('\n\n'),
88
+ RESSOURCES: resources.map((r) => (r.url ? `${r.label} : ${r.url}` : r.label)).join('\n'),
89
+ };
90
+ for (const [k, v] of Object.entries(fields)) {
91
+ if (v != null) map[String(k).toUpperCase()] = String(v);
92
+ }
93
+ return Object.entries(map).map(([key, value]) => ({
94
+ replaceAllText: {
95
+ containsText: { text: `{{${key}}}`, matchCase: true },
96
+ replaceText: value,
97
+ },
98
+ }));
99
+ }
100
+
101
+ /**
102
+ * Assemble the flat document text + the index ranges to colour, for NO-TEMPLATE
103
+ * mode. Docs is 1-indexed (index 1 = first insertable position), so every range
104
+ * is offset by +1 against the assembled string. Returns { text, titleRange,
105
+ * headingRanges } with ranges as { startIndex, endIndex } in Docs coordinates.
106
+ * @param {object} content
107
+ */
108
+ function assembleDocument(content) {
109
+ const { title, sections, resources } = normalizeContent(content);
110
+ const headingRanges = [];
111
+ let text = '';
112
+ const DOC_START = 1; // Docs body first insertable index
113
+
114
+ const titleStart = DOC_START + text.length;
115
+ text += `${title}\n\n`;
116
+ const titleRange = { startIndex: titleStart, endIndex: titleStart + title.length };
117
+
118
+ for (const s of sections) {
119
+ if (s.heading) {
120
+ const hs = DOC_START + text.length;
121
+ text += `${s.heading}\n`;
122
+ headingRanges.push({ startIndex: hs, endIndex: hs + s.heading.length });
123
+ }
124
+ if (s.body) text += `${s.body}\n`;
125
+ text += '\n';
126
+ }
127
+
128
+ if (resources.length) {
129
+ const rh = 'Ressources';
130
+ const rhs = DOC_START + text.length;
131
+ text += `${rh}\n`;
132
+ headingRanges.push({ startIndex: rhs, endIndex: rhs + rh.length });
133
+ for (const r of resources) {
134
+ text += `${r.url ? `${r.label} : ${r.url}` : r.label}\n`;
135
+ }
136
+ }
137
+
138
+ return { text, titleRange, headingRanges };
139
+ }
140
+
141
+ /**
142
+ * NO-TEMPLATE mode. Insert the assembled text at index 1, then colour the title
143
+ * (marine, bold, larger) and each heading (teal, bold) from the palette. Indices
144
+ * are valid because the single insertText happens first and every style range is
145
+ * computed against that one block. Returns the batchUpdate requests array.
146
+ * @param {object} content
147
+ * @param {object} [opts]
148
+ * @param {object} [opts.branding=BRANDING]
149
+ * @param {string} [opts.logoPngUrl] PNG URL inserted at the top ; absent -> skipped
150
+ */
151
+ function buildDocumentRequests(content, opts = {}) {
152
+ const branding = opts.branding || BRANDING;
153
+ const { text, titleRange, headingRanges } = assembleDocument(content);
154
+
155
+ const requests = [{ insertText: { location: { index: 1 }, text } }];
156
+
157
+ requests.push({
158
+ updateTextStyle: {
159
+ range: titleRange,
160
+ textStyle: {
161
+ bold: true,
162
+ fontSize: { magnitude: 20, unit: 'PT' },
163
+ foregroundColor: { color: { rgbColor: hexToRgbColor(branding.marine) } },
164
+ },
165
+ fields: 'bold,fontSize,foregroundColor',
166
+ },
167
+ });
168
+
169
+ for (const range of headingRanges) {
170
+ requests.push({
171
+ updateTextStyle: {
172
+ range,
173
+ textStyle: {
174
+ bold: true,
175
+ fontSize: { magnitude: 14, unit: 'PT' },
176
+ foregroundColor: { color: { rgbColor: hexToRgbColor(branding.teal) } },
177
+ },
178
+ fields: 'bold,fontSize,foregroundColor',
179
+ },
180
+ });
181
+ }
182
+
183
+ // Logo last : inserted at index 1 AFTER text+styles, so it lands at the very
184
+ // top and the already-styled text simply shifts down (styles stay bound to the
185
+ // characters). Skipped when no PNG URL is configured (graceful). Docs needs a
186
+ // raster URL (PNG/JPG/GIF) -- a non-PNG/SVG URL is rejected by the API and
187
+ // surfaces as gdoc-client's api-error, not a crash here.
188
+ const logoPngUrl = typeof opts.logoPngUrl === 'string' ? opts.logoPngUrl.trim() : '';
189
+ if (logoPngUrl) {
190
+ requests.push({ insertInlineImage: { location: { index: 1 }, uri: logoPngUrl } });
191
+ }
192
+
193
+ return requests;
194
+ }
195
+
196
+ export {
197
+ BRANDING,
198
+ hexToRgbColor,
199
+ normalizeContent,
200
+ buildReplaceRequests,
201
+ assembleDocument,
202
+ buildDocumentRequests,
203
+ };
@@ -26,8 +26,18 @@ import nodePath from 'node:path';
26
26
  const DEFAULT_API_URL = 'http://localhost:3737';
27
27
 
28
28
  // Keys the resolver understands. BYAN_API_URL is the only one with a non-empty
29
- // default; tokens and the optional Leantime URL stay empty when unset.
30
- const KEYS = ['BYAN_API_URL', 'BYAN_API_TOKEN', 'LEANTIME_API_URL', 'LEANTIME_API_TOKEN'];
29
+ // default; tokens, the optional Leantime URL, and the optional Google Docs
30
+ // publish config (service-account key path, template id, logo URL) stay empty
31
+ // when unset.
32
+ const KEYS = [
33
+ 'BYAN_API_URL',
34
+ 'BYAN_API_TOKEN',
35
+ 'LEANTIME_API_URL',
36
+ 'LEANTIME_API_TOKEN',
37
+ 'GOOGLE_APPLICATION_CREDENTIALS',
38
+ 'GDOC_TEMPLATE_ID',
39
+ 'GDOC_LOGO_PNG_URL',
40
+ ];
31
41
 
32
42
  /**
33
43
  * An unexpanded `${...}` placeholder is NOT a real value. It is what a launcher
@@ -98,6 +108,9 @@ function resolveConfig(opts = {}) {
98
108
  out.BYAN_API_TOKEN = out.BYAN_API_TOKEN || '';
99
109
  out.LEANTIME_API_URL = out.LEANTIME_API_URL || '';
100
110
  out.LEANTIME_API_TOKEN = out.LEANTIME_API_TOKEN || '';
111
+ out.GOOGLE_APPLICATION_CREDENTIALS = out.GOOGLE_APPLICATION_CREDENTIALS || '';
112
+ out.GDOC_TEMPLATE_ID = out.GDOC_TEMPLATE_ID || '';
113
+ out.GDOC_LOGO_PNG_URL = out.GDOC_LOGO_PNG_URL || '';
101
114
  return out;
102
115
  }
103
116
 
@@ -271,7 +271,7 @@ both gates hold (>= 2 non-substitutable options diverging on >= 1 weighted
271
271
  criterion). Emit the marker verbatim before the table:
272
272
  \`<!-- BYAN-BENCH:done g1=<#options> g2=<#divergent-criteria> scope=<internal|external> conf=<assertive|lean> -->\`.
273
273
  A confirm, a destructive prompt, or an obvious default is not a fork — emit
274
- \`<!-- BYAN-BENCH:skip reason=.. -->\` instead. Full doctrine: see @.claude/rules/benchmark.md`;
274
+ \`<!-- BYAN-BENCH:skip reason=.. -->\` instead. Full doctrine (loaded on demand): see .claude/rules/benchmark.md`;
275
275
  }
276
276
 
277
277
  // ---------------------------------------------------------------------------
@@ -14,6 +14,8 @@
14
14
  },
15
15
  "dependencies": {
16
16
  "@modelcontextprotocol/sdk": "^1.29.0",
17
+ "google-auth-library": "^9.15.1",
18
+ "googleapis": "^148.0.0",
17
19
  "js-yaml": "^4.1.1"
18
20
  },
19
21
  "engines": {
@@ -14,6 +14,7 @@ import { harvest as harvestInsights, renderDigest as renderInsightDigest } from
14
14
  import { appendOutcome } from './lib/outcome-buffer.js';
15
15
  import { validateForLog, eloOutcomeForStrictComplete } from './lib/advisory-autofeed.js';
16
16
  import { readSoul, appendSoulMemory } from './lib/soul.js';
17
+ import { createPublisher as createGdocPublisher } from './lib/gdoc-client.js';
17
18
  import {
18
19
  start as fdStart,
19
20
  status as fdStatus,
@@ -1353,6 +1354,56 @@ const tools = [
1353
1354
  additionalProperties: false,
1354
1355
  },
1355
1356
  },
1357
+
1358
+ // ─── Google Docs publish (service account, headless) ──────────────────
1359
+ // byan-owned, durable publishing : a service-account JWT (no OAuth, no 7-day
1360
+ // expiry) creates a branded Google Doc and returns its URL. Branding via a
1361
+ // template (GDOC_TEMPLATE_ID) or the AcadeNice palette programmatically.
1362
+ {
1363
+ name: 'byan_publish',
1364
+ description:
1365
+ 'Publish a branded Google Doc from content via a byan-owned SERVICE ACCOUNT (headless, durable, no OAuth). Copies a branded template when GDOC_TEMPLATE_ID is set (logo+palette in the template), else builds a programmatic doc branded by the AcadeNice palette. Returns the Doc URL; optionally shares it. Needs GOOGLE_APPLICATION_CREDENTIALS (SA key path). Degrades gracefully (ok:false + reason) when unconfigured. NOT remote-safe (network+auth).',
1366
+ inputSchema: {
1367
+ type: 'object',
1368
+ properties: {
1369
+ title: { type: 'string', description: 'Document title (required).' },
1370
+ sections: {
1371
+ type: 'array',
1372
+ description: 'Ordered sections of the document.',
1373
+ items: {
1374
+ type: 'object',
1375
+ properties: {
1376
+ heading: { type: 'string' },
1377
+ body: { type: 'string' },
1378
+ },
1379
+ additionalProperties: false,
1380
+ },
1381
+ },
1382
+ resources: {
1383
+ type: 'array',
1384
+ description: 'Resource links rendered at the end.',
1385
+ items: {
1386
+ type: 'object',
1387
+ properties: { label: { type: 'string' }, url: { type: 'string' } },
1388
+ additionalProperties: false,
1389
+ },
1390
+ },
1391
+ fields: {
1392
+ type: 'object',
1393
+ description: 'Extra {{KEY}} placeholder values for template mode.',
1394
+ },
1395
+ templateId: { type: 'string', description: 'Override GDOC_TEMPLATE_ID for this call.' },
1396
+ shareWith: { type: 'string', description: 'Email address to share the Doc with.' },
1397
+ role: {
1398
+ type: 'string',
1399
+ enum: ['reader', 'commenter', 'writer'],
1400
+ description: 'Share role (default reader).',
1401
+ },
1402
+ },
1403
+ required: ['title'],
1404
+ additionalProperties: false,
1405
+ },
1406
+ },
1356
1407
  ];
1357
1408
 
1358
1409
  // Remote-safe MVP allowlist: the ONLY tools exposed on the remote Org Connector
@@ -2092,6 +2143,25 @@ export function createByanServer({ token, remoteOnly = false } = {}) {
2092
2143
  return { content: [{ type: 'text', text: JSON.stringify(instructions, null, 2) }] };
2093
2144
  }
2094
2145
 
2146
+ // ─── Google Docs publish (service account, headless) ──────────────
2147
+ if (name === 'byan_publish') {
2148
+ const publisher = createGdocPublisher();
2149
+ const result = await publisher.publish(
2150
+ {
2151
+ title: args.title,
2152
+ sections: args.sections,
2153
+ resources: args.resources,
2154
+ fields: args.fields,
2155
+ },
2156
+ {
2157
+ templateId: args.templateId,
2158
+ shareWith: args.shareWith,
2159
+ role: args.role,
2160
+ }
2161
+ );
2162
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
2163
+ }
2164
+
2095
2165
  // ─── Leantime tools ───────────────────────────────────────────────
2096
2166
  if (name === 'byan_leantime_ping') {
2097
2167
  const status = {
@@ -184,7 +184,7 @@
184
184
  "name": "byan-hermes-dispatch",
185
185
  "module": "core",
186
186
  "tier": "connector-bound",
187
- "sourceHash": "62b9c959c77468c0e14e3dfd45039151d29b1bcdc18b04b4d0deac606052a976"
187
+ "sourceHash": "45003e8a525a792d1a5dbe34ecb8e1443061fa5036a81a38a841598ce5aaf5ff"
188
188
  },
189
189
  "byan-insight": {
190
190
  "name": "byan-insight",
@@ -0,0 +1,121 @@
1
+ # byan_publish -- Google Docs brandés, headless (service account)
2
+
3
+ Le tool MCP `byan_publish` crée un Google Doc brandé depuis un objet de contenu,
4
+ via un **service account que tu fournis** (modèle open-source, voir plus bas) :
5
+ auth JWT durable, sans navigateur, sans refresh token, sans expiration 7 jours.
6
+ C'est le chemin headless (cron, batch, sans humain) -- distinct du connecteur
7
+ OAuth `gdrive`/gw.
8
+
9
+ ## Modèle open-source : chacun fournit SA clé
10
+
11
+ BYAN est open-source : **aucune clé ne ship** dans le paquet npm. Chaque
12
+ utilisateur fournit SA propre clé service account, sur sa machine. La clé vit
13
+ dans `~/.byan/google-sa.json` (hors dépôt, 0600) et son chemin est persisté dans
14
+ `~/.byan/credentials.json` (`GOOGLE_APPLICATION_CREDENTIALS`) ; le MCP server la
15
+ lit via resolve-config.
16
+
17
+ ## Le setup (à l'install ou à la demande)
18
+
19
+ L'installeur byan **propose** ce setup (opt-in, défaut non) et il existe aussi en
20
+ commande dédiée :
21
+
22
+ ```bash
23
+ npm run setup-gdoc
24
+ ```
25
+
26
+ Il te guide vers la console Google, importe le JSON téléchargé dans
27
+ `~/.byan/google-sa.json` (0600), et persiste le chemin (+ `GDOC_TEMPLATE_ID` /
28
+ `GDOC_LOGO_PNG_URL` optionnels). Tu peux aussi tout faire à la main (poser le
29
+ fichier + éditer `~/.byan/credentials.json`).
30
+
31
+ ## La seule étape manuelle : créer la clé service account
32
+
33
+ byan ne peut pas créer cette clé à ta place (Google Cloud IAM est hors de portée
34
+ de ses outils). ~2 minutes, une fois :
35
+
36
+ 1. Projet Google Cloud (idéalement l'org acadenice.fr) :
37
+ `https://console.cloud.google.com/projectcreate`
38
+ 2. Activer les APIs **Google Docs** et **Google Drive** :
39
+ `https://console.cloud.google.com/apis/library`
40
+ 3. Créer un **service account** :
41
+ `https://console.cloud.google.com/iam-admin/serviceaccounts`
42
+ 4. Sur ce service account -> onglet **Keys** -> *Add key* -> *Create new key* ->
43
+ **JSON** -> télécharger le fichier.
44
+ 5. Déposer le JSON sur la machine et pointer `GOOGLE_APPLICATION_CREDENTIALS`
45
+ dessus, soit en variable d'environnement, soit dans `~/.byan/credentials.json` :
46
+
47
+ ```json
48
+ { "GOOGLE_APPLICATION_CREDENTIALS": "/home/<user>/.byan/google-sa.json" }
49
+ ```
50
+
51
+ La clé reste hors du dépôt (env ou `~/.byan/`), donc pas commitée.
52
+
53
+ ## Utilisation
54
+
55
+ ```jsonc
56
+ // byan_publish
57
+ {
58
+ "title": "Bilan d'évaluation - Alice",
59
+ "sections": [
60
+ { "heading": "Bloc 1 - Analyse", "body": "Acquis. ..." },
61
+ { "heading": "Bloc 2 - Conception", "body": "En cours. ..." }
62
+ ],
63
+ "resources": [
64
+ { "label": "Référentiel", "url": "https://..." }
65
+ ],
66
+ "shareWith": "alice@example.org", // optionnel
67
+ "role": "reader" // reader | commenter | writer
68
+ }
69
+ ```
70
+
71
+ Retour en cas de succès : `{ ok: true, documentId, url, mode, shared }`.
72
+ En cas de souci, le tool dégrade en `{ ok: false, reason, message }` plutôt que
73
+ de planter :
74
+
75
+ | reason | sens |
76
+ |--------|------|
77
+ | `no-credentials` | `GOOGLE_APPLICATION_CREDENTIALS` non défini |
78
+ | `bad-credentials` | fichier introuvable ou JSON invalide (client_email + private_key requis) |
79
+ | `dep-missing` | `googleapis` non installé -> `npm install googleapis google-auth-library` |
80
+ | `invalid-content` | `title` manquant |
81
+ | `api-error` | erreur renvoyée par l'API Google (message en clair) |
82
+
83
+ ## Branding : programmatique (défaut) ou template
84
+
85
+ - **Programmatique** (défaut, sans config) : doc construit depuis le code, titre
86
+ et en-têtes colorés avec la palette AcadéNice (marine `#0e2656`, teal `#24947a`,
87
+ turquoise `#4cccb8`). Logo optionnel via `GDOC_LOGO_PNG_URL` (ou `logoPngUrl` en
88
+ argument) : une URL **PNG** publique, insérée en haut du doc ; absente, le doc
89
+ est brandé par les couleurs seules (Google Docs n'accepte pas le SVG).
90
+ - **Template** (`GDOC_TEMPLATE_ID` ou `templateId` en argument) : le doc est une
91
+ **copie** d'un Google Doc template brandé (logo + palette dans le template), puis
92
+ les `{{PLACEHOLDERS}}` sont remplis (`{{TITLE}}`, `{{BLOCS}}`, `{{RESSOURCES}}`,
93
+ plus tout `{{KEY}}` passé via `fields`). C'est le mode recommandé pour un logo :
94
+ Google Docs n'insère pas de SVG, donc le logo vit dans le template (image déjà
95
+ posée), pas réinséré à chaque publication.
96
+
97
+ ## Portée et limites (honnêtes)
98
+
99
+ - Scopes : `documents` + `drive.file` (par-fichier, le plus étroit suffisant pour
100
+ créer + partager). Pas le scope large `drive`.
101
+ - Propriété des fichiers, selon le type de projet GCP :
102
+ - **Projet standalone (hors Workspace)** : le service account possède les
103
+ fichiers qu'il crée, dans son propre Drive (quota ~15 Go, valeur rapportée
104
+ par la communauté) ; partage-les via `shareWith` ; supprimer le service
105
+ account supprime ses fichiers.
106
+ - **Projet rattaché à une org Workspace (le setup recommandé plus haut)** : un
107
+ service account ne peut pas posséder d'assets Drive de l'org (Google IAM,
108
+ `service-account-overview`). Dans ce cas, crée le doc dans un **Shared Drive**
109
+ possédé par l'org (cible un dossier parent), ou impersonne un utilisateur via
110
+ la délégation domaine. Increment suivant côté code : un `GDOC_PARENT_FOLDER_ID`
111
+ (Shared Drive) ; en attendant, le partage `shareWith` reste fonctionnel et le
112
+ doc vit dans le stockage du service account.
113
+ - La délégation domaine n'est pas requise pour le mode standalone (le SA agit
114
+ pour lui-même). Elle ne sert qu'à faire posséder le fichier par un utilisateur.
115
+
116
+ ## Contenu RNCP (adaptateur)
117
+
118
+ `byan_publish` est générique. Pour publier un retour d'évaluation RNCP, un
119
+ adaptateur transforme l'objet eval (couverture par bloc + synthèse) en
120
+ `{ title, sections, resources }` puis appelle `byan_publish`. Cet adaptateur
121
+ vit là où la chaîne RNCP est définie -- hors de ce module générique.
@@ -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');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-byan-agent",
3
- "version": "2.29.2",
3
+ "version": "2.35.0",
4
4
  "description": "BYAN v2.8 - Intelligent AI agent creator with ELO trust system + scientific fact-check + Hermes universal dispatcher + native Claude Code integration (hooks, skills, MCP server). Multi-platform (Claude Code, Codex). Merise Agile + TDD + 71 Mantras. ~54% LLM cost savings.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -18,6 +18,7 @@
18
18
  "test:e2e": "jest install/__tests__/post-install.e2e.test.js",
19
19
  "setup-turbo-whisper": "node install/setup-turbo-whisper.js",
20
20
  "setup-rtk": "node install/setup-rtk.js",
21
+ "setup-gdoc": "node install/setup-gdoc.js",
21
22
  "byan": "echo \"BYAN agent installed. Open Claude Code (or Codex) in this project.\"",
22
23
  "version": "node scripts/sync-install-version.js && git add install/package.json",
23
24
  "app:dev": "npm --prefix app run dev",
@@ -63,6 +64,7 @@
63
64
  "install/setup-turbo-whisper.js",
64
65
  "install/setup-parakeet.js",
65
66
  "install/setup-rtk.js",
67
+ "install/setup-gdoc.js",
66
68
  "install/install.sh",
67
69
  "install/package.json",
68
70
  "update-byan-agent/bin/",