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,126 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Stop hook — BYAN punt-guard (F3).
4
+ *
5
+ * Goal : catch the laziness pattern where the agent ends its turn by telling the
6
+ * user to run a command and paste the output back, when the agent could have run
7
+ * it itself (a Bash tool-call) this turn. The pure detection lives in
8
+ * lib/punt-detect.js; this hook wires it to the real Stop payload and the arm
9
+ * flag.
10
+ *
11
+ * Ships DISARMED (puntGuard.armed=false in _byan/_config/delivery-default.json),
12
+ * same posture as the autobench Stop guard. Arming a Stop blocker without first
13
+ * measuring false positives breaks the flow. So by default the hook only
14
+ * OBSERVES: it appends one line to _byan-output/punt-ledger.jsonl
15
+ * (observed-disarmed / observed-disarmed-punt) and exits 0. Only when armed does
16
+ * it block (exit 2) on a detected punt.
17
+ *
18
+ * Carve-out (in punt-detect): git push / npm publish are never flagged — the
19
+ * server has no creds, so delegating those to the user is legitimate.
20
+ *
21
+ * Non-blocking on any IO/parse error : the hook never traps a turn it cannot read.
22
+ */
23
+
24
+ 'use strict';
25
+
26
+ const fs = require('fs');
27
+ const path = require('path');
28
+ const { decide } = require('./lib/punt-detect');
29
+ const { loadConfig } = require('./lib/delivery-contract');
30
+ // transcript-read is the canonical Stop-payload reader (text + raw content); it
31
+ // does NOT carry stdin helpers, so readStdin/parseJson come from strict-runtime
32
+ // like the other Stop/prompt hooks.
33
+ const { extractLastAssistantText, extractLastAssistantContent } = require('./lib/transcript-read');
34
+ const { readStdin, parseJson } = require('./lib/strict-runtime');
35
+
36
+ function projectRoot() {
37
+ return process.env.CLAUDE_PROJECT_DIR || process.cwd();
38
+ }
39
+
40
+ function isArmed(config) {
41
+ const pg = config && config.puntGuard;
42
+ return !!(pg && pg.armed === true);
43
+ }
44
+
45
+ // Pull the Bash tool-calls out of the finished assistant turn's RAW content.
46
+ // transcript-read gives the block array; a Bash tool_use is
47
+ // { type:'tool_use', name:'Bash', input:{ command } }. Returns [{ name, command }].
48
+ function bashToolCalls(content) {
49
+ if (!Array.isArray(content)) return [];
50
+ const out = [];
51
+ for (const b of content) {
52
+ if (b && b.type === 'tool_use' && typeof b.name === 'string') {
53
+ const command = b.input && typeof b.input.command === 'string' ? b.input.command : '';
54
+ out.push({ name: b.name, command });
55
+ }
56
+ }
57
+ return out;
58
+ }
59
+
60
+ function ledgerPath() {
61
+ return path.join(projectRoot(), '_byan-output', 'punt-ledger.jsonl');
62
+ }
63
+
64
+ function appendLedger(entry) {
65
+ try {
66
+ const p = ledgerPath();
67
+ fs.mkdirSync(path.dirname(p), { recursive: true });
68
+ fs.appendFileSync(p, JSON.stringify(entry) + '\n');
69
+ return true;
70
+ } catch {
71
+ return false;
72
+ }
73
+ }
74
+
75
+ if (require.main === module) {
76
+ (async () => {
77
+ // Wrap everything : the hook must NEVER trap a turn it cannot read.
78
+ try {
79
+ const config = loadConfig(projectRoot());
80
+ const payload = parseJson(await readStdin());
81
+ const lastAssistantText = extractLastAssistantText(payload);
82
+ const toolCallsThisTurn = bashToolCalls(extractLastAssistantContent(payload));
83
+
84
+ const result = decide({ lastAssistantText, toolCallsThisTurn });
85
+ const armed = isArmed(config);
86
+
87
+ const event = armed
88
+ ? result.punt
89
+ ? 'fired-block'
90
+ : 'no-punt'
91
+ : result.punt
92
+ ? 'observed-disarmed-punt'
93
+ : 'observed-disarmed';
94
+
95
+ appendLedger({
96
+ event,
97
+ punt: result.punt,
98
+ cmd: result.cmd || undefined,
99
+ reason: result.reason,
100
+ armed,
101
+ ts: process.env.BYAN_HOOK_TS || undefined,
102
+ session: process.env.CLAUDE_SESSION_ID || undefined,
103
+ });
104
+
105
+ if (armed && result.punt) {
106
+ const reason =
107
+ `Punt-guard: you asked the user to run "${result.cmd}" and paste the output, ` +
108
+ `but you did not run it via Bash this turn. Run it yourself, then report the result. ` +
109
+ `(git push / npm publish are exempt — the server has no creds for those.)`;
110
+ process.stdout.write(
111
+ JSON.stringify({ decision: 'block', reason, systemMessage: reason })
112
+ );
113
+ process.exit(2);
114
+ }
115
+
116
+ process.stdout.write(JSON.stringify({ continue: true }));
117
+ process.exit(0);
118
+ } catch {
119
+ // Last-resort net : on any unexpected failure, let the turn end.
120
+ process.stdout.write(JSON.stringify({ continue: true }));
121
+ process.exit(0);
122
+ }
123
+ })();
124
+ }
125
+
126
+ module.exports = { bashToolCalls, isArmed, ledgerPath, appendLedger };
@@ -23,15 +23,38 @@ const { extractLastAssistantText } = require('./lib/transcript-read');
23
23
 
24
24
  const DEFAULT_MARKERS = ['done', 'finished', 'complete', 'delivered', 'ready'];
25
25
 
26
+ // Strip the contexts where a completion marker is a MENTION, not a CLAIM:
27
+ // fenced + inline code, HTML comments (the BYAN-BENCH:done marker lives there),
28
+ // and snake_case / namespaced identifiers (byan_strict_complete, BENCH:done). A
29
+ // marker that survives this strip is prose -- the only place a real "it is done"
30
+ // claim lives. Exported so the regression cases are unit-testable.
31
+ function denoiseForClaim(text) {
32
+ return String(text)
33
+ .replace(/```[\s\S]*?```/g, ' ') // fenced code blocks
34
+ .replace(/`[^`]*`/g, ' ') // inline code spans
35
+ .replace(/<!--[\s\S]*?-->/g, ' ') // HTML comments (e.g. <!-- BYAN-BENCH:done -->)
36
+ .replace(/[A-Za-z0-9]+(?:[_:][A-Za-z0-9]+)+/g, ' '); // snake_case / ns identifiers
37
+ }
38
+
39
+ // A completion marker counts only as a STANDALONE claim, bounded by non-letters
40
+ // (Unicode-aware via the u flag, so "indefini" does not embed "fini" and
41
+ // "determine" does not embed "termine"), with a permissive trailing inflection
42
+ // (livre -> livree / livres). Bias: a false negative is caught by the pre-commit
43
+ // gate (the hard net), while a false positive traps a legitimate turn -- so a
44
+ // marker that is only mentioned is NOT read as a claim.
26
45
  function claimsCompletion(text, markers) {
27
46
  if (!text) return false;
28
- const lower = text.toLowerCase();
47
+ const clean = denoiseForClaim(text).toLowerCase();
29
48
  return (markers || DEFAULT_MARKERS).some((m) => {
30
- const marker = String(m).toLowerCase();
31
- if (/^[a-z]+$/.test(marker)) {
32
- return new RegExp(`\\b${marker}\\b`).test(lower);
49
+ const marker = String(m).toLowerCase().trim();
50
+ if (!marker) return false;
51
+ const escaped = marker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
52
+ try {
53
+ return new RegExp(`(?<![\\p{L}])${escaped}(?:s|e|es|ée|ées|és)?(?![\\p{L}])`, 'iu').test(clean);
54
+ } catch {
55
+ // Older runtimes without lookbehind/\p{L} -> fall back to a plain include.
56
+ return clean.includes(marker);
33
57
  }
34
- return lower.includes(marker);
35
58
  });
36
59
  }
37
60
 
@@ -85,4 +108,4 @@ if (require.main === module) {
85
108
  })();
86
109
  }
87
110
 
88
- module.exports = { decideStop, claimsCompletion, extractLastAssistantText };
111
+ module.exports = { decideStop, claimsCompletion, denoiseForClaim, extractLastAssistantText };
@@ -0,0 +1,81 @@
1
+ # BYAN Portable Core — Noyau portable, projection native
2
+
3
+ > BYAN est livre via npm et tourne sur deux plateformes (Claude Code, Codex).
4
+ > Son identite et son etat doivent survivre a l'absence de toute feature native.
5
+ > Les features natives sont un accelerateur opportuniste, pas une bequille.
6
+
7
+ ## Principe (hexagonal / ports-and-adapters)
8
+
9
+ Le noyau de BYAN — identite (soul, tao, soul-memory), etat (FD, strict, ELO),
10
+ connaissance — vit dans des artefacts **portables, in-repo**, sous `_byan/` et
11
+ `_byan-output/`, plus l'API `byan_web` quand le reseau est la. Ce noyau est la
12
+ **source de verite**. Il est shippe, il marche offline, il marche sur une machine
13
+ neuve, il marche sur Codex.
14
+
15
+ Les fonctionnalites natives de la plateforme (Claude Code) sont une **couche
16
+ d'acceleration en write-through** : BYAN ecrit *vers* elles pour en tirer le
17
+ benefice (cache, recall, isolation), mais ne les lit pas comme autorite. Si la
18
+ couche native est absente ou videe, BYAN se reconstruit depuis le noyau portable
19
+ sans rien perdre de son identite ni de son etat.
20
+
21
+ ```
22
+ +---------------------- noyau portable (source de verite) ----------------------+
23
+ | _byan/agent/byan/{soul,tao,soul-memory}.md _byan-output/fd-state.json |
24
+ | _byan/memoire/{elo,fact-graph}.json byan_web API (autorite reseau) |
25
+ +-------------------------------------------------------------------------------+
26
+ | (write-through, pas une dependance) ^ (reconstruction)
27
+ v |
28
+ +---------------------- adaptateurs natifs (accelerateurs) ---------------------+
29
+ | injection prefix-cache @-import memory files hooks subagent isolation |
30
+ +-------------------------------------------------------------------------------+
31
+ ```
32
+
33
+ ## Les trois frontieres
34
+
35
+ 1. **Source de verite portable.** Toute chose dont BYAN a besoin pour etre
36
+ lui-meme est un fichier sous `_byan/` (ou une entree `byan_web`). Rien
37
+ d'essentiel ne vit uniquement dans une couche native.
38
+ 2. **Le natif est un accelerateur write-through.** On projette l'etat portable
39
+ *vers* le natif pour la vitesse / le cache / le recall. On ne doit pas dependre
40
+ du natif en lecture pour une fonction essentielle. La projection est derivee et
41
+ regenerable depuis le noyau.
42
+ 3. **L'AutoMem est hors-perimetre.** Le repertoire AutoMem natif de Claude Code
43
+ (`~/.claude/projects/<hash>/memory/`) est local a l'assistant, par-machine,
44
+ indexe par un hash du chemin projet — **non shippable** via npm. Il n'est pas
45
+ une source de verite BYAN, et aucun chemin de lecture critique n'en depend. Ce
46
+ qui doit etre portable vit dans `soul-memory` / `_byan/`.
47
+
48
+ ## Table : feature native -> adaptateur -> chemin degrade
49
+
50
+ | Feature native Claude | Adaptateur BYAN (write-through) | Chemin degrade (natif absent / Codex) |
51
+ |-----------------------|--------------------------------|----------------------------------------|
52
+ | Prompt caching (prefixe stable) | `inject-soul.js` / `inject-tao.js` injectent soul+tao depuis `_byan/` au SessionStart | la voix reste lue depuis `_byan/` ; `inject-voice-anchor` re-pose l'ancre par tour |
53
+ | Memory files (`@-import`) | fichiers in-repo (`_byan/`, `.claude/rules/`) charges nativement par Claude | sur Codex, le bloc `AGENTS.md` porte l'equivalent ; les regles restent des fichiers lisibles |
54
+ | Hooks (SessionStart / PreCompact / Stop) | `.claude/hooks/*.js` | sur Codex (pas de hook), le contexte est porte par `AGENTS.md` ; le filet pre-commit reste actif |
55
+ | Subagent isolation | `byan-hermes-dispatch` delegue et ne ramene qu'un distillat | si pas de subagent, execution inline main-thread (meme resultat, plus de tokens) |
56
+ | Compaction | `pre-compact-save.js` ecrit un snapshot sous `_byan-output/` | l'etat FD vit dans `_byan-output/fd-state.json`, relisible sans la couche native |
57
+
58
+ L'AutoMem n'a **pas** de ligne : ce n'est pas un adaptateur, il est hors-perimetre.
59
+
60
+ ## Le test de degradation (litmus)
61
+
62
+ L'independance n'est pas une promesse, c'est un test. Wipe la couche native
63
+ (hooks off, `~/.claude/` absent, Codex) : l'identite (soul/tao/soul-memory) et
64
+ l'etat FD doivent se reconstruire depuis les artefacts portables `_byan/` seuls.
65
+ Rebranche le natif : BYAN tourne plus lean (cache, distillat). **Les deux doivent
66
+ tenir.** Garde-fou mecanique : `.claude/__tests__/portable-core.test.js` echoue
67
+ si un chemin critique se met a dependre de l'AutoMem natif.
68
+
69
+ ## Mantras
70
+
71
+ - `PORTABLE-1` Source de verite in-repo. Le noyau vit sous `_byan/`, pas dans le natif.
72
+ - `PORTABLE-2` Natif = accelerateur, pas une dependance. On ecrit vers, on ne lit pas comme autorite.
73
+ - `PORTABLE-3` AutoMem hors-perimetre. `~/.claude/projects/.../memory/` n'est pas une source BYAN.
74
+ - `PORTABLE-4` Projection derivee. Toute copie native est regenerable depuis le noyau.
75
+ - `PORTABLE-5` Degradation testee. L'independance passe un test, elle ne se decrete pas.
76
+
77
+ ## References
78
+
79
+ - Doctrine token (pourquoi un pointeur sans `@-import`) : CLAUDE.md, section `## Compact instructions` (chantier context-engineering).
80
+ - Isolation subagent (levier "plus avec moins") : `.claude/skills/byan-hermes-dispatch/SKILL.md`.
81
+ - Etat FD portable : `_byan/mcp/byan-mcp-server/lib/fd-state.js` -> `_byan-output/fd-state.json`.
@@ -8,6 +8,10 @@
8
8
  "type": "command",
9
9
  "command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/inject-soul.js"
10
10
  },
11
+ {
12
+ "type": "command",
13
+ "command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/inject-tao.js"
14
+ },
11
15
  {
12
16
  "type": "command",
13
17
  "command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/soul-memory-check.js"
@@ -21,7 +25,11 @@
21
25
  "hooks": [
22
26
  {
23
27
  "type": "command",
24
- "command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/inject-tao.js"
28
+ "command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/inject-voice-anchor.js"
29
+ },
30
+ {
31
+ "type": "command",
32
+ "command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/inject-delivery-default.js"
25
33
  },
26
34
  {
27
35
  "type": "command",
@@ -66,6 +74,10 @@
66
74
  "type": "command",
67
75
  "command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/autobench-stop-guard.js"
68
76
  },
77
+ {
78
+ "type": "command",
79
+ "command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/punt-guard.js"
80
+ },
69
81
  {
70
82
  "type": "command",
71
83
  "command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/drain-advisory.js"
@@ -73,10 +73,12 @@ prompt: |
73
73
  Load persona first : read <specialist stub path>.
74
74
  Task : <full goal>
75
75
  Deliverables : <list>
76
- When done, write a concise report (< 200 words).
76
+ When done, return ONLY a distilled summary (< 200 words, ~1-2k tokens) :
77
+ the verbose tool output, file contents and intermediate reasoning stay in
78
+ YOUR context and do not cross back -- only the distillate returns.
77
79
  ```
78
80
 
79
- **`mcp-worker`** : same Agent tool call but without `isolation`. Set the Agent's `model` to the returned `model` — `haiku` for exploration nature, otherwise omit `model` to inherit the session model. The tier follows the task nature, not its size.
81
+ **`mcp-worker`** : same Agent tool call but without `isolation` — including the SAME distilled-summary cap in the prompt (the subagent returns only the distillate, not its verbose work). Set the Agent's `model` to the returned `model` — `haiku` for exploration nature, otherwise omit `model` to inherit the session model. The tier follows the task nature, not its size.
80
82
 
81
83
  For any spawned strategy : pass `model` to the Agent tool when it is non-null; omit it when null so the subagent inherits the session model.
82
84
 
@@ -110,9 +112,21 @@ If the user (or calling agent) provides N independent subtasks and `parallelizab
110
112
  2. Dispatch all N Agent tool calls **in one message**.
111
113
  3. Aggregate via `coordination.aggregate` and `writeSummary`.
112
114
 
115
+ ## Subagent isolation (token leverage)
116
+
117
+ A subagent runs in its OWN context window — use that. Let it explore verbosely
118
+ (read files, run tools, reason at length) inside its own context, and have it
119
+ return ONLY a distilled summary (< 200 words, ~1-2k tokens) to the main thread.
120
+ The verbose work does not land in the lead context : this is the Anthropic
121
+ context-isolation principle (a subagent may burn ~9k tokens internally yet return
122
+ ~1-2k). It applies to BOTH spawn paths (worktree and mcp-worker), and it is why
123
+ heavy, verbose or exploratory work is worth delegating even when the lead could
124
+ do it inline — the delegation keeps the lead context lean ("plus avec moins").
125
+
113
126
  ## Hard rules
114
127
 
115
128
  - **Never ask for confirmation** before spawning. User opted into autonomous mode (Q1.b).
116
129
  - **Never execute the specialist's work yourself** unless strategy says `main-thread*`. You dispatch, you do not become the specialist.
117
130
  - **Never spawn with `isolation: "worktree"` for tasks < score 15** — the boot cost exceeds the gain.
118
131
  - **Never fabricate a specialist name**. If no match, say so and use `general-purpose`.
132
+ - **Cap every subagent return.** Both spawn paths (worktree AND mcp-worker) instruct the subagent to return only a distilled summary (< 200 words / ~1-2k tokens) — the raw exploration stays in the subagent's context.
@@ -0,0 +1,22 @@
1
+ {
2
+ "version": 1,
3
+ "name": "BYAN Delivery Default",
4
+ "description": "Makes prod-grade + maximal scope the mechanical default. F1 (the delivery-contract anchor) ships LIVE: it is pure injected context, no risk. F2 (completeness reject) and F3 (punt-guard) ship DISARMED behind armed flags: arming a turn/commit blocker without first measuring false positives would break the flow and could lock an in-progress strict session.",
5
+ "optOutWords": [
6
+ "mvp",
7
+ "quick",
8
+ "brouillon",
9
+ "jette",
10
+ "prototype",
11
+ "vite fait",
12
+ "pas besoin que ce soit parfait",
13
+ "poc",
14
+ "draft"
15
+ ],
16
+ "completenessGate": {
17
+ "armed": false
18
+ },
19
+ "puntGuard": {
20
+ "armed": false
21
+ }
22
+ }
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * channel-entry.js — Entrypoint stdio du channel MCP byan pour Claude Code.
4
+ *
5
+ * POURQUOI un fichier séparé de server.js : le channel est un serveur MCP
6
+ * distinct du serveur principal byan-mcp. Claude Code le spawne comme un
7
+ * subprocess indépendant via --dangerously-load-development-channels.
8
+ * server.js ne doit pas acquérir le transport channel comme effet de bord.
9
+ *
10
+ * Commande de lancement (research preview) :
11
+ * claude --dangerously-load-development-channels server:byan-channel
12
+ *
13
+ * Enregistrement dans .mcp.json (section mcpServers) — exactement ce que pose
14
+ * l'installeur : chemin RELATIF au projectRoot (portable, jamais absolu) et env
15
+ * VIDE (la config est resolue au boot via resolveConfig ci-dessous ; aucun secret
16
+ * n'est ecrit dans .mcp.json qui est tracke par git) :
17
+ * "byan-channel": {
18
+ * "command": "node",
19
+ * "args": ["_byan/mcp/byan-mcp-server/channel-entry.js"],
20
+ * "env": {}
21
+ * }
22
+ *
23
+ * Voie plugin (Phase 2, non implémentée ici) :
24
+ * Wrapper le channel-entry.js dans un plugin Claude Code et l'enregistrer
25
+ * dans la marketplace pour qu'il soit accessible via
26
+ * --channels plugin:byan-channel@byan-marketplace.
27
+ * Nécessite que le channel soit sur l'allowlist Anthropic ou sur la liste
28
+ * allowedChannelPlugins de l'organisation (Enterprise).
29
+ */
30
+
31
+ import { resolveConfig } from './lib/resolve-config.js';
32
+ import { runChannelStdio } from './lib/channel-server.js';
33
+
34
+ // Config : même resolver que server.js (env -> ~/.byan/credentials.json -> défauts).
35
+ const config = resolveConfig();
36
+ const apiUrl = config.BYAN_API_URL || process.env.BYAN_API_URL;
37
+ const apiToken = config.BYAN_API_TOKEN || process.env.BYAN_API_TOKEN;
38
+
39
+ if (!apiUrl) {
40
+ process.stderr.write('[byan-channel] BYAN_API_URL manquant — le poll sera désactivé\n');
41
+ }
42
+ if (!apiToken) {
43
+ process.stderr.write('[byan-channel] BYAN_API_TOKEN manquant — le poll et les replies seront désactivés\n');
44
+ }
45
+
46
+ await runChannelStdio({ apiUrl, apiToken });
@@ -0,0 +1,128 @@
1
+ /**
2
+ * lib/channel-poll.js — Boucle de poll outbox byan_web pour le channel CC.
3
+ *
4
+ * POURQUOI ce module est séparé de channel-server.js : isoler le réseau
5
+ * permet de le mocker proprement dans les tests sans toucher au serveur MCP.
6
+ *
7
+ * Contrat API (réconcilié avec les routes F2b dans api/routes/sessions-cli.js) :
8
+ * GET /api/sessions/outbox
9
+ * -> { data: [{ id, session_id, content, meta? }] }
10
+ * Les messages sont scoped au user via le token (RBAC serveur).
11
+ * POST /api/sessions/:session_id/outbox/:msg_id/ack
12
+ * -> { ok: true }
13
+ * Marque le message comme livré ; l'API doit être idempotente.
14
+ *
15
+ * Sécurité / gate sender : le poll utilise le token byan_web du user (scope
16
+ * user). La barrière anti-injection réelle est le RBAC owner côté serveur :
17
+ * seuls les messages des sessions appartenant à ce user sont dans l'outbox.
18
+ * Ce module ne filtre pas davantage : le gate est la vraie barrière.
19
+ */
20
+
21
+ const DEFAULT_POLL_INTERVAL_MS = 5_000; // 5 s — équilibre réactivité / charge API
22
+ const DEFAULT_TIMEOUT_MS = 8_000; // abort si l'API ne répond pas
23
+
24
+ /**
25
+ * Crée et démarre une boucle de poll vers l'outbox byan_web.
26
+ *
27
+ * @param {object} opts
28
+ * @param {string} opts.apiUrl Base URL byan_web (sans /api final)
29
+ * @param {string} opts.apiToken Token byan_web du user (ApiKey scheme)
30
+ * @param {Function} opts.onMessage Callback async (msg) => void, appelé pour chaque message pending
31
+ * @param {number} [opts.intervalMs] Intervalle entre polls (défaut 5 s)
32
+ * @param {object} [opts.fetchImpl] Override fetch pour les tests
33
+ * @returns {{ stop: Function }} Stopper la boucle
34
+ */
35
+ export function startPollLoop({ apiUrl, apiToken, onMessage, intervalMs, fetchImpl }) {
36
+ const interval = intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
37
+ const fetchFn = fetchImpl ?? globalThis.fetch;
38
+
39
+ let active = true;
40
+ let timer = null;
41
+
42
+ const authHeaders = () => {
43
+ // byan_web exige ApiKey pour les tokens préfixés byan_, Bearer sinon.
44
+ const scheme = apiToken && apiToken.startsWith('byan_') ? 'ApiKey' : 'Bearer';
45
+ return { Authorization: `${scheme} ${apiToken}` };
46
+ };
47
+
48
+ async function poll() {
49
+ if (!active) return;
50
+ try {
51
+ const ctrl = new AbortController();
52
+ const timeout = setTimeout(() => ctrl.abort(), DEFAULT_TIMEOUT_MS);
53
+ let res;
54
+ try {
55
+ res = await fetchFn(`${apiUrl}/api/sessions/outbox`, {
56
+ headers: { ...authHeaders(), 'Content-Type': 'application/json' },
57
+ signal: ctrl.signal,
58
+ });
59
+ } finally {
60
+ clearTimeout(timeout);
61
+ }
62
+
63
+ if (!res.ok) {
64
+ // Best-effort : ne pas interrompre la boucle sur une erreur transitoire.
65
+ // 401/403 sont loggés mais pas relancés (le token est fixe dans cette session).
66
+ if (res.status === 401 || res.status === 403) {
67
+ process.stderr.write(`[byan-channel] outbox auth error ${res.status} — vérifier BYAN_API_TOKEN\n`);
68
+ }
69
+ return;
70
+ }
71
+
72
+ const body = await res.json();
73
+ const messages = Array.isArray(body?.data) ? body.data : [];
74
+
75
+ for (const msg of messages) {
76
+ if (!msg || !msg.id || !msg.session_id) continue;
77
+
78
+ // Livrer le message au channel (notification MCP).
79
+ try {
80
+ await onMessage(msg);
81
+ } catch (err) {
82
+ process.stderr.write(`[byan-channel] onMessage error: ${err.message}\n`);
83
+ }
84
+
85
+ // Acquitter le message : marque "livré" côté serveur.
86
+ // Contrat F2b : POST /api/sessions/:session_id/outbox/:msg_id/ack
87
+ // Idempotent — si l'ack échoue on retente au prochain poll (le message
88
+ // réapparaît dans l'outbox, ce qui entraîne une double notification).
89
+ // La double notification est préférable à un message perdu.
90
+ try {
91
+ const ackCtrl = new AbortController();
92
+ const ackTimeout = setTimeout(() => ackCtrl.abort(), DEFAULT_TIMEOUT_MS);
93
+ try {
94
+ await fetchFn(
95
+ `${apiUrl}/api/sessions/${encodeURIComponent(msg.session_id)}/outbox/${encodeURIComponent(msg.id)}/ack`,
96
+ { method: 'POST', headers: authHeaders(), signal: ackCtrl.signal }
97
+ );
98
+ } finally {
99
+ clearTimeout(ackTimeout);
100
+ }
101
+ } catch (ackErr) {
102
+ // Ack raté = best-effort ; on ne bloque pas la livraison.
103
+ process.stderr.write(`[byan-channel] ack failed for msg ${msg.id}: ${ackErr.message}\n`);
104
+ }
105
+ }
106
+ } catch (err) {
107
+ // Réseau injoignable : best-effort, on retente au prochain tick.
108
+ if (err.name !== 'AbortError') {
109
+ process.stderr.write(`[byan-channel] poll error: ${err.message}\n`);
110
+ }
111
+ } finally {
112
+ if (active) {
113
+ // Programmer le prochain poll seulement si la boucle est encore active.
114
+ timer = setTimeout(poll, interval);
115
+ }
116
+ }
117
+ }
118
+
119
+ // Démarrage immédiat puis périodique.
120
+ timer = setTimeout(poll, 0);
121
+
122
+ return {
123
+ stop() {
124
+ active = false;
125
+ if (timer) clearTimeout(timer);
126
+ },
127
+ };
128
+ }