create-byan-agent 2.29.2 → 2.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +222 -0
- package/install/bin/create-byan-agent-v2.js +29 -1
- package/install/lib/gdoc-setup.js +210 -0
- package/install/lib/mcp-extensions/gdrive.js +27 -2
- package/install/lib/rtk-integration.js +18 -8
- package/install/package.json +1 -1
- package/install/packages/platform-config/lib/credentials.js +13 -1
- package/install/setup-gdoc.js +41 -0
- package/install/setup-rtk.js +1 -1
- package/install/templates/.claude/CLAUDE.md +15 -4
- package/install/templates/.claude/hooks/inject-soul.js +4 -3
- package/install/templates/.claude/hooks/inject-tao.js +65 -25
- package/install/templates/.claude/hooks/inject-voice-anchor.js +102 -0
- package/install/templates/.claude/rules/portable-core.md +81 -0
- package/install/templates/.claude/settings.json +5 -1
- package/install/templates/.claude/skills/byan-hermes-dispatch/SKILL.md +16 -2
- package/install/templates/_byan/mcp/byan-mcp-server/lib/gdoc-client.js +203 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/gdoc-content.js +203 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/resolve-config.js +15 -2
- package/install/templates/_byan/mcp/byan-mcp-server/lib/sync-rules.js +1 -1
- package/install/templates/_byan/mcp/byan-mcp-server/package.json +2 -0
- package/install/templates/_byan/mcp/byan-mcp-server/server.js +70 -0
- package/install/templates/_byan/mcp/byan-mcp-server/skill-bundles-manifest.json +1 -1
- package/install/templates/dist/skill-bundles/byan-hermes-dispatch.zip +0 -0
- package/install/templates/docs/google-docs-publish.md +121 -0
- package/node_modules/byan-platform-config/lib/credentials.js +13 -1
- package/package.json +3 -1
|
@@ -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
|
|
30
|
-
|
|
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
|
|
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,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": "
|
|
187
|
+
"sourceHash": "45003e8a525a792d1a5dbe34ecb8e1443061fa5036a81a38a841598ce5aaf5ff"
|
|
188
188
|
},
|
|
189
189
|
"byan-insight": {
|
|
190
190
|
"name": "byan-insight",
|
|
Binary file
|
|
@@ -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
|
-
|
|
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.
|
|
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/",
|