create-byan-agent 2.9.4 → 2.9.5

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 (92) 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/lib/cleanup/detector.js +154 -0
  5. package/install/lib/cleanup/executor.js +72 -0
  6. package/install/lib/subagent-generator.js +208 -0
  7. package/install/lib/token-ledger.js +131 -0
  8. package/install/templates/.claude/agents/bmad-bmad-master.md +14 -0
  9. package/install/templates/.claude/agents/bmad-bmb-agent-builder.md +14 -0
  10. package/install/templates/.claude/agents/bmad-bmb-module-builder.md +14 -0
  11. package/install/templates/.claude/agents/bmad-bmb-workflow-builder.md +14 -0
  12. package/install/templates/.claude/agents/bmad-bmm-analyst.md +14 -0
  13. package/install/templates/.claude/agents/bmad-bmm-architect.md +14 -0
  14. package/install/templates/.claude/agents/bmad-bmm-dev.md +14 -0
  15. package/install/templates/.claude/agents/bmad-bmm-pm.md +14 -0
  16. package/install/templates/.claude/agents/bmad-bmm-quick-flow-solo-dev.md +14 -0
  17. package/install/templates/.claude/agents/bmad-bmm-quinn.md +14 -0
  18. package/install/templates/.claude/agents/bmad-bmm-sm.md +14 -0
  19. package/install/templates/.claude/agents/bmad-bmm-tech-writer.md +14 -0
  20. package/install/templates/.claude/agents/bmad-bmm-ux-designer.md +14 -0
  21. package/install/templates/.claude/agents/bmad-byan-v2.md +14 -0
  22. package/install/templates/.claude/agents/bmad-byan.md +152 -0
  23. package/install/templates/.claude/agents/bmad-carmack.md +14 -0
  24. package/install/templates/.claude/agents/bmad-cis-brainstorming-coach.md +14 -0
  25. package/install/templates/.claude/agents/bmad-cis-creative-problem-solver.md +14 -0
  26. package/install/templates/.claude/agents/bmad-cis-design-thinking-coach.md +14 -0
  27. package/install/templates/.claude/agents/bmad-cis-innovation-strategist.md +14 -0
  28. package/install/templates/.claude/agents/bmad-cis-presentation-master.md +14 -0
  29. package/install/templates/.claude/agents/bmad-cis-storyteller.md +14 -0
  30. package/install/templates/.claude/agents/bmad-claude.md +26 -0
  31. package/install/templates/.claude/agents/bmad-codex.md +26 -0
  32. package/install/templates/.claude/agents/bmad-compliance.md +68 -0
  33. package/install/templates/.claude/agents/bmad-drawio.md +25 -0
  34. package/install/templates/.claude/agents/bmad-expert-merise-agile.md +54 -0
  35. package/install/templates/.claude/agents/bmad-fact-checker.md +14 -0
  36. package/install/templates/.claude/agents/bmad-forgeron.md +14 -0
  37. package/install/templates/.claude/agents/bmad-hermes.md +59 -0
  38. package/install/templates/.claude/agents/bmad-marc.md +25 -0
  39. package/install/templates/.claude/agents/bmad-patnote.md +26 -0
  40. package/install/templates/.claude/agents/bmad-rachid.md +25 -0
  41. package/install/templates/.claude/agents/bmad-tao.md +14 -0
  42. package/install/templates/.claude/agents/bmad-tea-tea.md +14 -0
  43. package/install/templates/.claude/agents/bmad-yanstaller.md +47 -0
  44. package/install/templates/.claude/hooks/fact-check-absolutes.js +185 -0
  45. package/install/templates/.claude/hooks/fd-phase-guard.js +87 -0
  46. package/install/templates/.claude/hooks/fd-response-check.js +92 -0
  47. package/install/templates/.claude/hooks/lib/failure-detector.js +14 -0
  48. package/install/templates/.claude/hooks/pre-compact-save.js +148 -0
  49. package/install/templates/.claude/hooks/tool-failure-guard.js +6 -0
  50. package/install/templates/.claude/hooks/tool-transparency.js +4 -0
  51. package/install/templates/.claude/settings.json +23 -0
  52. package/install/templates/.claude/skills/byan-byan/SKILL.md +115 -163
  53. package/install/templates/.claude/skills/byan-orchestrate/SKILL.md +100 -0
  54. package/install/templates/.githooks/pre-commit +75 -0
  55. package/install/templates/_byan/mcp/byan-mcp-server/lib/copilot.js +148 -0
  56. package/install/templates/_byan/mcp/byan-mcp-server/lib/fd-state.js +163 -0
  57. package/install/templates/_byan/mcp/byan-mcp-server/lib/kanban.js +226 -0
  58. package/install/templates/_byan/mcp/byan-mcp-server/lib/peer-review.js +187 -0
  59. package/install/templates/_byan/mcp/byan-mcp-server/server.js +463 -0
  60. package/install/templates/detector.js +154 -0
  61. package/package.json +6 -7
  62. package/src/loadbalancer/capability-matrix.js +157 -0
  63. package/src/loadbalancer/config.js +141 -0
  64. package/src/loadbalancer/graceful-degradation.js +212 -0
  65. package/src/loadbalancer/health-probe.js +151 -0
  66. package/src/loadbalancer/hooks/claude-hooks.js +53 -0
  67. package/src/loadbalancer/hooks/copilot-hooks.js +74 -0
  68. package/src/loadbalancer/index.js +81 -0
  69. package/src/loadbalancer/loadbalancer.default.yaml +65 -0
  70. package/src/loadbalancer/loadbalancer.js +324 -0
  71. package/src/loadbalancer/mcp-server.js +304 -0
  72. package/src/loadbalancer/metrics.js +146 -0
  73. package/src/loadbalancer/native/claude-integration.js +64 -0
  74. package/src/loadbalancer/native/copilot-integration.js +59 -0
  75. package/src/loadbalancer/pressure-score.js +102 -0
  76. package/src/loadbalancer/providers/base-provider.js +80 -0
  77. package/src/loadbalancer/providers/byan-api-provider.js +132 -0
  78. package/src/loadbalancer/providers/claude-provider.js +113 -0
  79. package/src/loadbalancer/providers/copilot-provider.js +104 -0
  80. package/src/loadbalancer/rate-limit-tracker.js +216 -0
  81. package/src/loadbalancer/session-bridge.js +179 -0
  82. package/src/loadbalancer/state/db.js +211 -0
  83. package/src/loadbalancer/state/migrations/001-initial.sql +50 -0
  84. package/src/loadbalancer/tools/index.js +123 -0
  85. package/src/loadbalancer/velocity-estimator.js +147 -0
  86. package/update-byan-agent/bin/update-byan-agent.js +27 -2
  87. package/API-BYAN-V2.md +0 -741
  88. package/BMAD-QUICK-REFERENCE.md +0 -370
  89. package/CHANGELOG-v2.1.0.md +0 -371
  90. package/MIGRATION-v2.0-to-v2.1.md +0 -430
  91. package/README-BYAN-V2.md +0 -446
  92. package/TEST-GUIDE-v2.3.2.md +0 -161
@@ -0,0 +1,179 @@
1
+ /**
2
+ * SessionBridge — Cross-Provider Context Transfer
3
+ *
4
+ * Serializes conversation state from one provider into a provider-agnostic
5
+ * format, then injects it as context into the target provider.
6
+ *
7
+ * Strategy:
8
+ * 1. Extract: conversation messages + files touched + decisions made
9
+ * 2. Summarize: if transcript > max_tokens, use LLM to compress
10
+ * 3. Package: provider-agnostic JSON (PortableContext)
11
+ * 4. Inject: as system prompt prefix on target provider
12
+ */
13
+
14
+ /**
15
+ * @typedef {object} PortableContext
16
+ * @property {string} conversationId
17
+ * @property {string} sourceProvider
18
+ * @property {string} targetProvider
19
+ * @property {string} summary - Concise conversation summary
20
+ * @property {string[]} filesTouched - Files read/modified
21
+ * @property {string[]} decisions - Key decisions made
22
+ * @property {string} currentTask - What was being worked on
23
+ * @property {object} agentState - BYAN agent state (soul, tao, FD phase, etc.)
24
+ * @property {number} fidelityPct - Estimated context preservation %
25
+ * @property {string} transferredAt - ISO timestamp
26
+ */
27
+
28
+ class SessionBridge {
29
+ /**
30
+ * @param {object} opts
31
+ * @param {import('./state/db').SharedStateStore} opts.store
32
+ * @param {number} [opts.maxTokens] - Max summary tokens (default: 2000)
33
+ */
34
+ constructor(opts = {}) {
35
+ this.store = opts.store;
36
+ this.maxTokens = opts.maxTokens || 2000;
37
+ }
38
+
39
+ /**
40
+ * Extract context from source provider's session.
41
+ * @param {object} provider - ProviderAdapter instance
42
+ * @param {string} conversationId
43
+ * @returns {Promise<PortableContext>}
44
+ */
45
+ async extract(provider, conversationId) {
46
+ const conversation = this.store?.getConversation(conversationId);
47
+ const existingContext = conversation?.context_json
48
+ ? JSON.parse(conversation.context_json)
49
+ : {};
50
+
51
+ const switchHistory = this.store?.getSwitchoversForConversation(conversationId) || [];
52
+
53
+ return {
54
+ conversationId,
55
+ sourceProvider: provider.name,
56
+ targetProvider: null,
57
+ summary: existingContext.summary || 'No conversation summary available.',
58
+ filesTouched: existingContext.filesTouched || [],
59
+ decisions: existingContext.decisions || [],
60
+ currentTask: existingContext.currentTask || 'Unknown task',
61
+ agentState: existingContext.agentState || {},
62
+ switchHistory: switchHistory.map(s => ({
63
+ from: s.from_provider,
64
+ to: s.to_provider,
65
+ reason: s.reason,
66
+ at: s.switched_at,
67
+ })),
68
+ fidelityPct: existingContext.summary ? 85 : 50,
69
+ transferredAt: new Date().toISOString(),
70
+ };
71
+ }
72
+
73
+ /**
74
+ * Update the stored context for a conversation.
75
+ * Called periodically during normal operation to keep context fresh.
76
+ * @param {string} conversationId
77
+ * @param {object} contextUpdate - Partial context to merge
78
+ */
79
+ updateContext(conversationId, contextUpdate) {
80
+ if (!this.store) return;
81
+
82
+ const conversation = this.store.getConversation(conversationId);
83
+ if (!conversation) return;
84
+
85
+ const existing = conversation.context_json
86
+ ? JSON.parse(conversation.context_json)
87
+ : {};
88
+
89
+ const merged = {
90
+ ...existing,
91
+ ...contextUpdate,
92
+ filesTouched: [
93
+ ...new Set([...(existing.filesTouched || []), ...(contextUpdate.filesTouched || [])]),
94
+ ],
95
+ decisions: [
96
+ ...(existing.decisions || []),
97
+ ...(contextUpdate.decisions || []),
98
+ ],
99
+ updatedAt: new Date().toISOString(),
100
+ };
101
+
102
+ this.store.updateConversation(conversationId, {
103
+ context_json: JSON.stringify(merged),
104
+ });
105
+ }
106
+
107
+ /**
108
+ * Format a PortableContext into a system prompt for the target provider.
109
+ * @param {PortableContext} ctx
110
+ * @returns {string} - Formatted context injection prompt
111
+ */
112
+ formatInjectionPrompt(ctx) {
113
+ const lines = [
114
+ '--- CONTEXT TRANSFER ---',
115
+ `Transferred from: ${ctx.sourceProvider}`,
116
+ `Conversation: ${ctx.conversationId}`,
117
+ `Fidelity: ~${ctx.fidelityPct}%`,
118
+ '',
119
+ '## Summary',
120
+ ctx.summary,
121
+ ];
122
+
123
+ if (ctx.currentTask) {
124
+ lines.push('', '## Current Task', ctx.currentTask);
125
+ }
126
+
127
+ if (ctx.filesTouched.length > 0) {
128
+ lines.push('', '## Files Touched', ...ctx.filesTouched.map(f => `- ${f}`));
129
+ }
130
+
131
+ if (ctx.decisions.length > 0) {
132
+ lines.push('', '## Key Decisions', ...ctx.decisions.map(d => `- ${d}`));
133
+ }
134
+
135
+ if (ctx.agentState && Object.keys(ctx.agentState).length > 0) {
136
+ lines.push('', '## Agent State', JSON.stringify(ctx.agentState, null, 2));
137
+ }
138
+
139
+ if (ctx.switchHistory && ctx.switchHistory.length > 0) {
140
+ lines.push(
141
+ '', '## Switch History',
142
+ ...ctx.switchHistory.map(s => `- ${s.from} -> ${s.to} (${s.reason}) at ${s.at}`)
143
+ );
144
+ }
145
+
146
+ lines.push('', '--- END CONTEXT TRANSFER ---');
147
+ return lines.join('\n');
148
+ }
149
+
150
+ /**
151
+ * Full transfer: extract from source, record switchover, format for target.
152
+ * @param {object} sourceProvider
153
+ * @param {string} targetProviderName
154
+ * @param {string} conversationId
155
+ * @param {string} reason
156
+ * @returns {Promise<{context: PortableContext, injectionPrompt: string}>}
157
+ */
158
+ async transfer(sourceProvider, targetProviderName, conversationId, reason) {
159
+ const context = await this.extract(sourceProvider, conversationId);
160
+ context.targetProvider = targetProviderName;
161
+
162
+ if (this.store) {
163
+ this.store.recordSwitchover({
164
+ conversationId,
165
+ from: sourceProvider.name,
166
+ to: targetProviderName,
167
+ reason,
168
+ contextSnapshot: JSON.stringify(context),
169
+ fidelityPct: context.fidelityPct,
170
+ });
171
+ }
172
+
173
+ const injectionPrompt = this.formatInjectionPrompt(context);
174
+
175
+ return { context, injectionPrompt };
176
+ }
177
+ }
178
+
179
+ module.exports = { SessionBridge };
@@ -0,0 +1,211 @@
1
+ /**
2
+ * SharedStateStore — SQLite-backed state for LoadBalancer
3
+ *
4
+ * Manages conversations, switchover events, rate limit logs, and
5
+ * provider statistics. Shared between providers via MCP tools.
6
+ *
7
+ * Uses better-sqlite3 for synchronous, fast access.
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+
13
+ const MIGRATION_DIR = path.join(__dirname, 'migrations');
14
+
15
+ class SharedStateStore {
16
+ /**
17
+ * @param {string} dbPath - Path to SQLite database file
18
+ */
19
+ constructor(dbPath) {
20
+ this.dbPath = dbPath;
21
+ this.db = null;
22
+ }
23
+
24
+ /**
25
+ * Initialize database and run migrations.
26
+ */
27
+ open() {
28
+ let Database;
29
+ try {
30
+ Database = require('better-sqlite3');
31
+ } catch {
32
+ throw new Error('better-sqlite3 required for SharedStateStore. Run: npm install better-sqlite3');
33
+ }
34
+
35
+ const dir = path.dirname(this.dbPath);
36
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
37
+
38
+ this.db = new Database(this.dbPath);
39
+ this.db.pragma('journal_mode = WAL');
40
+ this.db.pragma('foreign_keys = ON');
41
+
42
+ this._runMigrations();
43
+ return this;
44
+ }
45
+
46
+ _runMigrations() {
47
+ const files = fs.readdirSync(MIGRATION_DIR)
48
+ .filter(f => f.endsWith('.sql'))
49
+ .sort();
50
+
51
+ for (const file of files) {
52
+ const sql = fs.readFileSync(path.join(MIGRATION_DIR, file), 'utf8');
53
+ this.db.exec(sql);
54
+ }
55
+ }
56
+
57
+ // --- Conversations ---
58
+
59
+ createConversation(id, provider) {
60
+ const stmt = this.db.prepare(
61
+ 'INSERT INTO lb_conversations (id, active_provider) VALUES (?, ?)'
62
+ );
63
+ stmt.run(id, provider);
64
+ return this.getConversation(id);
65
+ }
66
+
67
+ getConversation(id) {
68
+ return this.db.prepare('SELECT * FROM lb_conversations WHERE id = ?').get(id);
69
+ }
70
+
71
+ getActiveConversations() {
72
+ return this.db.prepare(
73
+ "SELECT * FROM lb_conversations WHERE status = 'active' ORDER BY updated_at DESC"
74
+ ).all();
75
+ }
76
+
77
+ updateConversation(id, fields) {
78
+ const allowed = ['active_provider', 'context_json', 'status'];
79
+ const sets = [];
80
+ const values = [];
81
+
82
+ for (const [key, val] of Object.entries(fields)) {
83
+ if (!allowed.includes(key)) continue;
84
+ sets.push(`${key} = ?`);
85
+ values.push(val);
86
+ }
87
+
88
+ if (sets.length === 0) return;
89
+
90
+ sets.push("updated_at = datetime('now')");
91
+ values.push(id);
92
+
93
+ this.db.prepare(
94
+ `UPDATE lb_conversations SET ${sets.join(', ')} WHERE id = ?`
95
+ ).run(...values);
96
+ }
97
+
98
+ // --- Switchovers ---
99
+
100
+ recordSwitchover({ conversationId, from, to, reason, contextSnapshot, fidelityPct }) {
101
+ const stmt = this.db.prepare(`
102
+ INSERT INTO lb_switchovers (conversation_id, from_provider, to_provider, reason, context_snapshot, context_fidelity_pct)
103
+ VALUES (?, ?, ?, ?, ?, ?)
104
+ `);
105
+ const info = stmt.run(conversationId, from, to, reason, contextSnapshot || null, fidelityPct || null);
106
+
107
+ if (conversationId) {
108
+ this.updateConversation(conversationId, {
109
+ active_provider: to,
110
+ status: 'active',
111
+ });
112
+ }
113
+
114
+ this._updateProviderStat(from, { total_switchovers_from: 1 });
115
+ this._updateProviderStat(to, { total_switchovers_to: 1 });
116
+
117
+ return info.lastInsertRowid;
118
+ }
119
+
120
+ getSwitchoverHistory(limit = 20) {
121
+ return this.db.prepare(
122
+ 'SELECT * FROM lb_switchovers ORDER BY switched_at DESC LIMIT ?'
123
+ ).all(limit);
124
+ }
125
+
126
+ getSwitchoversForConversation(conversationId) {
127
+ return this.db.prepare(
128
+ 'SELECT * FROM lb_switchovers WHERE conversation_id = ? ORDER BY switched_at'
129
+ ).all(conversationId);
130
+ }
131
+
132
+ // --- Rate Limit Log ---
133
+
134
+ logRateLimit({ provider, eventType, stateBefore, stateAfter, meta }) {
135
+ const stmt = this.db.prepare(`
136
+ INSERT INTO lb_rate_limit_log (provider, event_type, state_before, state_after, meta_json)
137
+ VALUES (?, ?, ?, ?, ?)
138
+ `);
139
+ stmt.run(provider, eventType, stateBefore, stateAfter, meta ? JSON.stringify(meta) : null);
140
+
141
+ if (eventType === '429') {
142
+ this._updateProviderStat(provider, { total_429s: 1 });
143
+ }
144
+ }
145
+
146
+ getRateLimitLog(provider, limit = 50) {
147
+ if (provider) {
148
+ return this.db.prepare(
149
+ 'SELECT * FROM lb_rate_limit_log WHERE provider = ? ORDER BY logged_at DESC LIMIT ?'
150
+ ).all(provider, limit);
151
+ }
152
+ return this.db.prepare(
153
+ 'SELECT * FROM lb_rate_limit_log ORDER BY logged_at DESC LIMIT ?'
154
+ ).all(limit);
155
+ }
156
+
157
+ // --- Provider Stats ---
158
+
159
+ recordRequest(provider, latencyMs) {
160
+ this._ensureProviderStat(provider);
161
+ const current = this.db.prepare('SELECT * FROM lb_provider_stats WHERE provider = ?').get(provider);
162
+
163
+ const newTotal = current.total_requests + 1;
164
+ const newAvg = ((current.avg_latency_ms * current.total_requests) + latencyMs) / newTotal;
165
+
166
+ this.db.prepare(`
167
+ UPDATE lb_provider_stats
168
+ SET total_requests = ?, avg_latency_ms = ?, last_used_at = datetime('now'), updated_at = datetime('now')
169
+ WHERE provider = ?
170
+ `).run(newTotal, newAvg, provider);
171
+ }
172
+
173
+ getProviderStats() {
174
+ return this.db.prepare('SELECT * FROM lb_provider_stats ORDER BY provider').all();
175
+ }
176
+
177
+ _ensureProviderStat(provider) {
178
+ this.db.prepare(
179
+ 'INSERT OR IGNORE INTO lb_provider_stats (provider) VALUES (?)'
180
+ ).run(provider);
181
+ }
182
+
183
+ _updateProviderStat(provider, increments) {
184
+ this._ensureProviderStat(provider);
185
+ const sets = [];
186
+ const values = [];
187
+
188
+ for (const [key, inc] of Object.entries(increments)) {
189
+ sets.push(`${key} = ${key} + ?`);
190
+ values.push(inc);
191
+ }
192
+
193
+ sets.push("updated_at = datetime('now')");
194
+ values.push(provider);
195
+
196
+ this.db.prepare(
197
+ `UPDATE lb_provider_stats SET ${sets.join(', ')} WHERE provider = ?`
198
+ ).run(...values);
199
+ }
200
+
201
+ // --- Lifecycle ---
202
+
203
+ close() {
204
+ if (this.db) {
205
+ this.db.close();
206
+ this.db = null;
207
+ }
208
+ }
209
+ }
210
+
211
+ module.exports = { SharedStateStore };
@@ -0,0 +1,50 @@
1
+ -- LoadBalancer SharedStateStore — Initial Schema
2
+ -- Tracks conversations, switchover events, and rate limit history
3
+
4
+ CREATE TABLE IF NOT EXISTS lb_conversations (
5
+ id TEXT PRIMARY KEY,
6
+ active_provider TEXT NOT NULL,
7
+ started_at TEXT NOT NULL DEFAULT (datetime('now')),
8
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
9
+ context_json TEXT,
10
+ status TEXT NOT NULL DEFAULT 'active'
11
+ CHECK (status IN ('active', 'paused', 'completed', 'transferred'))
12
+ );
13
+
14
+ CREATE TABLE IF NOT EXISTS lb_switchovers (
15
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
16
+ conversation_id TEXT,
17
+ from_provider TEXT NOT NULL,
18
+ to_provider TEXT NOT NULL,
19
+ reason TEXT NOT NULL,
20
+ context_snapshot TEXT,
21
+ context_fidelity_pct INTEGER,
22
+ switched_at TEXT NOT NULL DEFAULT (datetime('now')),
23
+ FOREIGN KEY (conversation_id) REFERENCES lb_conversations(id)
24
+ );
25
+
26
+ CREATE TABLE IF NOT EXISTS lb_rate_limit_log (
27
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
28
+ provider TEXT NOT NULL,
29
+ event_type TEXT NOT NULL CHECK (event_type IN ('429', 'header_warning', 'retry_after', 'recovery')),
30
+ state_before TEXT NOT NULL,
31
+ state_after TEXT NOT NULL,
32
+ meta_json TEXT,
33
+ logged_at TEXT NOT NULL DEFAULT (datetime('now'))
34
+ );
35
+
36
+ CREATE TABLE IF NOT EXISTS lb_provider_stats (
37
+ provider TEXT PRIMARY KEY,
38
+ total_requests INTEGER NOT NULL DEFAULT 0,
39
+ total_429s INTEGER NOT NULL DEFAULT 0,
40
+ total_switchovers_from INTEGER NOT NULL DEFAULT 0,
41
+ total_switchovers_to INTEGER NOT NULL DEFAULT 0,
42
+ avg_latency_ms REAL NOT NULL DEFAULT 0,
43
+ last_used_at TEXT,
44
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
45
+ );
46
+
47
+ CREATE INDEX IF NOT EXISTS idx_switchovers_conversation ON lb_switchovers(conversation_id);
48
+ CREATE INDEX IF NOT EXISTS idx_switchovers_time ON lb_switchovers(switched_at);
49
+ CREATE INDEX IF NOT EXISTS idx_rate_limit_provider ON lb_rate_limit_log(provider, logged_at);
50
+ CREATE INDEX IF NOT EXISTS idx_conversations_status ON lb_conversations(status);
@@ -0,0 +1,123 @@
1
+ /**
2
+ * LoadBalancer MCP Tool Definitions
3
+ *
4
+ * Tools exposed to both Claude Code and Copilot CLI via MCP protocol.
5
+ * Each tool is a { name, description, inputSchema, handler } object.
6
+ */
7
+
8
+ function createTools(lb) {
9
+ return [
10
+ {
11
+ name: 'lb_status',
12
+ description: 'Get current loadbalancer status: active provider, rate limit states, session info.',
13
+ inputSchema: {
14
+ type: 'object',
15
+ properties: {},
16
+ },
17
+ handler: async () => {
18
+ const status = lb.getStatus();
19
+ return { content: [{ type: 'text', text: JSON.stringify(status, null, 2) }] };
20
+ },
21
+ },
22
+ {
23
+ name: 'lb_send',
24
+ description: 'Send a prompt through the loadbalancer. Routes to healthiest provider automatically.',
25
+ inputSchema: {
26
+ type: 'object',
27
+ properties: {
28
+ prompt: { type: 'string', description: 'The prompt to send' },
29
+ session_id: { type: 'string', description: 'Optional session ID for sticky routing' },
30
+ prefer_provider: { type: 'string', enum: ['claude', 'copilot', 'auto'], description: 'Provider preference (default: auto)' },
31
+ },
32
+ required: ['prompt'],
33
+ },
34
+ handler: async (args) => {
35
+ const result = await lb.send({
36
+ prompt: args.prompt,
37
+ sessionId: args.session_id,
38
+ preferProvider: args.prefer_provider || 'auto',
39
+ });
40
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
41
+ },
42
+ },
43
+ {
44
+ name: 'lb_switch',
45
+ description: 'Force switch to a specific provider. Triggers context transfer via SessionBridge.',
46
+ inputSchema: {
47
+ type: 'object',
48
+ properties: {
49
+ target_provider: { type: 'string', enum: ['claude', 'copilot'], description: 'Provider to switch to' },
50
+ session_id: { type: 'string', description: 'Session to transfer context from' },
51
+ reason: { type: 'string', description: 'Reason for the switch' },
52
+ },
53
+ required: ['target_provider'],
54
+ },
55
+ handler: async (args) => {
56
+ const result = await lb.switchProvider({
57
+ target: args.target_provider,
58
+ sessionId: args.session_id,
59
+ reason: args.reason || 'manual_switch',
60
+ });
61
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
62
+ },
63
+ },
64
+ {
65
+ name: 'lb_get_context',
66
+ description: 'Get the current session context in provider-agnostic format. Useful before manual switch.',
67
+ inputSchema: {
68
+ type: 'object',
69
+ properties: {
70
+ session_id: { type: 'string', description: 'Session to get context for' },
71
+ },
72
+ },
73
+ handler: async (args) => {
74
+ const context = await lb.getSessionContext(args.session_id);
75
+ return { content: [{ type: 'text', text: JSON.stringify(context, null, 2) }] };
76
+ },
77
+ },
78
+ {
79
+ name: 'lb_rate_limits',
80
+ description: 'Get detailed rate limit state for all providers. Shows circuit breaker state, 429 counts, reset times.',
81
+ inputSchema: {
82
+ type: 'object',
83
+ properties: {},
84
+ },
85
+ handler: async () => {
86
+ const limits = lb.getRateLimitDetails();
87
+ return { content: [{ type: 'text', text: JSON.stringify(limits, null, 2) }] };
88
+ },
89
+ },
90
+ {
91
+ name: 'lb_history',
92
+ description: 'Get switchover history. Shows when and why provider switches occurred.',
93
+ inputSchema: {
94
+ type: 'object',
95
+ properties: {
96
+ limit: { type: 'number', description: 'Max events to return (default: 20)' },
97
+ },
98
+ },
99
+ handler: async (args) => {
100
+ const history = lb.getSwitchoverHistory(args.limit || 20);
101
+ return { content: [{ type: 'text', text: JSON.stringify(history, null, 2) }] };
102
+ },
103
+ },
104
+ {
105
+ name: 'lb_quota',
106
+ description: 'Get real-time rate limit pressure per provider as percentage (0-100). Shows pressure score, velocity (req/min), trend, ETA to limit, and recommendation (ok/caution/switch_now).',
107
+ inputSchema: {
108
+ type: 'object',
109
+ properties: {},
110
+ },
111
+ handler: async () => {
112
+ if (typeof lb.getQuota !== 'function') {
113
+ return { content: [{ type: 'text', text: 'lb_quota requires LoadBalancerLive (not stub). Upgrade mcp-server.' }] };
114
+ }
115
+ const quota = lb.getQuota();
116
+ const summaries = Object.values(quota).map(q => q.summary).join('\n\n');
117
+ return { content: [{ type: 'text', text: summaries + '\n\n' + JSON.stringify(quota, null, 2) }] };
118
+ },
119
+ },
120
+ ];
121
+ }
122
+
123
+ module.exports = { createTools };