@cpretzinger/boss-claude 1.0.0 → 1.0.2

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 (87) hide show
  1. package/README.md +304 -1
  2. package/bin/boss-claude.js +1138 -0
  3. package/bin/commands/mode.js +250 -0
  4. package/bin/onyx-guard.js +259 -0
  5. package/bin/onyx-guard.sh +251 -0
  6. package/bin/prompts.js +284 -0
  7. package/bin/rollback.js +85 -0
  8. package/bin/setup-wizard.js +492 -0
  9. package/config/.env.example +17 -0
  10. package/lib/README.md +83 -0
  11. package/lib/agent-logger.js +61 -0
  12. package/lib/agents/memory-engineers/github-memory-engineer.js +251 -0
  13. package/lib/agents/memory-engineers/postgres-memory-engineer.js +633 -0
  14. package/lib/agents/memory-engineers/qdrant-memory-engineer.js +358 -0
  15. package/lib/agents/memory-engineers/redis-memory-engineer.js +383 -0
  16. package/lib/agents/memory-supervisor.js +526 -0
  17. package/lib/agents/registry.js +135 -0
  18. package/lib/auto-monitor.js +131 -0
  19. package/lib/checkpoint-hook.js +112 -0
  20. package/lib/checkpoint.js +319 -0
  21. package/lib/commentator.js +213 -0
  22. package/lib/context-scribe.js +120 -0
  23. package/lib/delegation-strategies.js +326 -0
  24. package/lib/hierarchy-validator.js +643 -0
  25. package/lib/index.js +15 -0
  26. package/lib/init-with-mode.js +261 -0
  27. package/lib/init.js +44 -6
  28. package/lib/memory-result-aggregator.js +252 -0
  29. package/lib/memory.js +35 -7
  30. package/lib/mode-enforcer.js +473 -0
  31. package/lib/onyx-banner.js +169 -0
  32. package/lib/onyx-identity.js +214 -0
  33. package/lib/onyx-monitor.js +381 -0
  34. package/lib/onyx-reminder.js +188 -0
  35. package/lib/onyx-tool-interceptor.js +341 -0
  36. package/lib/onyx-wrapper.js +315 -0
  37. package/lib/orchestrator-gate.js +334 -0
  38. package/lib/output-formatter.js +296 -0
  39. package/lib/postgres.js +1 -1
  40. package/lib/prompt-injector.js +220 -0
  41. package/lib/prompts.js +532 -0
  42. package/lib/session.js +153 -6
  43. package/lib/setup/README.md +187 -0
  44. package/lib/setup/env-manager.js +785 -0
  45. package/lib/setup/error-recovery.js +630 -0
  46. package/lib/setup/explain-scopes.js +385 -0
  47. package/lib/setup/github-instructions.js +333 -0
  48. package/lib/setup/github-repo.js +254 -0
  49. package/lib/setup/import-credentials.js +498 -0
  50. package/lib/setup/index.js +62 -0
  51. package/lib/setup/init-postgres.js +785 -0
  52. package/lib/setup/init-redis.js +456 -0
  53. package/lib/setup/integration-test.js +652 -0
  54. package/lib/setup/progress.js +357 -0
  55. package/lib/setup/rollback.js +670 -0
  56. package/lib/setup/rollback.test.js +452 -0
  57. package/lib/setup/setup-with-rollback.example.js +351 -0
  58. package/lib/setup/summary.js +400 -0
  59. package/lib/setup/test-github-setup.js +10 -0
  60. package/lib/setup/test-postgres-init.js +98 -0
  61. package/lib/setup/verify-setup.js +102 -0
  62. package/lib/task-agent-worker.js +235 -0
  63. package/lib/token-monitor.js +466 -0
  64. package/lib/tool-wrapper-integration.js +369 -0
  65. package/lib/tool-wrapper.js +387 -0
  66. package/lib/validators/README.md +497 -0
  67. package/lib/validators/config.js +583 -0
  68. package/lib/validators/config.test.js +175 -0
  69. package/lib/validators/github.js +310 -0
  70. package/lib/validators/github.test.js +61 -0
  71. package/lib/validators/index.js +15 -0
  72. package/lib/validators/postgres.js +525 -0
  73. package/package.json +98 -13
  74. package/scripts/benchmark-memory.js +433 -0
  75. package/scripts/check-secrets.sh +12 -0
  76. package/scripts/fetch-todos.mjs +148 -0
  77. package/scripts/graceful-shutdown.sh +156 -0
  78. package/scripts/install-onyx-hooks.js +373 -0
  79. package/scripts/install.js +119 -18
  80. package/scripts/redis-monitor.js +284 -0
  81. package/scripts/redis-setup.js +412 -0
  82. package/scripts/test-memory-retrieval.js +201 -0
  83. package/scripts/validate-exports.js +68 -0
  84. package/scripts/validate-package.js +120 -0
  85. package/scripts/verify-onyx-deployment.js +309 -0
  86. package/scripts/verify-redis-deployment.js +354 -0
  87. package/scripts/verify-redis-init.js +219 -0
@@ -0,0 +1,473 @@
1
+ /**
2
+ * REDIS-BASED MODE ENFORCEMENT SYSTEM
3
+ *
4
+ * Purpose: Store "mode=orchestrator" flag in Redis and check BEFORE every action
5
+ * Architecture: Fail-safe gate that prevents operations outside current mode
6
+ *
7
+ * Key Design Principles:
8
+ * 1. Single source of truth in Redis
9
+ * 2. Atomic check-and-execute pattern
10
+ * 3. TTL-based auto-expiration (safety fallback)
11
+ * 4. Zero-downtime mode switching
12
+ * 5. Audit trail of all mode changes
13
+ */
14
+
15
+ import Redis from 'ioredis';
16
+ import { fileURLToPath } from 'url';
17
+ import { dirname, join } from 'path';
18
+ import { existsSync } from 'fs';
19
+ import os from 'os';
20
+ import dotenv from 'dotenv';
21
+
22
+ const __filename = fileURLToPath(import.meta.url);
23
+ const __dirname = dirname(__filename);
24
+
25
+ // Load environment variables
26
+ const envPath = join(os.homedir(), '.boss-claude', '.env');
27
+ if (existsSync(envPath)) {
28
+ dotenv.config({ path: envPath });
29
+ }
30
+
31
+ // ==========================================
32
+ // REDIS CONNECTION SINGLETON
33
+ // ==========================================
34
+ let redis = null;
35
+
36
+ function getRedis() {
37
+ if (!redis) {
38
+ if (!process.env.REDIS_URL) {
39
+ throw new Error('REDIS_URL not found. Run: boss-claude init');
40
+ }
41
+ redis = new Redis(process.env.REDIS_URL);
42
+ }
43
+ return redis;
44
+ }
45
+
46
+ // ==========================================
47
+ // REDIS KEY ARCHITECTURE
48
+ // ==========================================
49
+ const KEYS = {
50
+ // Current active mode (orchestrator|specialist|worker)
51
+ MODE: 'boss:mode:current',
52
+
53
+ // Mode metadata (who set it, when, why)
54
+ MODE_META: 'boss:mode:metadata',
55
+
56
+ // Mode change history (sorted set by timestamp)
57
+ MODE_HISTORY: 'boss:mode:history',
58
+
59
+ // Action counter per mode (metrics)
60
+ MODE_ACTIONS: (mode) => `boss:mode:${mode}:actions`,
61
+
62
+ // Blocked action log (when mode check fails)
63
+ BLOCKED_ACTIONS: 'boss:mode:blocked_actions',
64
+
65
+ // Active session tracking
66
+ SESSION: 'boss:session:current',
67
+
68
+ // Agent identity (which agent is running)
69
+ AGENT_IDENTITY: 'boss:agent:identity'
70
+ };
71
+
72
+ // ==========================================
73
+ // MODE DEFINITIONS
74
+ // ==========================================
75
+ const MODES = {
76
+ ORCHESTRATOR: 'orchestrator', // Meta-boss coordinating agents
77
+ SPECIALIST: 'specialist', // Domain expert (n8n, postgres, redis, etc)
78
+ WORKER: 'worker', // Individual task execution
79
+ REVIEW: 'review', // Code review mode
80
+ LEARNING: 'learning' // Training/improvement mode
81
+ };
82
+
83
+ // Mode capabilities matrix
84
+ const MODE_CAPABILITIES = {
85
+ [MODES.ORCHESTRATOR]: {
86
+ canDelegate: true,
87
+ canReview: true,
88
+ canExecute: true,
89
+ canLearn: true,
90
+ canModifyGlobalConfig: true,
91
+ canSwitchMode: true,
92
+ maxTokenBudget: 100000,
93
+ requiresApproval: false
94
+ },
95
+ [MODES.SPECIALIST]: {
96
+ canDelegate: false,
97
+ canReview: false,
98
+ canExecute: true,
99
+ canLearn: true,
100
+ canModifyGlobalConfig: false,
101
+ canSwitchMode: false,
102
+ maxTokenBudget: 50000,
103
+ requiresApproval: true
104
+ },
105
+ [MODES.WORKER]: {
106
+ canDelegate: false,
107
+ canReview: false,
108
+ canExecute: true,
109
+ canLearn: false,
110
+ canModifyGlobalConfig: false,
111
+ canSwitchMode: false,
112
+ maxTokenBudget: 20000,
113
+ requiresApproval: true
114
+ },
115
+ [MODES.REVIEW]: {
116
+ canDelegate: false,
117
+ canReview: true,
118
+ canExecute: false,
119
+ canLearn: true,
120
+ canModifyGlobalConfig: false,
121
+ canSwitchMode: false,
122
+ maxTokenBudget: 30000,
123
+ requiresApproval: false
124
+ },
125
+ [MODES.LEARNING]: {
126
+ canDelegate: false,
127
+ canReview: false,
128
+ canExecute: false,
129
+ canLearn: true,
130
+ canModifyGlobalConfig: false,
131
+ canSwitchMode: false,
132
+ maxTokenBudget: 15000,
133
+ requiresApproval: false
134
+ }
135
+ };
136
+
137
+ // ==========================================
138
+ // MODE ENFORCEMENT CLASS
139
+ // ==========================================
140
+ export class ModeEnforcer {
141
+ constructor() {
142
+ this.redis = getRedis();
143
+ }
144
+
145
+ /**
146
+ * SET MODE - Atomic operation with metadata
147
+ *
148
+ * @param {string} mode - Mode to activate (orchestrator|specialist|worker|review|learning)
149
+ * @param {object} metadata - Who, when, why
150
+ * @param {number} ttl - TTL in seconds (default 86400 = 24h, prevents stuck modes)
151
+ */
152
+ async setMode(mode, metadata = {}, ttl = 86400) {
153
+ if (!Object.values(MODES).includes(mode)) {
154
+ throw new Error(`Invalid mode: ${mode}. Valid modes: ${Object.values(MODES).join(', ')}`);
155
+ }
156
+
157
+ const client = this.redis;
158
+ const timestamp = Date.now();
159
+
160
+ const modeData = {
161
+ mode,
162
+ setAt: new Date().toISOString(),
163
+ setBy: metadata.agent || 'boss-claude',
164
+ reason: metadata.reason || 'mode-change',
165
+ sessionId: metadata.sessionId || null,
166
+ ttl
167
+ };
168
+
169
+ // ATOMIC MULTI OPERATION
170
+ const pipeline = client.multi();
171
+
172
+ // 1. Set current mode with TTL
173
+ pipeline.set(KEYS.MODE, mode, 'EX', ttl);
174
+
175
+ // 2. Store metadata
176
+ pipeline.set(KEYS.MODE_META, JSON.stringify(modeData));
177
+
178
+ // 3. Add to history (sorted set by timestamp)
179
+ pipeline.zadd(KEYS.MODE_HISTORY, timestamp, JSON.stringify(modeData));
180
+
181
+ // 4. Trim history to last 1000 entries
182
+ pipeline.zremrangebyrank(KEYS.MODE_HISTORY, 0, -1001);
183
+
184
+ await pipeline.exec();
185
+
186
+ console.log(`[MODE ENFORCER] Mode set to: ${mode}`);
187
+ console.log(`[MODE ENFORCER] Metadata:`, modeData);
188
+
189
+ return modeData;
190
+ }
191
+
192
+ /**
193
+ * GET CURRENT MODE - Single source of truth
194
+ */
195
+ async getCurrentMode() {
196
+ const client = this.redis;
197
+ const mode = await client.get(KEYS.MODE);
198
+
199
+ if (!mode) {
200
+ // Default to WORKER mode if not set (safe default)
201
+ console.warn('[MODE ENFORCER] No mode set. Defaulting to WORKER mode.');
202
+ await this.setMode(MODES.WORKER, { reason: 'default-initialization' });
203
+ return MODES.WORKER;
204
+ }
205
+
206
+ return mode;
207
+ }
208
+
209
+ /**
210
+ * GET MODE METADATA - Who set it, when, why
211
+ */
212
+ async getModeMetadata() {
213
+ const client = this.redis;
214
+ const metadata = await client.get(KEYS.MODE_META);
215
+ return metadata ? JSON.parse(metadata) : null;
216
+ }
217
+
218
+ /**
219
+ * CHECK MODE - Gate check before every action
220
+ *
221
+ * @param {string} requiredMode - Mode required for this action
222
+ * @param {string} action - Description of action being attempted
223
+ * @throws {Error} if current mode doesn't match required mode
224
+ */
225
+ async checkMode(requiredMode, action = 'action') {
226
+ const currentMode = await this.getCurrentMode();
227
+
228
+ if (currentMode !== requiredMode) {
229
+ // Log blocked action
230
+ await this.logBlockedAction(currentMode, requiredMode, action);
231
+
232
+ throw new Error(
233
+ `[MODE ENFORCEMENT] Action blocked!\n` +
234
+ `Current mode: ${currentMode}\n` +
235
+ `Required mode: ${requiredMode}\n` +
236
+ `Action: ${action}\n` +
237
+ `Switch mode with: boss-claude mode ${requiredMode}`
238
+ );
239
+ }
240
+
241
+ // Increment action counter for this mode
242
+ await this.incrementActionCounter(currentMode);
243
+
244
+ return true;
245
+ }
246
+
247
+ /**
248
+ * CHECK CAPABILITY - Verify if current mode allows this capability
249
+ *
250
+ * @param {string} capability - Capability name (canDelegate, canReview, etc)
251
+ * @param {string} action - Description of action
252
+ * @throws {Error} if current mode lacks this capability
253
+ */
254
+ async checkCapability(capability, action = 'action') {
255
+ const currentMode = await this.getCurrentMode();
256
+ const capabilities = MODE_CAPABILITIES[currentMode];
257
+
258
+ if (!capabilities) {
259
+ throw new Error(`Unknown mode: ${currentMode}`);
260
+ }
261
+
262
+ if (!capabilities[capability]) {
263
+ await this.logBlockedAction(currentMode, `capability:${capability}`, action);
264
+
265
+ throw new Error(
266
+ `[CAPABILITY ENFORCEMENT] Action blocked!\n` +
267
+ `Current mode: ${currentMode}\n` +
268
+ `Required capability: ${capability}\n` +
269
+ `Action: ${action}\n` +
270
+ `This mode does not have ${capability} capability.`
271
+ );
272
+ }
273
+
274
+ return true;
275
+ }
276
+
277
+ /**
278
+ * ENFORCE TOKEN BUDGET - Check if action fits within mode's token budget
279
+ *
280
+ * @param {number} estimatedTokens - Estimated token cost
281
+ * @param {string} action - Description of action
282
+ */
283
+ async enforceTokenBudget(estimatedTokens, action = 'action') {
284
+ const currentMode = await this.getCurrentMode();
285
+ const capabilities = MODE_CAPABILITIES[currentMode];
286
+
287
+ if (estimatedTokens > capabilities.maxTokenBudget) {
288
+ await this.logBlockedAction(currentMode, 'token-budget-exceeded', action);
289
+
290
+ throw new Error(
291
+ `[TOKEN BUDGET ENFORCEMENT] Action blocked!\n` +
292
+ `Current mode: ${currentMode}\n` +
293
+ `Max budget: ${capabilities.maxTokenBudget} tokens\n` +
294
+ `Estimated cost: ${estimatedTokens} tokens\n` +
295
+ `Action: ${action}\n` +
296
+ `Consider delegating to a specialist or switching to orchestrator mode.`
297
+ );
298
+ }
299
+
300
+ return true;
301
+ }
302
+
303
+ /**
304
+ * LOG BLOCKED ACTION - Audit trail
305
+ */
306
+ async logBlockedAction(currentMode, requiredMode, action) {
307
+ const client = this.redis;
308
+ const timestamp = Date.now();
309
+
310
+ const blockData = {
311
+ timestamp: new Date().toISOString(),
312
+ currentMode,
313
+ requiredMode,
314
+ action,
315
+ stackTrace: new Error().stack
316
+ };
317
+
318
+ await client.zadd(
319
+ KEYS.BLOCKED_ACTIONS,
320
+ timestamp,
321
+ JSON.stringify(blockData)
322
+ );
323
+
324
+ // Keep last 500 blocked actions
325
+ await client.zremrangebyrank(KEYS.BLOCKED_ACTIONS, 0, -501);
326
+ }
327
+
328
+ /**
329
+ * INCREMENT ACTION COUNTER - Track actions per mode
330
+ */
331
+ async incrementActionCounter(mode) {
332
+ const client = this.redis;
333
+ await client.incr(KEYS.MODE_ACTIONS(mode));
334
+ }
335
+
336
+ /**
337
+ * GET MODE HISTORY - Last N mode changes
338
+ */
339
+ async getModeHistory(limit = 20) {
340
+ const client = this.redis;
341
+ const history = await client.zrevrange(KEYS.MODE_HISTORY, 0, limit - 1);
342
+ return history.map(entry => JSON.parse(entry));
343
+ }
344
+
345
+ /**
346
+ * GET BLOCKED ACTIONS - Last N blocked actions
347
+ */
348
+ async getBlockedActions(limit = 20) {
349
+ const client = this.redis;
350
+ const blocked = await client.zrevrange(KEYS.BLOCKED_ACTIONS, 0, limit - 1);
351
+ return blocked.map(entry => JSON.parse(entry));
352
+ }
353
+
354
+ /**
355
+ * GET MODE STATS - Action counts per mode
356
+ */
357
+ async getModeStats() {
358
+ const client = this.redis;
359
+ const stats = {};
360
+
361
+ for (const mode of Object.values(MODES)) {
362
+ const count = await client.get(KEYS.MODE_ACTIONS(mode));
363
+ stats[mode] = parseInt(count || 0);
364
+ }
365
+
366
+ return stats;
367
+ }
368
+
369
+ /**
370
+ * RESET MODE - Emergency reset to safe default
371
+ */
372
+ async resetMode() {
373
+ await this.setMode(MODES.WORKER, {
374
+ reason: 'emergency-reset',
375
+ agent: 'system'
376
+ });
377
+ }
378
+
379
+ /**
380
+ * SET AGENT IDENTITY - Track which agent is running
381
+ */
382
+ async setAgentIdentity(agentName, domain = null) {
383
+ const client = this.redis;
384
+ const identity = {
385
+ agent: agentName,
386
+ domain,
387
+ setAt: new Date().toISOString()
388
+ };
389
+
390
+ await client.set(KEYS.AGENT_IDENTITY, JSON.stringify(identity));
391
+ return identity;
392
+ }
393
+
394
+ /**
395
+ * GET AGENT IDENTITY - Who is running right now
396
+ */
397
+ async getAgentIdentity() {
398
+ const client = this.redis;
399
+ const identity = await client.get(KEYS.AGENT_IDENTITY);
400
+ return identity ? JSON.parse(identity) : null;
401
+ }
402
+ }
403
+
404
+ // ==========================================
405
+ // CONVENIENCE EXPORTS
406
+ // ==========================================
407
+ export { MODES, MODE_CAPABILITIES };
408
+
409
+ // Singleton instance
410
+ let enforcerInstance = null;
411
+
412
+ export function getEnforcer() {
413
+ if (!enforcerInstance) {
414
+ enforcerInstance = new ModeEnforcer();
415
+ }
416
+ return enforcerInstance;
417
+ }
418
+
419
+ // ==========================================
420
+ // USAGE EXAMPLES
421
+ // ==========================================
422
+
423
+ /*
424
+
425
+ // Example 1: Set orchestrator mode
426
+ const enforcer = getEnforcer();
427
+ await enforcer.setMode(MODES.ORCHESTRATOR, {
428
+ agent: 'boss-claude',
429
+ reason: 'session-start',
430
+ sessionId: 'session-12345'
431
+ });
432
+
433
+ // Example 2: Gate check before action
434
+ try {
435
+ await enforcer.checkMode(MODES.ORCHESTRATOR, 'Delegating task to postgres-specialist');
436
+ // Action proceeds
437
+ console.log('✅ Action allowed');
438
+ } catch (error) {
439
+ // Action blocked
440
+ console.error('❌', error.message);
441
+ }
442
+
443
+ // Example 3: Check capability
444
+ try {
445
+ await enforcer.checkCapability('canDelegate', 'Assigning task to n8n-workflow-architect');
446
+ // Delegation allowed
447
+ } catch (error) {
448
+ // Delegation blocked
449
+ console.error('❌', error.message);
450
+ }
451
+
452
+ // Example 4: Token budget enforcement
453
+ try {
454
+ await enforcer.enforceTokenBudget(75000, 'Generating architecture document');
455
+ // Within budget
456
+ } catch (error) {
457
+ // Over budget - should delegate
458
+ console.error('❌', error.message);
459
+ }
460
+
461
+ // Example 5: Get mode history
462
+ const history = await enforcer.getModeHistory(10);
463
+ console.log('Mode changes:', history);
464
+
465
+ // Example 6: Get blocked actions (security audit)
466
+ const blocked = await enforcer.getBlockedActions(10);
467
+ console.log('Recent blocked actions:', blocked);
468
+
469
+ // Example 7: Get mode stats
470
+ const stats = await enforcer.getModeStats();
471
+ console.log('Actions per mode:', stats);
472
+
473
+ */
@@ -0,0 +1,169 @@
1
+ /**
2
+ * ONYX MODE Banner System
3
+ * Displays "ONYX MODE: DELEGATE ONLY" banner at conversation start and every 10 messages
4
+ */
5
+
6
+ import chalk from 'chalk';
7
+
8
+ /**
9
+ * ASCII art banner for ONYX MODE
10
+ */
11
+ const ONYX_BANNER = `
12
+ ╔═══════════════════════════════════════════════════════════════════════════════╗
13
+ ║ ║
14
+ ║ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄ ▄ ▄ ▄ ▄▄ ▄▄ ║
15
+ ║ ▐░░░░░░░░░░░▌▐░░▌ ▐░▌▐░▌ ▐░▌▐░░▌ ▐░░▌ ║
16
+ ║ ▐░█▀▀▀▀▀▀▀█░▌▐░▌░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌░▌ ▐░▐░▌ ║
17
+ ║ ▐░▌ ▐░▌▐░▌▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌▐░▌ ▐░▌▐░▌ ║
18
+ ║ ▐░▌ ▐░▌▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▐░▌ ▐░▌ ║
19
+ ║ ▐░▌ ▐░▌▐░▌ ▐░▌ ▐░▌ ▐░▐░▌ ▐░▌ ▐░▌ ▐░▌ ║
20
+ ║ ▐░▌ ▐░▌▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▀ ▐░▌ ║
21
+ ║ ▐░▌ ▐░▌▐░▌ ▐░▌▐░▌ ▐░▌░▌ ▐░▌ ▐░▌ ║
22
+ ║ ▐░█▄▄▄▄▄▄▄█░▌▐░▌ ▐░▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ║
23
+ ║ ▐░░░░░░░░░░░▌▐░▌ ▐░░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ║
24
+ ║ ▀▀▀▀▀▀▀▀▀▀▀ ▀ ▀▀ ▀ ▀ ▀ ▀ ║
25
+ ║ ║
26
+ ║ ███╗ ███╗ ██████╗ ██████╗ ███████╗ ║
27
+ ║ ████╗ ████║██╔═══██╗██╔══██╗██╔════╝ ║
28
+ ║ ██╔████╔██║██║ ██║██║ ██║█████╗ ║
29
+ ║ ██║╚██╔╝██║██║ ██║██║ ██║██╔══╝ ║
30
+ ║ ██║ ╚═╝ ██║╚██████╔╝██████╔╝███████╗ ║
31
+ ║ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝ ║
32
+ ║ ║
33
+ ║ DELEGATE ONLY - NO EXECUTION ║
34
+ ║ ║
35
+ ╚═══════════════════════════════════════════════════════════════════════════════╝
36
+ `;
37
+
38
+ /**
39
+ * Compact version of banner for periodic display
40
+ */
41
+ const ONYX_BANNER_COMPACT = `
42
+ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
43
+ ┃ ⬛ ONYX MODE: DELEGATE ONLY ⬛ No task execution | Agent delegation only ┃
44
+ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
45
+ `;
46
+
47
+ /**
48
+ * Message counter for periodic banner display
49
+ */
50
+ let messageCount = 0;
51
+
52
+ /**
53
+ * Display the full ONYX MODE banner (for conversation start)
54
+ * @param {boolean} colorize - Whether to apply color styling
55
+ * @returns {string} Formatted banner
56
+ */
57
+ export function displayOnyxBanner(colorize = true) {
58
+ if (colorize) {
59
+ return chalk.black.bgWhite(ONYX_BANNER);
60
+ }
61
+ return ONYX_BANNER;
62
+ }
63
+
64
+ /**
65
+ * Display the compact ONYX MODE banner (for periodic reminders)
66
+ * @param {boolean} colorize - Whether to apply color styling
67
+ * @returns {string} Formatted compact banner
68
+ */
69
+ export function displayOnyxBannerCompact(colorize = true) {
70
+ if (colorize) {
71
+ return chalk.black.bgWhite(ONYX_BANNER_COMPACT);
72
+ }
73
+ return ONYX_BANNER_COMPACT;
74
+ }
75
+
76
+ /**
77
+ * Check if banner should be displayed based on message count
78
+ * Displays at start (count = 0) and every 10 messages
79
+ * @returns {boolean} True if banner should be shown
80
+ */
81
+ export function shouldDisplayBanner() {
82
+ if (messageCount === 0 || messageCount % 10 === 0) {
83
+ return true;
84
+ }
85
+ return false;
86
+ }
87
+
88
+ /**
89
+ * Increment message counter
90
+ * Call this after each user message
91
+ */
92
+ export function incrementMessageCount() {
93
+ messageCount++;
94
+ }
95
+
96
+ /**
97
+ * Reset message counter
98
+ * Call this at conversation start
99
+ */
100
+ export function resetMessageCount() {
101
+ messageCount = 0;
102
+ }
103
+
104
+ /**
105
+ * Get current message count
106
+ * @returns {number} Current message count
107
+ */
108
+ export function getMessageCount() {
109
+ return messageCount;
110
+ }
111
+
112
+ /**
113
+ * Auto-display banner with message tracking
114
+ * Call this function after each message
115
+ * @param {boolean} isConversationStart - True if this is the first message
116
+ * @param {boolean} colorize - Whether to apply color styling
117
+ * @returns {string|null} Banner to display, or null if not needed
118
+ */
119
+ export function autoDisplayBanner(isConversationStart = false, colorize = true) {
120
+ if (isConversationStart) {
121
+ resetMessageCount();
122
+ return displayOnyxBanner(colorize);
123
+ }
124
+
125
+ incrementMessageCount();
126
+
127
+ if (shouldDisplayBanner()) {
128
+ return displayOnyxBannerCompact(colorize);
129
+ }
130
+
131
+ return null;
132
+ }
133
+
134
+ /**
135
+ * Format banner for injection into CLI output
136
+ * Returns formatted string that can be console.log'd
137
+ * @param {boolean} isConversationStart - True if this is the first message
138
+ * @returns {string} Formatted banner ready for output
139
+ */
140
+ export function formatForCLI(isConversationStart = false) {
141
+ const banner = autoDisplayBanner(isConversationStart, true);
142
+
143
+ if (!banner) {
144
+ return '';
145
+ }
146
+
147
+ return `\n${banner}\n`;
148
+ }
149
+
150
+ /**
151
+ * Get banner with statistics
152
+ * Displays banner plus message count info
153
+ * @param {boolean} isConversationStart - True if this is the first message
154
+ * @returns {string} Banner with stats
155
+ */
156
+ export function getBannerWithStats(isConversationStart = false) {
157
+ const banner = autoDisplayBanner(isConversationStart, true);
158
+
159
+ if (!banner) {
160
+ return '';
161
+ }
162
+
163
+ const stats = chalk.dim(`Message count: ${messageCount} | Next banner at: ${Math.ceil(messageCount / 10) * 10}`);
164
+
165
+ return `\n${banner}\n${stats}\n`;
166
+ }
167
+
168
+ // Export constants for direct use
169
+ export { ONYX_BANNER, ONYX_BANNER_COMPACT };