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.
- package/install/bin/byan-cleanup.js +156 -0
- package/install/bin/byan-kanban.js +159 -0
- package/install/bin/byan-ledger.js +45 -0
- package/install/bin/create-byan-agent-v2.js +15 -1
- package/install/lib/cleanup/detector.js +154 -0
- package/install/lib/cleanup/executor.js +72 -0
- package/install/lib/staging-consent.js +149 -0
- package/install/lib/subagent-generator.js +208 -0
- package/install/lib/token-ledger.js +131 -0
- package/install/templates/.claude/agents/bmad-bmad-master.md +14 -0
- package/install/templates/.claude/agents/bmad-bmb-agent-builder.md +14 -0
- package/install/templates/.claude/agents/bmad-bmb-module-builder.md +14 -0
- package/install/templates/.claude/agents/bmad-bmb-workflow-builder.md +14 -0
- package/install/templates/.claude/agents/bmad-bmm-analyst.md +14 -0
- package/install/templates/.claude/agents/bmad-bmm-architect.md +14 -0
- package/install/templates/.claude/agents/bmad-bmm-dev.md +14 -0
- package/install/templates/.claude/agents/bmad-bmm-pm.md +14 -0
- package/install/templates/.claude/agents/bmad-bmm-quick-flow-solo-dev.md +14 -0
- package/install/templates/.claude/agents/bmad-bmm-quinn.md +14 -0
- package/install/templates/.claude/agents/bmad-bmm-sm.md +14 -0
- package/install/templates/.claude/agents/bmad-bmm-tech-writer.md +14 -0
- package/install/templates/.claude/agents/bmad-bmm-ux-designer.md +14 -0
- package/install/templates/.claude/agents/bmad-byan-v2.md +14 -0
- package/install/templates/.claude/agents/bmad-byan.md +152 -0
- package/install/templates/.claude/agents/bmad-carmack.md +14 -0
- package/install/templates/.claude/agents/bmad-cis-brainstorming-coach.md +14 -0
- package/install/templates/.claude/agents/bmad-cis-creative-problem-solver.md +14 -0
- package/install/templates/.claude/agents/bmad-cis-design-thinking-coach.md +14 -0
- package/install/templates/.claude/agents/bmad-cis-innovation-strategist.md +14 -0
- package/install/templates/.claude/agents/bmad-cis-presentation-master.md +14 -0
- package/install/templates/.claude/agents/bmad-cis-storyteller.md +14 -0
- package/install/templates/.claude/agents/bmad-claude.md +26 -0
- package/install/templates/.claude/agents/bmad-codex.md +26 -0
- package/install/templates/.claude/agents/bmad-compliance.md +68 -0
- package/install/templates/.claude/agents/bmad-drawio.md +25 -0
- package/install/templates/.claude/agents/bmad-expert-merise-agile.md +54 -0
- package/install/templates/.claude/agents/bmad-fact-checker.md +14 -0
- package/install/templates/.claude/agents/bmad-forgeron.md +14 -0
- package/install/templates/.claude/agents/bmad-hermes.md +59 -0
- package/install/templates/.claude/agents/bmad-marc.md +25 -0
- package/install/templates/.claude/agents/bmad-patnote.md +26 -0
- package/install/templates/.claude/agents/bmad-rachid.md +25 -0
- package/install/templates/.claude/agents/bmad-tao.md +14 -0
- package/install/templates/.claude/agents/bmad-tea-tea.md +14 -0
- package/install/templates/.claude/agents/bmad-yanstaller.md +47 -0
- package/install/templates/.claude/hooks/fact-check-absolutes.js +185 -0
- package/install/templates/.claude/hooks/fd-phase-guard.js +87 -0
- package/install/templates/.claude/hooks/fd-response-check.js +92 -0
- package/install/templates/.claude/hooks/lib/failure-detector.js +14 -0
- package/install/templates/.claude/hooks/pre-compact-save.js +148 -0
- package/install/templates/.claude/hooks/stage-to-byan.js +119 -0
- package/install/templates/.claude/hooks/tool-failure-guard.js +6 -0
- package/install/templates/.claude/hooks/tool-transparency.js +4 -0
- package/install/templates/.claude/settings.json +27 -0
- package/install/templates/.claude/skills/byan-byan/SKILL.md +115 -163
- package/install/templates/.claude/skills/byan-orchestrate/SKILL.md +100 -0
- package/install/templates/.githooks/pre-commit +75 -0
- package/install/templates/.github/extensions/byan-staging/extension.mjs +169 -0
- package/install/templates/.github/extensions/byan-staging/package.json +8 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/copilot.js +148 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/fd-state.js +163 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/kanban.js +226 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/peer-review.js +187 -0
- package/install/templates/_byan/mcp/byan-mcp-server/server.js +463 -0
- package/install/templates/detector.js +154 -0
- package/package.json +6 -7
- package/src/loadbalancer/capability-matrix.js +157 -0
- package/src/loadbalancer/config.js +141 -0
- package/src/loadbalancer/graceful-degradation.js +212 -0
- package/src/loadbalancer/health-probe.js +151 -0
- package/src/loadbalancer/hooks/claude-hooks.js +53 -0
- package/src/loadbalancer/hooks/copilot-hooks.js +74 -0
- package/src/loadbalancer/index.js +81 -0
- package/src/loadbalancer/loadbalancer.default.yaml +65 -0
- package/src/loadbalancer/loadbalancer.js +324 -0
- package/src/loadbalancer/mcp-server.js +304 -0
- package/src/loadbalancer/metrics.js +146 -0
- package/src/loadbalancer/native/claude-integration.js +64 -0
- package/src/loadbalancer/native/copilot-integration.js +59 -0
- package/src/loadbalancer/pressure-score.js +102 -0
- package/src/loadbalancer/providers/base-provider.js +80 -0
- package/src/loadbalancer/providers/byan-api-provider.js +132 -0
- package/src/loadbalancer/providers/claude-provider.js +113 -0
- package/src/loadbalancer/providers/copilot-provider.js +104 -0
- package/src/loadbalancer/rate-limit-tracker.js +216 -0
- package/src/loadbalancer/session-bridge.js +179 -0
- package/src/loadbalancer/state/db.js +211 -0
- package/src/loadbalancer/state/migrations/001-initial.sql +50 -0
- package/src/loadbalancer/tools/index.js +123 -0
- package/src/loadbalancer/velocity-estimator.js +147 -0
- package/src/staging/staging.js +394 -0
- package/update-byan-agent/bin/update-byan-agent.js +27 -2
- package/API-BYAN-V2.md +0 -741
- package/BMAD-QUICK-REFERENCE.md +0 -370
- package/CHANGELOG-v2.1.0.md +0 -371
- package/MIGRATION-v2.0-to-v2.1.md +0 -430
- package/README-BYAN-V2.md +0 -446
- 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
|
|
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');
|