@covibes/zeroshot 1.0.2 → 1.1.4

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/CHANGELOG.md CHANGED
@@ -1,3 +1,57 @@
1
+ ## [1.1.4](https://github.com/covibes/zeroshot/compare/v1.1.3...v1.1.4) (2025-12-28)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **cli:** read version from package.json instead of hardcoded value ([a6e0e57](https://github.com/covibes/zeroshot/commit/a6e0e570feeaffa64dbc46d494eeef000f32b708))
7
+ * **cli:** resolve streaming mode crash and refactor message formatters ([efb9264](https://github.com/covibes/zeroshot/commit/efb9264ce0d3ede0eb7d502d4625694c2c525230))
8
+
9
+ ## [1.1.3](https://github.com/covibes/zeroshot/compare/v1.1.2...v1.1.3) (2025-12-28)
10
+
11
+
12
+ ### Bug Fixes
13
+
14
+ * **publish:** remove tests from prepublishOnly to prevent double execution ([3e11e71](https://github.com/covibes/zeroshot/commit/3e11e71cb722f835634d21f80fee79ea3c29b031))
15
+
16
+ ## [1.1.2](https://github.com/covibes/zeroshot/compare/v1.1.1...v1.1.2) (2025-12-28)
17
+
18
+
19
+ ### Bug Fixes
20
+
21
+ * **ci:** resolve ESLint violations and status-footer test failures ([0d794f9](https://github.com/covibes/zeroshot/commit/0d794f98aa10d2492d8ab0af516bb1e5abee0566))
22
+ * **isolation:** handle missing/directory .gitconfig in CI environments ([3d754e4](https://github.com/covibes/zeroshot/commit/3d754e4a02d40e2fd902d97d17a6532ba247f780))
23
+ * **workflow:** extract tarball filename correctly from npm pack output ([3cf48a3](https://github.com/covibes/zeroshot/commit/3cf48a3ddf4f1938916c7ed5a2be1796003a988f))
24
+
25
+ ## [1.1.1](https://github.com/covibes/zeroshot/compare/v1.1.0...v1.1.1) (2025-12-28)
26
+
27
+
28
+ ### Bug Fixes
29
+
30
+ * **lint:** resolve require-await and unused-imports errors ([852c8a0](https://github.com/covibes/zeroshot/commit/852c8a0e9076eb5403105c6f319e66e53c27fd6d))
31
+
32
+ # [1.1.0](https://github.com/covibes/zeroshot/compare/v1.0.2...v1.1.0) (2025-12-28)
33
+
34
+
35
+ ### Bug Fixes
36
+
37
+ * **docker:** use repo root as build context for Dockerfile ([c1d6719](https://github.com/covibes/zeroshot/commit/c1d6719eb43787ba62e5f69663eb4e5bd1aeb492))
38
+ * **lint:** remove unused import and fix undefined variable in test ([41c9965](https://github.com/covibes/zeroshot/commit/41c9965eb84d2b8c22eaaf8e1d65a5f41c7b1e44))
39
+
40
+
41
+ ### Features
42
+
43
+ * **isolation:** use zeroshot task infrastructure inside containers ([922f30d](https://github.com/covibes/zeroshot/commit/922f30d5ddd8c4d87cac375fd97025f402e7c43e))
44
+ * **monitoring:** add live status footer with CPU/memory metrics ([2df3de0](https://github.com/covibes/zeroshot/commit/2df3de0a1fe9573961b596da9e78a159f3c33086))
45
+ * **validators:** add zero-tolerance rejection rules for incomplete code ([308aef8](https://github.com/covibes/zeroshot/commit/308aef8b5ee2e3ff05e336ee810b842492183b2e))
46
+ * **validators:** strengthen with senior engineering principles ([d83f666](https://github.com/covibes/zeroshot/commit/d83f6668a145e36bd7d807d9821e8631a3a1cc18))
47
+
48
+ ## [1.0.2](https://github.com/covibes/zeroshot/compare/v1.0.1...v1.0.2) (2025-12-27)
49
+
50
+
51
+ ### Bug Fixes
52
+
53
+ * include task-lib in npm package ([37602fb](https://github.com/covibes/zeroshot/commit/37602fb3f1f6cd735d8db232be5829dc342b815d))
54
+
1
55
  ## [1.0.1](https://github.com/covibes/zeroshot/compare/v1.0.0...v1.0.1) (2025-12-27)
2
56
 
3
57
 
package/README.md CHANGED
@@ -4,6 +4,8 @@
4
4
  [![Node 18+](https://img.shields.io/badge/node-18%2B-brightgreen.svg)](https://nodejs.org/)
5
5
  [![Platform: Linux | macOS](https://img.shields.io/badge/platform-Linux%20%7C%20macOS-blue.svg)]()
6
6
 
7
+ > **2024** was the year of LLMs. **2025** was the year of agents. **2026** is the year of agent clusters.
8
+
7
9
  **Multi-agent coding CLI built on Claude Code.**
8
10
 
9
11
  You know the problem. Your AI agent:
package/cli/index.js CHANGED
@@ -22,6 +22,23 @@ const chalk = require('chalk');
22
22
  const Orchestrator = require('../src/orchestrator');
23
23
  const { setupCompletion } = require('../lib/completion');
24
24
  const { parseChunk } = require('../lib/stream-json-parser');
25
+ const { formatWatchMode } = require('./message-formatters-watch');
26
+ const {
27
+ formatAgentLifecycle,
28
+ formatAgentError: formatAgentErrorNormal,
29
+ formatIssueOpened: formatIssueOpenedNormal,
30
+ formatImplementationReady: formatImplementationReadyNormal,
31
+ formatValidationResult: formatValidationResultNormal,
32
+ formatPrCreated,
33
+ formatClusterComplete,
34
+ formatClusterFailed,
35
+ formatGenericMessage,
36
+ } = require('./message-formatters-normal');
37
+ const {
38
+ getColorForSender,
39
+ buildMessagePrefix,
40
+ buildClusterPrefix,
41
+ } = require('./message-formatter-utils');
25
42
  const {
26
43
  loadSettings,
27
44
  saveSettings,
@@ -29,6 +46,8 @@ const {
29
46
  coerceValue,
30
47
  DEFAULT_SETTINGS,
31
48
  } = require('../lib/settings');
49
+ const { requirePreflight } = require('../src/preflight');
50
+ const { StatusFooter } = require('../src/status-footer');
32
51
 
33
52
  const program = new Command();
34
53
 
@@ -317,16 +336,12 @@ if (shouldShowBanner) {
317
336
  showBanner();
318
337
  }
319
338
 
320
- // Color palette for agents (avoid green/red - reserved for APPROVED/REJECTED)
321
- const COLORS = [chalk.cyan, chalk.yellow, chalk.magenta, chalk.blue, chalk.white, chalk.gray];
322
-
323
- // Map agent IDs to colors
324
- const agentColors = new Map();
339
+ // NOTE: Agent color handling moved to message-formatter-utils.js
325
340
 
326
341
  program
327
342
  .name('zeroshot')
328
343
  .description('Multi-agent orchestration and task management for Claude')
329
- .version('1.0.0')
344
+ .version(require('../package.json').version)
330
345
  .addHelpText(
331
346
  'after',
332
347
  `
@@ -423,6 +438,16 @@ Input formats:
423
438
  input.text = inputArg;
424
439
  }
425
440
 
441
+ // === PREFLIGHT CHECKS ===
442
+ // Validate all dependencies BEFORE starting anything
443
+ // This gives users clear, actionable error messages upfront
444
+ const preflightOptions = {
445
+ requireGh: !!input.issue, // gh CLI required when fetching GitHub issues
446
+ requireDocker: options.isolation || options.full, // Docker required for isolation mode
447
+ quiet: process.env.CREW_DAEMON === '1', // Suppress success in daemon mode
448
+ };
449
+ requirePreflight(preflightOptions);
450
+
426
451
  // === CLUSTER MODE ===
427
452
  // Validate --pr requires --isolation
428
453
  if (options.pr && !options.isolation) {
@@ -580,6 +605,60 @@ Input formats:
580
605
  // Track messages we've already processed (to avoid duplicates between history and subscription)
581
606
  const processedMessageIds = new Set();
582
607
 
608
+ // === STATUS FOOTER: Live agent monitoring ===
609
+ // Shows CPU, memory, network metrics for all agents at bottom of terminal
610
+ const statusFooter = new StatusFooter({
611
+ refreshInterval: 1000,
612
+ enabled: process.stdout.isTTY,
613
+ });
614
+ statusFooter.setCluster(clusterId);
615
+ statusFooter.setClusterState('running');
616
+
617
+ // Subscribe to AGENT_LIFECYCLE to track agent states and PIDs
618
+ const lifecycleUnsubscribe = cluster.messageBus.subscribeTopic('AGENT_LIFECYCLE', (msg) => {
619
+ const data = msg.content?.data || {};
620
+ const event = data.event;
621
+ const agentId = data.agent || msg.sender;
622
+
623
+ // Update agent state based on lifecycle event
624
+ if (event === 'STARTED') {
625
+ statusFooter.updateAgent({
626
+ id: agentId,
627
+ state: 'idle',
628
+ pid: null,
629
+ iteration: data.iteration || 0,
630
+ });
631
+ } else if (event === 'TASK_STARTED') {
632
+ statusFooter.updateAgent({
633
+ id: agentId,
634
+ state: 'executing',
635
+ pid: statusFooter.agents.get(agentId)?.pid || null,
636
+ iteration: data.iteration || 0,
637
+ });
638
+ } else if (event === 'PROCESS_SPAWNED') {
639
+ // Got the PID - update the agent with it
640
+ const current = statusFooter.agents.get(agentId) || { state: 'executing', iteration: 0 };
641
+ statusFooter.updateAgent({
642
+ id: agentId,
643
+ state: current.state,
644
+ pid: data.pid,
645
+ iteration: current.iteration,
646
+ });
647
+ } else if (event === 'TASK_COMPLETED' || event === 'TASK_FAILED') {
648
+ statusFooter.updateAgent({
649
+ id: agentId,
650
+ state: 'idle',
651
+ pid: null,
652
+ iteration: data.iteration || 0,
653
+ });
654
+ } else if (event === 'STOPPED') {
655
+ statusFooter.removeAgent(agentId);
656
+ }
657
+ });
658
+
659
+ // Start the status footer
660
+ statusFooter.start();
661
+
583
662
  // Message handler - processes messages, deduplicates by ID
584
663
  const handleMessage = (msg) => {
585
664
  if (msg.cluster_id !== clusterId) return;
@@ -618,6 +697,9 @@ Input formats:
618
697
  if (status.state !== 'running') {
619
698
  clearInterval(checkInterval);
620
699
  clearInterval(flushInterval);
700
+ // Stop status footer
701
+ statusFooter.stop();
702
+ lifecycleUnsubscribe();
621
703
  // Final flush
622
704
  for (const sender of sendersWithOutput) {
623
705
  const prefix = getColorForSender(sender)(`${sender.padEnd(15)} |`);
@@ -630,6 +712,8 @@ Input formats:
630
712
  // Cluster may have been removed
631
713
  clearInterval(checkInterval);
632
714
  clearInterval(flushInterval);
715
+ statusFooter.stop();
716
+ lifecycleUnsubscribe();
633
717
  unsubscribe();
634
718
  resolve();
635
719
  }
@@ -639,6 +723,10 @@ Input formats:
639
723
  // CRITICAL: In foreground mode, the cluster runs IN this process.
640
724
  // If we exit without stopping, the cluster becomes a zombie (state=running but no process).
641
725
  process.on('SIGINT', async () => {
726
+ // Stop status footer first to restore terminal
727
+ statusFooter.stop();
728
+ lifecycleUnsubscribe();
729
+
642
730
  console.log(chalk.dim('\n\n--- Interrupted ---'));
643
731
  clearInterval(checkInterval);
644
732
  clearInterval(flushInterval);
@@ -752,6 +840,14 @@ taskCmd
752
840
  .option('--silent-json-output', 'Log ONLY final structured output')
753
841
  .action(async (prompt, options) => {
754
842
  try {
843
+ // === PREFLIGHT CHECKS ===
844
+ // Claude CLI must be installed and authenticated for task execution
845
+ requirePreflight({
846
+ requireGh: false, // gh not needed for plain tasks
847
+ requireDocker: false, // Docker not needed for plain tasks
848
+ quiet: false,
849
+ });
850
+
755
851
  // Dynamically import task command (ESM module)
756
852
  const { runTask } = await import('../task-lib/commands/run.js');
757
853
  await runTask(prompt, options);
@@ -1886,6 +1982,16 @@ program
1886
1982
  // Check if cluster exists
1887
1983
  const cluster = orchestrator.getCluster(id);
1888
1984
 
1985
+ // === PREFLIGHT CHECKS ===
1986
+ // Claude CLI must be installed and authenticated
1987
+ // Check if cluster uses isolation (needs Docker)
1988
+ const requiresDocker = cluster?.isolation?.enabled || false;
1989
+ requirePreflight({
1990
+ requireGh: false, // Resume doesn't fetch new issues
1991
+ requireDocker: requiresDocker,
1992
+ quiet: false,
1993
+ });
1994
+
1889
1995
  if (cluster) {
1890
1996
  // Resume cluster
1891
1997
  console.log(chalk.cyan(`Resuming cluster ${id}...`));
@@ -2928,18 +3034,6 @@ function formatToolResult(content, isError, toolName, toolInput) {
2928
3034
 
2929
3035
  // Helper function to get deterministic color for an agent/sender based on name hash
2930
3036
  // Uses djb2 hash algorithm for good distribution across color palette
2931
- function getColorForSender(sender) {
2932
- if (!agentColors.has(sender)) {
2933
- let hash = 5381;
2934
- for (let i = 0; i < sender.length; i++) {
2935
- hash = (hash << 5) + hash + sender.charCodeAt(i);
2936
- }
2937
- const colorIndex = Math.abs(hash) % COLORS.length;
2938
- agentColors.set(sender, COLORS[colorIndex]);
2939
- }
2940
- return agentColors.get(sender);
2941
- }
2942
-
2943
3037
  // Track recently seen content to avoid duplicates
2944
3038
  const recentContentHashes = new Set();
2945
3039
  const MAX_RECENT_HASHES = 100;
@@ -3546,166 +3640,62 @@ const FILTERED_PATTERNS = [
3546
3640
 
3547
3641
  // Helper function to print a message (docker-compose style with colors)
3548
3642
  function printMessage(msg, showClusterId = false, watchMode = false, isActive = true) {
3643
+ // Build prefix using utility function
3644
+ const prefix = buildMessagePrefix(msg, showClusterId, isActive);
3645
+
3549
3646
  const timestamp = new Date(msg.timestamp).toLocaleTimeString('en-US', {
3550
3647
  hour12: false,
3551
3648
  });
3552
- // Use dim colors for inactive clusters
3553
- const color = isActive ? getColorForSender(msg.sender) : chalk.dim;
3554
3649
 
3555
- // Build prefix with optional cluster ID and model (sender_model is set by agent-wrapper._publish)
3556
- let senderLabel = msg.sender;
3557
- if (showClusterId && msg.cluster_id) {
3558
- senderLabel = `${msg.cluster_id}/${msg.sender}`;
3559
- }
3560
- const modelSuffix = msg.sender_model ? chalk.dim(` [${msg.sender_model}]`) : '';
3561
- const prefix = color(`${senderLabel.padEnd(showClusterId ? 25 : 15)} |`) + modelSuffix;
3562
-
3563
- // Watch mode: ONLY show high-level business events in human-readable format
3650
+ // Watch mode: delegate to watch mode formatter
3564
3651
  if (watchMode) {
3565
- // Skip low-level topics (too noisy for watch mode)
3566
- if (msg.topic === 'AGENT_OUTPUT' || msg.topic === 'AGENT_LIFECYCLE') {
3567
- return;
3568
- }
3569
-
3570
- // Clear status line, print message, will be redrawn by status interval
3571
- process.stdout.write('\r' + ' '.repeat(120) + '\r');
3572
-
3573
- // Simplified prefix for watch mode: just cluster ID (white = alive, grey = dead)
3574
- const clusterPrefix = isActive
3575
- ? chalk.white(`${msg.cluster_id.padEnd(20)} |`)
3576
- : chalk.dim(`${msg.cluster_id.padEnd(20)} |`);
3577
-
3578
- // AGENT_ERROR: Show errors prominently
3579
- if (msg.topic === 'AGENT_ERROR') {
3580
- const errorMsg = `${msg.sender} ${chalk.bold.red('ERROR')}`;
3581
- console.log(`${clusterPrefix} ${errorMsg}`);
3582
- if (msg.content?.text) {
3583
- console.log(`${clusterPrefix} ${chalk.red(msg.content.text)}`);
3584
- }
3585
- return;
3586
- }
3587
-
3588
- // Human-readable event descriptions (with consistent agent colors)
3589
- const agentColor = getColorForSender(msg.sender);
3590
- const agentName = agentColor(msg.sender);
3591
- let eventText = '';
3592
-
3593
- switch (msg.topic) {
3594
- case 'ISSUE_OPENED':
3595
- const issueNum = msg.content?.data?.issue_number || '';
3596
- const title = msg.content?.data?.title || '';
3597
- const prompt = msg.content?.data?.prompt || msg.content?.text || '';
3598
-
3599
- // If it's manual input, show the prompt instead of "Manual Input"
3600
- const taskDesc = title === 'Manual Input' && prompt ? prompt : title;
3601
- const truncatedDesc =
3602
- taskDesc && taskDesc.length > 60 ? taskDesc.substring(0, 60) + '...' : taskDesc;
3603
-
3604
- eventText = `Started ${issueNum ? `#${issueNum}` : 'task'}${truncatedDesc ? chalk.dim(` - ${truncatedDesc}`) : ''}`;
3605
- break;
3606
-
3607
- case 'IMPLEMENTATION_READY':
3608
- eventText = `${agentName} completed implementation`;
3609
- break;
3610
-
3611
- case 'VALIDATION_RESULT':
3612
- const data = msg.content?.data;
3613
- const approved = data?.approved === 'true' || data?.approved === true;
3614
- const status = approved ? chalk.green('APPROVED') : chalk.red('REJECTED');
3615
- eventText = `${agentName} ${status}`;
3616
- if (data?.summary && !approved) {
3617
- eventText += chalk.dim(` - ${data.summary}`);
3618
- }
3619
- console.log(`${clusterPrefix} ${eventText}`);
3620
-
3621
- // Show rejection details (character counts only)
3622
- if (!approved) {
3623
- let errors = data.errors;
3624
- let issues = data.issues;
3625
-
3626
- if (typeof errors === 'string') {
3627
- try {
3628
- errors = JSON.parse(errors);
3629
- } catch {
3630
- errors = [];
3631
- }
3632
- }
3633
- if (typeof issues === 'string') {
3634
- try {
3635
- issues = JSON.parse(issues);
3636
- } catch {
3637
- issues = [];
3638
- }
3639
- }
3652
+ const clusterPrefix = buildClusterPrefix(msg, isActive);
3653
+ formatWatchMode(msg, clusterPrefix);
3654
+ return;
3655
+ }
3640
3656
 
3641
- // Calculate total character counts
3642
- let errorsCharCount = 0;
3643
- let issuesCharCount = 0;
3657
+ // Normal mode: delegate to appropriate formatter based on topic
3658
+ if (msg.topic === 'AGENT_LIFECYCLE') {
3659
+ formatAgentLifecycle(msg, prefix);
3660
+ return;
3661
+ }
3644
3662
 
3645
- if (Array.isArray(errors) && errors.length > 0) {
3646
- errorsCharCount = JSON.stringify(errors).length;
3647
- console.log(
3648
- `${clusterPrefix} ${chalk.red('•')} ${errors.length} error${errors.length > 1 ? 's' : ''} (${errorsCharCount} chars)`
3649
- );
3650
- }
3663
+ if (msg.topic === 'AGENT_ERROR') {
3664
+ formatAgentErrorNormal(msg, prefix, timestamp);
3665
+ return;
3666
+ }
3651
3667
 
3652
- if (Array.isArray(issues) && issues.length > 0) {
3653
- issuesCharCount = JSON.stringify(issues).length;
3654
- console.log(
3655
- `${clusterPrefix} ${chalk.yellow('•')} ${issues.length} issue${issues.length > 1 ? 's' : ''} (${issuesCharCount} chars)`
3656
- );
3657
- }
3658
- }
3659
- return;
3668
+ if (msg.topic === 'ISSUE_OPENED') {
3669
+ formatIssueOpenedNormal(msg, prefix, timestamp, shownNewTaskForCluster);
3670
+ return;
3671
+ }
3660
3672
 
3661
- case 'PR_CREATED':
3662
- const prNum = msg.content?.data?.pr_number || '';
3663
- eventText = `${agentName} created PR${prNum ? ` #${prNum}` : ''}`;
3664
- break;
3673
+ if (msg.topic === 'IMPLEMENTATION_READY') {
3674
+ formatImplementationReadyNormal(msg, prefix, timestamp);
3675
+ return;
3676
+ }
3665
3677
 
3666
- case 'PR_MERGED':
3667
- eventText = `${agentName} merged PR`;
3668
- break;
3678
+ if (msg.topic === 'VALIDATION_RESULT') {
3679
+ formatValidationResultNormal(msg, prefix, timestamp);
3680
+ return;
3681
+ }
3669
3682
 
3670
- default:
3671
- // Fallback for unknown topics
3672
- eventText = `${agentName} ${msg.topic.toLowerCase().replace(/_/g, ' ')}`;
3673
- }
3683
+ if (msg.topic === 'PR_CREATED') {
3684
+ formatPrCreated(msg, prefix, timestamp);
3685
+ return;
3686
+ }
3674
3687
 
3675
- console.log(`${clusterPrefix} ${eventText}`);
3688
+ if (msg.topic === 'CLUSTER_COMPLETE') {
3689
+ formatClusterComplete(msg, prefix, timestamp);
3676
3690
  return;
3677
3691
  }
3678
3692
 
3679
- // AGENT_LIFECYCLE: Show agent start/trigger/task events
3680
- if (msg.topic === 'AGENT_LIFECYCLE') {
3681
- const data = msg.content?.data;
3682
- const event = data?.event;
3683
-
3684
- let icon, eventText;
3685
- switch (event) {
3686
- case 'STARTED':
3687
- icon = chalk.green('▶');
3688
- const triggers = data.triggers?.join(', ') || 'none';
3689
- eventText = `started (listening for: ${chalk.dim(triggers)})`;
3690
- break;
3691
- case 'TASK_STARTED':
3692
- icon = chalk.yellow('⚡');
3693
- eventText = `${chalk.cyan(data.triggeredBy)} → task #${data.iteration} (${chalk.dim(data.model)})`;
3694
- break;
3695
- case 'TASK_COMPLETED':
3696
- icon = chalk.green('✓');
3697
- eventText = `task #${data.iteration} completed`;
3698
- break;
3699
- default:
3700
- icon = chalk.dim('•');
3701
- eventText = event || 'unknown event';
3702
- }
3703
-
3704
- console.log(`${prefix} ${icon} ${eventText}`);
3693
+ if (msg.topic === 'CLUSTER_FAILED') {
3694
+ formatClusterFailed(msg, prefix, timestamp);
3705
3695
  return;
3706
3696
  }
3707
3697
 
3708
- // AGENT_OUTPUT: parse streaming JSON and display all content
3698
+ // AGENT_OUTPUT: handle separately (complex streaming logic - kept in main file due to dependencies)
3709
3699
  if (msg.topic === 'AGENT_OUTPUT') {
3710
3700
  // Support both old 'chunk' and new 'line' formats
3711
3701
  const content = msg.content?.data?.line || msg.content?.data?.chunk || msg.content?.text;
@@ -3917,55 +3907,8 @@ function printMessage(msg, showClusterId = false, watchMode = false, isActive =
3917
3907
  return;
3918
3908
  }
3919
3909
 
3920
- // PR_CREATED: show PR created banner
3921
- if (msg.topic === 'PR_CREATED') {
3922
- console.log('');
3923
- console.log(chalk.bold.green(`${'─'.repeat(60)}`));
3924
- console.log(`${prefix} ${chalk.gray(timestamp)} ${chalk.bold.green('🔗 PR CREATED')}`);
3925
- if (msg.content?.data?.pr_url) {
3926
- console.log(`${prefix} ${chalk.cyan(msg.content.data.pr_url)}`);
3927
- }
3928
- if (msg.content?.data?.merged) {
3929
- console.log(`${prefix} ${chalk.bold.cyan('✓ MERGED')}`);
3930
- }
3931
- console.log(chalk.bold.green(`${'─'.repeat(60)}`));
3932
- return;
3933
- }
3934
-
3935
- // CLUSTER_COMPLETE: show completion banner
3936
- if (msg.topic === 'CLUSTER_COMPLETE') {
3937
- console.log('');
3938
- console.log(chalk.bold.green(`${'═'.repeat(60)}`));
3939
- console.log(`${prefix} ${chalk.gray(timestamp)} ${chalk.bold.green('✅ CLUSTER COMPLETED')}`);
3940
- if (msg.content?.text) {
3941
- console.log(`${prefix} ${chalk.white(msg.content.text)}`);
3942
- }
3943
- if (msg.content?.data?.reason) {
3944
- console.log(`${prefix} ${chalk.dim('Reason:')} ${msg.content.data.reason}`);
3945
- }
3946
- console.log(chalk.bold.green(`${'═'.repeat(60)}`));
3947
- console.log('');
3948
- return;
3949
- }
3950
-
3951
- // Other topics: generic display
3952
- const topicColor = msg.topic.startsWith('TASK_')
3953
- ? chalk.bold.green
3954
- : msg.topic.startsWith('ERROR')
3955
- ? chalk.bold.red
3956
- : chalk.bold;
3957
-
3958
- console.log(`${prefix} ${chalk.gray(timestamp)} ${topicColor(msg.topic)}`);
3959
-
3960
- // Show text content (skip template variables)
3961
- if (msg.content?.text && !msg.content.text.includes('{{')) {
3962
- const lines = msg.content.text.split('\n');
3963
- for (const line of lines) {
3964
- if (line.trim()) {
3965
- console.log(`${prefix} ${line}`);
3966
- }
3967
- }
3968
- }
3910
+ // Fallback: generic message display for unknown topics
3911
+ formatGenericMessage(msg, prefix, timestamp);
3969
3912
  }
3970
3913
 
3971
3914
  // Default command handling: if first arg doesn't match a known command, treat it as 'run'
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Message formatting utilities for CLI output
3
+ * Extracted from index.js to reduce complexity
4
+ */
5
+
6
+ const chalk = require('chalk');
7
+
8
+ /**
9
+ * Get color for sender based on consistent hashing
10
+ * @param {string} sender - Sender name
11
+ * @returns {Function} Chalk color function
12
+ */
13
+ function getColorForSender(sender) {
14
+ const colors = [chalk.cyan, chalk.magenta, chalk.yellow, chalk.green, chalk.blue];
15
+ let hash = 0;
16
+ for (let i = 0; i < sender.length; i++) {
17
+ hash = (hash << 5) - hash + sender.charCodeAt(i);
18
+ hash = hash & hash;
19
+ }
20
+ return colors[Math.abs(hash) % colors.length];
21
+ }
22
+
23
+ /**
24
+ * Build message prefix with timestamp, sender, and optional cluster ID
25
+ * @param {Object} msg - Message object
26
+ * @param {boolean} showClusterId - Whether to show cluster ID
27
+ * @param {boolean} isActive - Whether cluster is active
28
+ * @returns {string} Formatted prefix
29
+ */
30
+ function buildMessagePrefix(msg, showClusterId, isActive) {
31
+ const color = isActive ? getColorForSender(msg.sender) : chalk.dim;
32
+
33
+ let senderLabel = msg.sender;
34
+ if (showClusterId && msg.cluster_id) {
35
+ senderLabel = `${msg.cluster_id}/${msg.sender}`;
36
+ }
37
+
38
+ const modelSuffix = msg.sender_model ? chalk.dim(` [${msg.sender_model}]`) : '';
39
+ return color(`${senderLabel.padEnd(showClusterId ? 25 : 15)} |`) + modelSuffix;
40
+ }
41
+
42
+ /**
43
+ * Build cluster prefix for watch mode
44
+ * @param {string} clusterId - Cluster ID
45
+ * @param {boolean} isActive - Whether cluster is active
46
+ * @returns {string} Formatted prefix
47
+ */
48
+ function buildClusterPrefix(clusterId, isActive) {
49
+ return isActive
50
+ ? chalk.white(`${clusterId.padEnd(20)} |`)
51
+ : chalk.dim(`${clusterId.padEnd(20)} |`);
52
+ }
53
+
54
+ /**
55
+ * Parse and normalize data fields (handles string JSON)
56
+ * @param {string|Array} data - Data to parse
57
+ * @returns {Array} Parsed array
58
+ */
59
+ function parseDataField(data) {
60
+ if (typeof data === 'string') {
61
+ try {
62
+ return JSON.parse(data);
63
+ } catch {
64
+ return [];
65
+ }
66
+ }
67
+ return Array.isArray(data) ? data : [];
68
+ }
69
+
70
+ module.exports = {
71
+ getColorForSender,
72
+ buildMessagePrefix,
73
+ buildClusterPrefix,
74
+ parseDataField,
75
+ };