create-byan-agent 2.9.4 → 2.9.6

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 (98) hide show
  1. package/install/bin/byan-cleanup.js +156 -0
  2. package/install/bin/byan-kanban.js +159 -0
  3. package/install/bin/byan-ledger.js +45 -0
  4. package/install/bin/create-byan-agent-v2.js +15 -1
  5. package/install/lib/cleanup/detector.js +154 -0
  6. package/install/lib/cleanup/executor.js +72 -0
  7. package/install/lib/staging-consent.js +149 -0
  8. package/install/lib/subagent-generator.js +208 -0
  9. package/install/lib/token-ledger.js +131 -0
  10. package/install/templates/.claude/agents/bmad-bmad-master.md +14 -0
  11. package/install/templates/.claude/agents/bmad-bmb-agent-builder.md +14 -0
  12. package/install/templates/.claude/agents/bmad-bmb-module-builder.md +14 -0
  13. package/install/templates/.claude/agents/bmad-bmb-workflow-builder.md +14 -0
  14. package/install/templates/.claude/agents/bmad-bmm-analyst.md +14 -0
  15. package/install/templates/.claude/agents/bmad-bmm-architect.md +14 -0
  16. package/install/templates/.claude/agents/bmad-bmm-dev.md +14 -0
  17. package/install/templates/.claude/agents/bmad-bmm-pm.md +14 -0
  18. package/install/templates/.claude/agents/bmad-bmm-quick-flow-solo-dev.md +14 -0
  19. package/install/templates/.claude/agents/bmad-bmm-quinn.md +14 -0
  20. package/install/templates/.claude/agents/bmad-bmm-sm.md +14 -0
  21. package/install/templates/.claude/agents/bmad-bmm-tech-writer.md +14 -0
  22. package/install/templates/.claude/agents/bmad-bmm-ux-designer.md +14 -0
  23. package/install/templates/.claude/agents/bmad-byan-v2.md +14 -0
  24. package/install/templates/.claude/agents/bmad-byan.md +152 -0
  25. package/install/templates/.claude/agents/bmad-carmack.md +14 -0
  26. package/install/templates/.claude/agents/bmad-cis-brainstorming-coach.md +14 -0
  27. package/install/templates/.claude/agents/bmad-cis-creative-problem-solver.md +14 -0
  28. package/install/templates/.claude/agents/bmad-cis-design-thinking-coach.md +14 -0
  29. package/install/templates/.claude/agents/bmad-cis-innovation-strategist.md +14 -0
  30. package/install/templates/.claude/agents/bmad-cis-presentation-master.md +14 -0
  31. package/install/templates/.claude/agents/bmad-cis-storyteller.md +14 -0
  32. package/install/templates/.claude/agents/bmad-claude.md +26 -0
  33. package/install/templates/.claude/agents/bmad-codex.md +26 -0
  34. package/install/templates/.claude/agents/bmad-compliance.md +68 -0
  35. package/install/templates/.claude/agents/bmad-drawio.md +25 -0
  36. package/install/templates/.claude/agents/bmad-expert-merise-agile.md +54 -0
  37. package/install/templates/.claude/agents/bmad-fact-checker.md +14 -0
  38. package/install/templates/.claude/agents/bmad-forgeron.md +14 -0
  39. package/install/templates/.claude/agents/bmad-hermes.md +59 -0
  40. package/install/templates/.claude/agents/bmad-marc.md +25 -0
  41. package/install/templates/.claude/agents/bmad-patnote.md +26 -0
  42. package/install/templates/.claude/agents/bmad-rachid.md +25 -0
  43. package/install/templates/.claude/agents/bmad-tao.md +14 -0
  44. package/install/templates/.claude/agents/bmad-tea-tea.md +14 -0
  45. package/install/templates/.claude/agents/bmad-yanstaller.md +47 -0
  46. package/install/templates/.claude/hooks/fact-check-absolutes.js +185 -0
  47. package/install/templates/.claude/hooks/fd-phase-guard.js +87 -0
  48. package/install/templates/.claude/hooks/fd-response-check.js +92 -0
  49. package/install/templates/.claude/hooks/lib/failure-detector.js +14 -0
  50. package/install/templates/.claude/hooks/pre-compact-save.js +148 -0
  51. package/install/templates/.claude/hooks/stage-to-byan.js +119 -0
  52. package/install/templates/.claude/hooks/tool-failure-guard.js +6 -0
  53. package/install/templates/.claude/hooks/tool-transparency.js +4 -0
  54. package/install/templates/.claude/settings.json +27 -0
  55. package/install/templates/.claude/skills/byan-byan/SKILL.md +115 -163
  56. package/install/templates/.claude/skills/byan-orchestrate/SKILL.md +100 -0
  57. package/install/templates/.githooks/pre-commit +75 -0
  58. package/install/templates/.github/extensions/byan-staging/extension.mjs +169 -0
  59. package/install/templates/.github/extensions/byan-staging/package.json +8 -0
  60. package/install/templates/_byan/mcp/byan-mcp-server/lib/copilot.js +148 -0
  61. package/install/templates/_byan/mcp/byan-mcp-server/lib/fd-state.js +163 -0
  62. package/install/templates/_byan/mcp/byan-mcp-server/lib/kanban.js +226 -0
  63. package/install/templates/_byan/mcp/byan-mcp-server/lib/peer-review.js +187 -0
  64. package/install/templates/_byan/mcp/byan-mcp-server/server.js +463 -0
  65. package/install/templates/detector.js +154 -0
  66. package/package.json +6 -7
  67. package/src/loadbalancer/capability-matrix.js +157 -0
  68. package/src/loadbalancer/config.js +141 -0
  69. package/src/loadbalancer/graceful-degradation.js +212 -0
  70. package/src/loadbalancer/health-probe.js +151 -0
  71. package/src/loadbalancer/hooks/claude-hooks.js +53 -0
  72. package/src/loadbalancer/hooks/copilot-hooks.js +74 -0
  73. package/src/loadbalancer/index.js +81 -0
  74. package/src/loadbalancer/loadbalancer.default.yaml +65 -0
  75. package/src/loadbalancer/loadbalancer.js +324 -0
  76. package/src/loadbalancer/mcp-server.js +304 -0
  77. package/src/loadbalancer/metrics.js +146 -0
  78. package/src/loadbalancer/native/claude-integration.js +64 -0
  79. package/src/loadbalancer/native/copilot-integration.js +59 -0
  80. package/src/loadbalancer/pressure-score.js +102 -0
  81. package/src/loadbalancer/providers/base-provider.js +80 -0
  82. package/src/loadbalancer/providers/byan-api-provider.js +132 -0
  83. package/src/loadbalancer/providers/claude-provider.js +113 -0
  84. package/src/loadbalancer/providers/copilot-provider.js +104 -0
  85. package/src/loadbalancer/rate-limit-tracker.js +216 -0
  86. package/src/loadbalancer/session-bridge.js +179 -0
  87. package/src/loadbalancer/state/db.js +211 -0
  88. package/src/loadbalancer/state/migrations/001-initial.sql +50 -0
  89. package/src/loadbalancer/tools/index.js +123 -0
  90. package/src/loadbalancer/velocity-estimator.js +147 -0
  91. package/src/staging/staging.js +394 -0
  92. package/update-byan-agent/bin/update-byan-agent.js +27 -2
  93. package/API-BYAN-V2.md +0 -741
  94. package/BMAD-QUICK-REFERENCE.md +0 -370
  95. package/CHANGELOG-v2.1.0.md +0 -371
  96. package/MIGRATION-v2.0-to-v2.1.md +0 -430
  97. package/README-BYAN-V2.md +0 -446
  98. package/TEST-GUIDE-v2.3.2.md +0 -161
@@ -0,0 +1,147 @@
1
+ /**
2
+ * VelocityEstimator — Sliding Window Request Rate
3
+ *
4
+ * Tracks request timestamps in a sliding window to calculate:
5
+ * - Current velocity (requests/minute)
6
+ * - Trend (accelerating / stable / decelerating)
7
+ * - ETA to a configurable threshold (minutes until likely rate limit)
8
+ *
9
+ * Emits 'threshold_warning' when velocity exceeds configured threshold.
10
+ */
11
+
12
+ const { EventEmitter } = require('events');
13
+
14
+ const TREND = {
15
+ ACCELERATING: 'accelerating',
16
+ STABLE: 'stable',
17
+ DECELERATING: 'decelerating',
18
+ IDLE: 'idle',
19
+ };
20
+
21
+ class VelocityEstimator extends EventEmitter {
22
+ /**
23
+ * @param {string} provider
24
+ * @param {object} opts
25
+ * @param {number} [opts.windowMs=120000] - Sliding window for velocity calc (default 2min)
26
+ * @param {number} [opts.warningThresholdPerMin=10] - Emit warning above this req/min
27
+ * @param {number} [opts.maxRequestsBeforeLimit=30] - Estimated provider limit per window for ETA
28
+ * @param {number} [opts.trendSplitRatio=0.5] - Split point for trend calc (first half vs second half)
29
+ */
30
+ constructor(provider, opts = {}) {
31
+ super();
32
+ this.provider = provider;
33
+ this.windowMs = opts.windowMs || 120000;
34
+ this.warningThreshold = opts.warningThresholdPerMin || 10;
35
+ this.maxRequestsBeforeLimit = opts.maxRequestsBeforeLimit || 30;
36
+ this.trendSplitRatio = opts.trendSplitRatio || 0.5;
37
+
38
+ this.timestamps = [];
39
+ this.warningEmitted = false;
40
+ }
41
+
42
+ recordRequest() {
43
+ const now = Date.now();
44
+ this.timestamps.push(now);
45
+ this._prune(now);
46
+
47
+ const velocity = this.getVelocity();
48
+ if (velocity >= this.warningThreshold && !this.warningEmitted) {
49
+ this.warningEmitted = true;
50
+ this.emit('threshold_warning', {
51
+ provider: this.provider,
52
+ velocity,
53
+ threshold: this.warningThreshold,
54
+ });
55
+ } else if (velocity < this.warningThreshold * 0.8) {
56
+ this.warningEmitted = false;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Current requests per minute in the sliding window.
62
+ */
63
+ getVelocity() {
64
+ const now = Date.now();
65
+ this._prune(now);
66
+ if (this.timestamps.length < 2) return 0;
67
+
68
+ const windowSpanMs = now - this.timestamps[0];
69
+ if (windowSpanMs < 1000) return 0; // min 1s span to avoid burst spikes
70
+
71
+ return (this.timestamps.length / windowSpanMs) * 60000;
72
+ }
73
+
74
+ /**
75
+ * Compare first-half velocity vs second-half velocity.
76
+ */
77
+ getTrend() {
78
+ const now = Date.now();
79
+ this._prune(now);
80
+ if (this.timestamps.length < 4) return TREND.IDLE;
81
+
82
+ const splitTime = now - (this.windowMs * this.trendSplitRatio);
83
+ const firstHalf = this.timestamps.filter(t => t < splitTime);
84
+ const secondHalf = this.timestamps.filter(t => t >= splitTime);
85
+
86
+ if (firstHalf.length === 0 || secondHalf.length === 0) return TREND.STABLE;
87
+
88
+ const firstSpan = splitTime - (now - this.windowMs);
89
+ const secondSpan = now - splitTime;
90
+
91
+ const firstRate = firstSpan > 0 ? (firstHalf.length / firstSpan) * 60000 : 0;
92
+ const secondRate = secondSpan > 0 ? (secondHalf.length / secondSpan) * 60000 : 0;
93
+
94
+ const ratio = firstRate > 0 ? secondRate / firstRate : 1;
95
+
96
+ if (ratio > 1.25) return TREND.ACCELERATING;
97
+ if (ratio < 0.75) return TREND.DECELERATING;
98
+ return TREND.STABLE;
99
+ }
100
+
101
+ /**
102
+ * Estimated minutes until rate limit at current velocity.
103
+ * Returns Infinity if velocity is 0 or negligible.
104
+ */
105
+ getEtaMinutes() {
106
+ const velocity = this.getVelocity();
107
+ if (velocity < 0.1) return Infinity;
108
+
109
+ const remaining = Math.max(0, this.maxRequestsBeforeLimit - this.timestamps.length);
110
+ return remaining / velocity;
111
+ }
112
+
113
+ /**
114
+ * Full snapshot for consumption by PressureScore / lb_quota.
115
+ */
116
+ getSnapshot() {
117
+ return {
118
+ provider: this.provider,
119
+ velocity: Math.round(this.getVelocity() * 100) / 100,
120
+ trend: this.getTrend(),
121
+ etaMinutes: Math.round(this.getEtaMinutes() * 10) / 10,
122
+ requestsInWindow: this.timestamps.length,
123
+ windowMs: this.windowMs,
124
+ maxRequestsBeforeLimit: this.maxRequestsBeforeLimit,
125
+ warningThreshold: this.warningThreshold,
126
+ };
127
+ }
128
+
129
+ reset() {
130
+ this.timestamps = [];
131
+ this.warningEmitted = false;
132
+ }
133
+
134
+ destroy() {
135
+ this.reset();
136
+ this.removeAllListeners();
137
+ }
138
+
139
+ _prune(now) {
140
+ const cutoff = now - this.windowMs;
141
+ while (this.timestamps.length > 0 && this.timestamps[0] < cutoff) {
142
+ this.timestamps.shift();
143
+ }
144
+ }
145
+ }
146
+
147
+ module.exports = { VelocityEstimator, TREND };
@@ -0,0 +1,394 @@
1
+ /**
2
+ * BYAN staging core — extract / filter / dedup / queue / flush conversation
3
+ * knowledge from any supported CLI (claude-code, copilot-cli, codex) to a
4
+ * byan_web instance via POST /api/memory.
5
+ *
6
+ * Usage from a Claude Code Stop hook :
7
+ * const { processTurn } = require('./staging');
8
+ * await processTurn({ turn, cliSource: 'claude-code', config, projectRoot });
9
+ *
10
+ * Usage from a Copilot CLI extension.mjs :
11
+ * import { processTurn } from '<repo>/src/staging/staging.js';
12
+ * await processTurn({ turn, cliSource: 'copilot-cli', config, projectRoot });
13
+ *
14
+ * Contract :
15
+ * - processTurn() is idempotent (dedup by content hash)
16
+ * - never throws — errors go to the retry queue
17
+ * - if enabled=false, it's a pure no-op (zero bytes sent)
18
+ */
19
+
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+ const crypto = require('crypto');
23
+ const { execSync } = require('child_process');
24
+
25
+ const QUEUE_FILENAME = 'staging-queue.jsonl';
26
+ const SEEN_FILENAME = 'staging-seen.json';
27
+ const STAGING_DIR = path.join('_byan-output', 'staging');
28
+
29
+ // Patterns considered "chit-chat" — skipped by the triage filter.
30
+ const CHITCHAT_PATTERNS = [
31
+ /^(hi|hello|ok|thanks|merci|salut|bye|lol|yep|nope)[!. ]*$/i,
32
+ /^(y|yes|n|no|go|stop)$/i,
33
+ ];
34
+
35
+ const MIN_CONTENT_CHARS = 50;
36
+
37
+ function resolveRoot(projectRoot) {
38
+ return projectRoot || process.env.CLAUDE_PROJECT_DIR || process.env.BYAN_PROJECT_ROOT || process.cwd();
39
+ }
40
+
41
+ function stagingDir(projectRoot) {
42
+ return path.join(resolveRoot(projectRoot), STAGING_DIR);
43
+ }
44
+
45
+ function queuePath(projectRoot) {
46
+ return path.join(stagingDir(projectRoot), QUEUE_FILENAME);
47
+ }
48
+
49
+ function seenPath(projectRoot) {
50
+ return path.join(stagingDir(projectRoot), SEEN_FILENAME);
51
+ }
52
+
53
+ function ensureDir(dir) {
54
+ fs.mkdirSync(dir, { recursive: true });
55
+ }
56
+
57
+ function sha256(s) {
58
+ return crypto.createHash('sha256').update(String(s)).digest('hex');
59
+ }
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // Enablement + config
63
+ // ---------------------------------------------------------------------------
64
+
65
+ function isEnabled(config) {
66
+ if (!config || typeof config !== 'object') return false;
67
+ const ms = config.memory_sync || config.memorySync;
68
+ if (!ms) return false;
69
+ return ms.enabled === true;
70
+ }
71
+
72
+ function apiUrl(config) {
73
+ if (!config) return null;
74
+ return config.byan_api_url || config.BYAN_API_URL || process.env.BYAN_API_URL || null;
75
+ }
76
+
77
+ function apiToken(config) {
78
+ if (!config) return process.env.BYAN_API_TOKEN || null;
79
+ return config.byan_api_token || config.BYAN_API_TOKEN || process.env.BYAN_API_TOKEN || null;
80
+ }
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // Extract — normalize a turn payload into a memory entry draft
84
+ // ---------------------------------------------------------------------------
85
+
86
+ function extractUserText(turn) {
87
+ if (!turn) return '';
88
+ if (typeof turn.userMessage === 'string') return turn.userMessage;
89
+ if (typeof turn.prompt === 'string') return turn.prompt;
90
+ if (Array.isArray(turn.messages)) {
91
+ const u = [...turn.messages].reverse().find((m) => m && m.role === 'user');
92
+ if (u && typeof u.content === 'string') return u.content;
93
+ }
94
+ return '';
95
+ }
96
+
97
+ function extractAssistantText(turn) {
98
+ if (!turn) return '';
99
+ if (typeof turn.assistantMessage === 'string') return turn.assistantMessage;
100
+ if (Array.isArray(turn.messages)) {
101
+ const a = [...turn.messages].reverse().find((m) => m && m.role === 'assistant');
102
+ if (a) {
103
+ if (typeof a.content === 'string') return a.content;
104
+ if (Array.isArray(a.content)) {
105
+ return a.content
106
+ .map((c) => (c && typeof c === 'object' && c.text ? c.text : ''))
107
+ .join(' ')
108
+ .trim();
109
+ }
110
+ }
111
+ }
112
+ return '';
113
+ }
114
+
115
+ function extractFilesTouched(turn) {
116
+ if (!turn) return [];
117
+ if (Array.isArray(turn.filesTouched)) return turn.filesTouched.filter(Boolean);
118
+ if (Array.isArray(turn.toolCalls)) {
119
+ const files = [];
120
+ for (const tc of turn.toolCalls) {
121
+ const p = tc?.input?.file_path || tc?.args?.file_path || tc?.input?.path;
122
+ if (p && typeof p === 'string') files.push(p);
123
+ }
124
+ return files;
125
+ }
126
+ return [];
127
+ }
128
+
129
+ function classify(content, turn) {
130
+ const c = String(content || '').toLowerCase();
131
+ if (/\b(decid(e|ed|ing)|choix|trade-?off|architecture)\b/i.test(c)) return 'decision';
132
+ if (/\b(bug|error|fail|broken|bloque|blocked|can't|impossible)\b/i.test(c)) return 'blocker';
133
+ const files = extractFilesTouched(turn);
134
+ if (files.length > 0) return 'artifact';
135
+ return 'fact';
136
+ }
137
+
138
+ function extract({ turn, cliSource }) {
139
+ const user = extractUserText(turn);
140
+ const assistant = extractAssistantText(turn);
141
+ const filesTouched = extractFilesTouched(turn);
142
+ const content = [user, assistant].filter(Boolean).join('\n\n').trim();
143
+
144
+ return {
145
+ cliSource: cliSource || 'unknown',
146
+ sessionId: turn?.sessionId || null,
147
+ category: classify(content, turn),
148
+ content,
149
+ metadata: {
150
+ userMessageLen: user.length,
151
+ assistantMessageLen: assistant.length,
152
+ filesTouched,
153
+ timestamp: new Date().toISOString(),
154
+ },
155
+ pinned: false,
156
+ };
157
+ }
158
+
159
+ // ---------------------------------------------------------------------------
160
+ // Filter — triage chit-chat
161
+ // ---------------------------------------------------------------------------
162
+
163
+ function shouldKeep(entry) {
164
+ if (!entry || typeof entry.content !== 'string') return false;
165
+ if (entry.content.length < MIN_CONTENT_CHARS) return false;
166
+
167
+ const trimmed = entry.content.trim();
168
+ for (const re of CHITCHAT_PATTERNS) {
169
+ if (re.test(trimmed)) return false;
170
+ }
171
+
172
+ // Must have at least one of : files touched, substantive content, or decision keywords
173
+ if (entry.metadata && Array.isArray(entry.metadata.filesTouched) && entry.metadata.filesTouched.length > 0) {
174
+ return true;
175
+ }
176
+ // Otherwise require reasonable content length
177
+ return trimmed.length >= MIN_CONTENT_CHARS * 2;
178
+ }
179
+
180
+ // ---------------------------------------------------------------------------
181
+ // Dedup — hash-based, persisted
182
+ // ---------------------------------------------------------------------------
183
+
184
+ function readSeen(projectRoot) {
185
+ const p = seenPath(projectRoot);
186
+ if (!fs.existsSync(p)) return { hashes: [] };
187
+ try {
188
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
189
+ } catch {
190
+ return { hashes: [] };
191
+ }
192
+ }
193
+
194
+ function writeSeen(projectRoot, seen) {
195
+ ensureDir(stagingDir(projectRoot));
196
+ // Keep only last 500 hashes to cap disk use
197
+ const trimmed = { hashes: seen.hashes.slice(-500) };
198
+ fs.writeFileSync(seenPath(projectRoot), JSON.stringify(trimmed));
199
+ }
200
+
201
+ function isDuplicate(entry, projectRoot) {
202
+ const h = sha256(entry.content);
203
+ const seen = readSeen(projectRoot);
204
+ return seen.hashes.includes(h);
205
+ }
206
+
207
+ function markSeen(entry, projectRoot) {
208
+ const h = sha256(entry.content);
209
+ const seen = readSeen(projectRoot);
210
+ if (!seen.hashes.includes(h)) {
211
+ seen.hashes.push(h);
212
+ writeSeen(projectRoot, seen);
213
+ }
214
+ }
215
+
216
+ // ---------------------------------------------------------------------------
217
+ // Queue — local append-only, flushed by flush()
218
+ // ---------------------------------------------------------------------------
219
+
220
+ function enqueue(entry, projectRoot) {
221
+ ensureDir(stagingDir(projectRoot));
222
+ const p = queuePath(projectRoot);
223
+ const line = JSON.stringify({
224
+ ...entry,
225
+ enqueued_at: new Date().toISOString(),
226
+ attempts: 0,
227
+ });
228
+ fs.appendFileSync(p, line + '\n');
229
+ }
230
+
231
+ function readQueue(projectRoot) {
232
+ const p = queuePath(projectRoot);
233
+ if (!fs.existsSync(p)) return [];
234
+ return fs
235
+ .readFileSync(p, 'utf8')
236
+ .split('\n')
237
+ .filter(Boolean)
238
+ .map((line) => {
239
+ try {
240
+ return JSON.parse(line);
241
+ } catch {
242
+ return null;
243
+ }
244
+ })
245
+ .filter(Boolean);
246
+ }
247
+
248
+ function writeQueue(projectRoot, entries) {
249
+ const p = queuePath(projectRoot);
250
+ if (entries.length === 0) {
251
+ if (fs.existsSync(p)) fs.unlinkSync(p);
252
+ return;
253
+ }
254
+ fs.writeFileSync(
255
+ p,
256
+ entries.map((e) => JSON.stringify(e)).join('\n') + '\n'
257
+ );
258
+ }
259
+
260
+ // ---------------------------------------------------------------------------
261
+ // Project ID — derived from git remote or cwd
262
+ // ---------------------------------------------------------------------------
263
+
264
+ function detectProjectId(projectRoot) {
265
+ const root = resolveRoot(projectRoot);
266
+ try {
267
+ const url = execSync('git remote get-url origin', {
268
+ cwd: root,
269
+ stdio: ['ignore', 'pipe', 'ignore'],
270
+ encoding: 'utf8',
271
+ }).trim();
272
+ if (url) return sha256(url).slice(0, 16);
273
+ } catch {
274
+ // no git remote, fall through
275
+ }
276
+ return sha256(root).slice(0, 16);
277
+ }
278
+
279
+ // ---------------------------------------------------------------------------
280
+ // Flush — POST queued entries to /api/memory with retry
281
+ // ---------------------------------------------------------------------------
282
+
283
+ async function postEntry({ entry, url, token, projectId }) {
284
+ const body = {
285
+ projectId,
286
+ sessionId: entry.sessionId,
287
+ cliSource: entry.cliSource,
288
+ category: entry.category,
289
+ content: entry.content,
290
+ metadata: entry.metadata,
291
+ pinned: entry.pinned === true,
292
+ };
293
+ const res = await fetch(`${url.replace(/\/$/, '')}/api/memory`, {
294
+ method: 'POST',
295
+ headers: {
296
+ 'Content-Type': 'application/json',
297
+ Authorization: `Bearer ${token}`,
298
+ },
299
+ body: JSON.stringify(body),
300
+ });
301
+ if (!res.ok) {
302
+ const text = await res.text().catch(() => '');
303
+ const err = new Error(`HTTP ${res.status}: ${text.slice(0, 200)}`);
304
+ err.status = res.status;
305
+ throw err;
306
+ }
307
+ return res.json().catch(() => ({}));
308
+ }
309
+
310
+ async function flush({ config, projectRoot, maxAttempts = 5 } = {}) {
311
+ const url = apiUrl(config);
312
+ const token = apiToken(config);
313
+ if (!url || !token) {
314
+ return { flushed: 0, requeued: 0, dropped: 0, reason: 'missing url or token' };
315
+ }
316
+
317
+ const projectId = detectProjectId(projectRoot);
318
+ const queue = readQueue(projectRoot);
319
+ if (queue.length === 0) return { flushed: 0, requeued: 0, dropped: 0 };
320
+
321
+ let flushed = 0;
322
+ const remaining = [];
323
+ let dropped = 0;
324
+
325
+ for (const entry of queue) {
326
+ try {
327
+ await postEntry({ entry, url, token, projectId });
328
+ flushed += 1;
329
+ } catch (err) {
330
+ const attempts = (entry.attempts || 0) + 1;
331
+ if (attempts >= maxAttempts) {
332
+ dropped += 1;
333
+ continue;
334
+ }
335
+ remaining.push({ ...entry, attempts, last_error: err.message });
336
+ }
337
+ }
338
+
339
+ writeQueue(projectRoot, remaining);
340
+
341
+ return { flushed, requeued: remaining.length, dropped };
342
+ }
343
+
344
+ // ---------------------------------------------------------------------------
345
+ // Orchestration — the single entry point used by both hooks/extensions
346
+ // ---------------------------------------------------------------------------
347
+
348
+ async function processTurn({ turn, cliSource, config, projectRoot, flushNow = true } = {}) {
349
+ if (!isEnabled(config)) {
350
+ return { skipped: 'disabled' };
351
+ }
352
+
353
+ const entry = extract({ turn, cliSource });
354
+
355
+ if (!shouldKeep(entry)) {
356
+ return { skipped: 'filtered', category: entry.category };
357
+ }
358
+
359
+ if (isDuplicate(entry, projectRoot)) {
360
+ return { skipped: 'duplicate', category: entry.category };
361
+ }
362
+
363
+ enqueue(entry, projectRoot);
364
+ markSeen(entry, projectRoot);
365
+
366
+ if (!flushNow) {
367
+ return { queued: true, flushed: 0, category: entry.category };
368
+ }
369
+
370
+ const result = await flush({ config, projectRoot });
371
+ return { queued: true, ...result, category: entry.category };
372
+ }
373
+
374
+ module.exports = {
375
+ processTurn,
376
+ extract,
377
+ shouldKeep,
378
+ isEnabled,
379
+ isDuplicate,
380
+ markSeen,
381
+ enqueue,
382
+ readQueue,
383
+ writeQueue,
384
+ flush,
385
+ detectProjectId,
386
+ sha256,
387
+ classify,
388
+ queuePath,
389
+ seenPath,
390
+ STAGING_DIR,
391
+ QUEUE_FILENAME,
392
+ SEEN_FILENAME,
393
+ MIN_CONTENT_CHARS,
394
+ };
@@ -146,13 +146,38 @@ program
146
146
  });
147
147
 
148
148
  // Copy _byan from node_modules to project root
149
- const nodeModulesByan = path.join(installPath, 'node_modules', 'create-byan-agent', '_byan');
149
+ const pkgRoot = path.join(installPath, 'node_modules', 'create-byan-agent');
150
+ const nodeModulesByan = path.join(pkgRoot, '_byan');
150
151
  if (fs.existsSync(nodeModulesByan)) {
151
152
  copyRecursive(nodeModulesByan, byanDir);
152
153
  } else {
153
154
  throw new Error('_byan directory not found in npm package');
154
155
  }
155
-
156
+
157
+ // Also refresh .github/agents/ from templates (Copilot stubs)
158
+ const ghAgentsSrc = path.join(pkgRoot, 'install', 'templates', '.github', 'agents');
159
+ const ghAgentsDst = path.join(installPath, '.github', 'agents');
160
+ if (fs.existsSync(ghAgentsSrc)) {
161
+ if (fs.existsSync(ghAgentsDst)) {
162
+ fs.rmSync(ghAgentsDst, { recursive: true, force: true });
163
+ }
164
+ fs.mkdirSync(path.dirname(ghAgentsDst), { recursive: true });
165
+ copyRecursive(ghAgentsSrc, ghAgentsDst);
166
+ }
167
+
168
+ // Refresh Claude Code native (.claude/hooks, .claude/skills,
169
+ // .claude/agents, .claude/settings.json, .mcp.json, _byan/mcp/)
170
+ try {
171
+ const setupModule = path.join(pkgRoot, 'install', 'lib', 'claude-native-setup.js');
172
+ if (fs.existsSync(setupModule)) {
173
+ // eslint-disable-next-line import/no-dynamic-require, global-require
174
+ const { setupClaudeNative } = require(setupModule);
175
+ await setupClaudeNative(installPath, { installDeps: true, quiet: false });
176
+ }
177
+ } catch (e) {
178
+ console.warn(chalk.yellow(` ⚠ Claude native refresh skipped: ${e.message}`));
179
+ }
180
+
156
181
  updateSpinner.succeed('Derniere version installee');
157
182
  } catch (error) {
158
183
  updateSpinner.fail('Erreur installation');