@covibes/zeroshot 1.4.0 → 2.0.0

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.
@@ -0,0 +1,174 @@
1
+ /**
2
+ * First-Run Setup Wizard
3
+ *
4
+ * Interactive setup on first use:
5
+ * - Welcome banner
6
+ * - Default model selection (sonnet/opus/haiku)
7
+ * - Auto-update preference
8
+ * - Marks setup as complete
9
+ */
10
+
11
+ const readline = require('readline');
12
+ const { loadSettings, saveSettings } = require('../../lib/settings');
13
+
14
+ /**
15
+ * Print welcome banner
16
+ */
17
+ function printWelcome() {
18
+ console.log(`
19
+ ╔═══════════════════════════════════════════════════════════════╗
20
+ ║ ║
21
+ ║ Welcome to Zeroshot! ║
22
+ ║ Multi-agent orchestration for Claude ║
23
+ ║ ║
24
+ ║ Let's configure a few settings to get started. ║
25
+ ║ ║
26
+ ╚═══════════════════════════════════════════════════════════════╝
27
+ `);
28
+ }
29
+
30
+ /**
31
+ * Create readline interface
32
+ * @returns {readline.Interface}
33
+ */
34
+ function createReadline() {
35
+ return readline.createInterface({
36
+ input: process.stdin,
37
+ output: process.stdout,
38
+ });
39
+ }
40
+
41
+ /**
42
+ * Prompt for model selection
43
+ * @param {readline.Interface} rl
44
+ * @returns {Promise<string>}
45
+ */
46
+ function promptModel(rl) {
47
+ return new Promise((resolve) => {
48
+ console.log('Which Claude model should agents use by default?\n');
49
+ console.log(' 1) sonnet - Fast & capable (recommended)');
50
+ console.log(' 2) opus - Most capable, slower');
51
+ console.log(' 3) haiku - Fastest, for simple tasks\n');
52
+
53
+ rl.question('Enter 1, 2, or 3 [1]: ', (answer) => {
54
+ const choice = answer.trim() || '1';
55
+ switch (choice) {
56
+ case '2':
57
+ resolve('opus');
58
+ break;
59
+ case '3':
60
+ resolve('haiku');
61
+ break;
62
+ default:
63
+ resolve('sonnet');
64
+ }
65
+ });
66
+ });
67
+ }
68
+
69
+ /**
70
+ * Prompt for auto-update preference
71
+ * @param {readline.Interface} rl
72
+ * @returns {Promise<boolean>}
73
+ */
74
+ function promptAutoUpdate(rl) {
75
+ return new Promise((resolve) => {
76
+ console.log('\nWould you like zeroshot to check for updates automatically?');
77
+ console.log('(Checks npm registry every 24 hours)\n');
78
+
79
+ rl.question('Enable auto-update checks? [Y/n]: ', (answer) => {
80
+ const normalized = answer.trim().toLowerCase();
81
+ // Default to yes if empty or starts with 'y'
82
+ resolve(normalized === '' || normalized === 'y' || normalized === 'yes');
83
+ });
84
+ });
85
+ }
86
+
87
+ /**
88
+ * Print completion message
89
+ * @param {object} settings - Saved settings
90
+ */
91
+ function printComplete(settings) {
92
+ console.log(`
93
+ ╔═══════════════════════════════════════════════════════════════╗
94
+ ║ Setup complete! ║
95
+ ╚═══════════════════════════════════════════════════════════════╝
96
+
97
+ Your settings:
98
+ • Default model: ${settings.defaultModel}
99
+ • Auto-updates: ${settings.autoCheckUpdates ? 'enabled' : 'disabled'}
100
+
101
+ Change anytime with: zeroshot settings set <key> <value>
102
+
103
+ Get started:
104
+ zeroshot run "Fix the bug in auth.js"
105
+ zeroshot run 123 (GitHub issue number)
106
+ zeroshot --help
107
+
108
+ `);
109
+ }
110
+
111
+ /**
112
+ * Check if first-run setup is needed
113
+ * @param {object} settings - Current settings
114
+ * @returns {boolean}
115
+ */
116
+ function detectFirstRun(settings) {
117
+ return !settings.firstRunComplete;
118
+ }
119
+
120
+ /**
121
+ * Main entry point - run first-time setup if needed
122
+ * @param {object} options
123
+ * @param {boolean} options.quiet - Skip interactive prompts
124
+ * @returns {Promise<boolean>} True if setup was run
125
+ */
126
+ async function checkFirstRun(options = {}) {
127
+ const settings = loadSettings();
128
+
129
+ // Already completed setup
130
+ if (!detectFirstRun(settings)) {
131
+ return false;
132
+ }
133
+
134
+ // Quiet mode - use defaults, mark complete
135
+ if (options.quiet) {
136
+ settings.firstRunComplete = true;
137
+ saveSettings(settings);
138
+ return true;
139
+ }
140
+
141
+ // Interactive setup
142
+ printWelcome();
143
+
144
+ const rl = createReadline();
145
+
146
+ try {
147
+ // Model selection
148
+ const model = await promptModel(rl);
149
+ settings.defaultModel = model;
150
+
151
+ // Auto-update preference
152
+ const autoUpdate = await promptAutoUpdate(rl);
153
+ settings.autoCheckUpdates = autoUpdate;
154
+
155
+ // Mark complete
156
+ settings.firstRunComplete = true;
157
+ saveSettings(settings);
158
+
159
+ // Print completion
160
+ printComplete(settings);
161
+
162
+ return true;
163
+ } finally {
164
+ rl.close();
165
+ }
166
+ }
167
+
168
+ module.exports = {
169
+ checkFirstRun,
170
+ // Exported for testing
171
+ detectFirstRun,
172
+ printWelcome,
173
+ printComplete,
174
+ };
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Update Checker - Checks npm registry for newer versions
3
+ *
4
+ * Features:
5
+ * - 24-hour check interval (avoids registry spam)
6
+ * - 5-second timeout (non-blocking if offline)
7
+ * - Interactive prompt for manual update
8
+ * - Respects quiet mode (no prompts in CI/scripts)
9
+ */
10
+
11
+ const https = require('https');
12
+ const { spawn } = require('child_process');
13
+ const readline = require('readline');
14
+ const { loadSettings, saveSettings } = require('../../lib/settings');
15
+
16
+ // 24 hours in milliseconds
17
+ const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
18
+
19
+ // Timeout for npm registry fetch (5 seconds)
20
+ const FETCH_TIMEOUT_MS = 5000;
21
+
22
+ // npm registry URL
23
+ const REGISTRY_URL = 'https://registry.npmjs.org/@covibes/zeroshot/latest';
24
+
25
+ /**
26
+ * Get current package version
27
+ * @returns {string}
28
+ */
29
+ function getCurrentVersion() {
30
+ const pkg = require('../../package.json');
31
+ return pkg.version;
32
+ }
33
+
34
+ /**
35
+ * Compare semver versions
36
+ * @param {string} current - Current version (e.g., "1.5.0")
37
+ * @param {string} latest - Latest version (e.g., "1.6.0")
38
+ * @returns {boolean} True if latest > current
39
+ */
40
+ function isNewerVersion(current, latest) {
41
+ const currentParts = current.split('.').map(Number);
42
+ const latestParts = latest.split('.').map(Number);
43
+
44
+ for (let i = 0; i < 3; i++) {
45
+ const c = currentParts[i] || 0;
46
+ const l = latestParts[i] || 0;
47
+ if (l > c) return true;
48
+ if (l < c) return false;
49
+ }
50
+ return false;
51
+ }
52
+
53
+ /**
54
+ * Fetch latest version from npm registry
55
+ * @returns {Promise<string|null>} Latest version or null on failure
56
+ */
57
+ function fetchLatestVersion() {
58
+ return new Promise((resolve) => {
59
+ const req = https.get(REGISTRY_URL, { timeout: FETCH_TIMEOUT_MS }, (res) => {
60
+ if (res.statusCode !== 200) {
61
+ resolve(null);
62
+ return;
63
+ }
64
+
65
+ let data = '';
66
+ res.on('data', (chunk) => {
67
+ data += chunk;
68
+ });
69
+
70
+ res.on('end', () => {
71
+ try {
72
+ const json = JSON.parse(data);
73
+ resolve(json.version || null);
74
+ } catch {
75
+ resolve(null);
76
+ }
77
+ });
78
+ });
79
+
80
+ req.on('error', () => {
81
+ resolve(null);
82
+ });
83
+
84
+ req.on('timeout', () => {
85
+ req.destroy();
86
+ resolve(null);
87
+ });
88
+
89
+ // Additional safety timeout
90
+ setTimeout(() => {
91
+ req.destroy();
92
+ resolve(null);
93
+ }, FETCH_TIMEOUT_MS + 1000);
94
+ });
95
+ }
96
+
97
+ /**
98
+ * Prompt user for update confirmation
99
+ * @param {string} currentVersion
100
+ * @param {string} latestVersion
101
+ * @returns {Promise<boolean>} True if user wants to update
102
+ */
103
+ function promptForUpdate(currentVersion, latestVersion) {
104
+ return new Promise((resolve) => {
105
+ const rl = readline.createInterface({
106
+ input: process.stdin,
107
+ output: process.stdout,
108
+ });
109
+
110
+ console.log(`\n📦 Update available: ${currentVersion} → ${latestVersion}`);
111
+ rl.question(' Install now? [y/N] ', (answer) => {
112
+ rl.close();
113
+ resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
114
+ });
115
+ });
116
+ }
117
+
118
+ /**
119
+ * Run npm install to update the package
120
+ * @returns {Promise<boolean>} True if update succeeded
121
+ */
122
+ function runUpdate() {
123
+ return new Promise((resolve) => {
124
+ console.log('\n📥 Installing update...');
125
+
126
+ const proc = spawn('npm', ['install', '-g', '@covibes/zeroshot@latest'], {
127
+ stdio: 'inherit',
128
+ shell: true,
129
+ });
130
+
131
+ proc.on('close', (code) => {
132
+ if (code === 0) {
133
+ console.log('✅ Update installed successfully!');
134
+ console.log(' Restart zeroshot to use the new version.\n');
135
+ resolve(true);
136
+ } else {
137
+ console.log('❌ Update failed. Try manually:');
138
+ console.log(' npm install -g @covibes/zeroshot@latest\n');
139
+ resolve(false);
140
+ }
141
+ });
142
+
143
+ proc.on('error', () => {
144
+ console.log('❌ Update failed. Try manually:');
145
+ console.log(' npm install -g @covibes/zeroshot@latest\n');
146
+ resolve(false);
147
+ });
148
+ });
149
+ }
150
+
151
+ /**
152
+ * Check if update check should run
153
+ * @param {object} settings - Current settings
154
+ * @returns {boolean}
155
+ */
156
+ function shouldCheckForUpdates(settings) {
157
+ // Disabled by user
158
+ if (!settings.autoCheckUpdates) {
159
+ return false;
160
+ }
161
+
162
+ // Never checked before
163
+ if (!settings.lastUpdateCheckAt) {
164
+ return true;
165
+ }
166
+
167
+ // Check if 24 hours have passed
168
+ const elapsed = Date.now() - settings.lastUpdateCheckAt;
169
+ return elapsed >= CHECK_INTERVAL_MS;
170
+ }
171
+
172
+ /**
173
+ * Main entry point - check for updates
174
+ * @param {object} options
175
+ * @param {boolean} options.quiet - Skip interactive prompts
176
+ * @returns {Promise<void>}
177
+ */
178
+ async function checkForUpdates(options = {}) {
179
+ const settings = loadSettings();
180
+
181
+ // Skip check if not due
182
+ if (!shouldCheckForUpdates(settings)) {
183
+ return;
184
+ }
185
+
186
+ const currentVersion = getCurrentVersion();
187
+ const latestVersion = await fetchLatestVersion();
188
+
189
+ // Update last check timestamp regardless of result
190
+ settings.lastUpdateCheckAt = Date.now();
191
+ saveSettings(settings);
192
+
193
+ // Network failure - silently skip
194
+ if (!latestVersion) {
195
+ return;
196
+ }
197
+
198
+ // No update available
199
+ if (!isNewerVersion(currentVersion, latestVersion)) {
200
+ return;
201
+ }
202
+
203
+ // Already notified about this version
204
+ if (settings.lastSeenVersion === latestVersion) {
205
+ return;
206
+ }
207
+
208
+ // Update lastSeenVersion so we don't nag about the same version
209
+ settings.lastSeenVersion = latestVersion;
210
+ saveSettings(settings);
211
+
212
+ // Quiet mode - just inform, no prompt
213
+ if (options.quiet) {
214
+ console.log(`📦 Update available: ${currentVersion} → ${latestVersion}`);
215
+ console.log(' Run: npm install -g @covibes/zeroshot@latest\n');
216
+ return;
217
+ }
218
+
219
+ // Interactive mode - prompt for update
220
+ const wantsUpdate = await promptForUpdate(currentVersion, latestVersion);
221
+ if (wantsUpdate) {
222
+ await runUpdate();
223
+ }
224
+ }
225
+
226
+ module.exports = {
227
+ checkForUpdates,
228
+ // Exported for testing
229
+ getCurrentVersion,
230
+ isNewerVersion,
231
+ fetchLatestVersion,
232
+ shouldCheckForUpdates,
233
+ CHECK_INTERVAL_MS,
234
+ };
@@ -1,6 +1,9 @@
1
1
  /**
2
2
  * Normal mode message formatters
3
3
  * Full-detail message display for non-watch mode
4
+ *
5
+ * All functions accept an optional `print` parameter for output routing.
6
+ * When StatusFooter is active, pass safePrint to avoid terminal garbling.
4
7
  */
5
8
 
6
9
  const chalk = require('chalk');
@@ -9,9 +12,10 @@ const chalk = require('chalk');
9
12
  * Format AGENT_LIFECYCLE events
10
13
  * @param {Object} msg - Message object
11
14
  * @param {string} prefix - Formatted message prefix
15
+ * @param {Function} [print=console.log] - Print function for output
12
16
  * @returns {boolean} True if message was handled
13
17
  */
14
- function formatAgentLifecycle(msg, prefix) {
18
+ function formatAgentLifecycle(msg, prefix, print = console.log) {
15
19
  const data = msg.content?.data;
16
20
  const event = data?.event;
17
21
 
@@ -35,7 +39,7 @@ function formatAgentLifecycle(msg, prefix) {
35
39
  eventText = event || 'unknown event';
36
40
  }
37
41
 
38
- console.log(`${prefix} ${icon} ${eventText}`);
42
+ print(`${prefix} ${icon} ${eventText}`);
39
43
  return true;
40
44
  }
41
45
 
@@ -44,27 +48,28 @@ function formatAgentLifecycle(msg, prefix) {
44
48
  * @param {Object} msg - Message object
45
49
  * @param {string} prefix - Formatted message prefix
46
50
  * @param {string} timestamp - Formatted timestamp
51
+ * @param {Function} [print=console.log] - Print function for output
47
52
  * @returns {boolean} True if message was handled
48
53
  */
49
- function formatAgentError(msg, prefix, timestamp) {
50
- console.log(''); // Blank line before error
51
- console.log(chalk.bold.red(`${'─'.repeat(60)}`));
52
- console.log(`${prefix} ${chalk.gray(timestamp)} ${chalk.bold.red('🔴 AGENT ERROR')}`);
54
+ function formatAgentError(msg, prefix, timestamp, print = console.log) {
55
+ print(''); // Blank line before error
56
+ print(chalk.bold.red(`${'─'.repeat(60)}`));
57
+ print(`${prefix} ${chalk.gray(timestamp)} ${chalk.bold.red('🔴 AGENT ERROR')}`);
53
58
 
54
59
  if (msg.content?.text) {
55
- console.log(`${prefix} ${chalk.red(msg.content.text)}`);
60
+ print(`${prefix} ${chalk.red(msg.content.text)}`);
56
61
  }
57
62
 
58
63
  if (msg.content?.data?.stack) {
59
64
  const stackLines = msg.content.data.stack.split('\n').slice(0, 5);
60
65
  for (const line of stackLines) {
61
66
  if (line.trim()) {
62
- console.log(`${prefix} ${chalk.dim(line)}`);
67
+ print(`${prefix} ${chalk.dim(line)}`);
63
68
  }
64
69
  }
65
70
  }
66
71
 
67
- console.log(chalk.bold.red(`${'─'.repeat(60)}`));
72
+ print(chalk.bold.red(`${'─'.repeat(60)}`));
68
73
  return true;
69
74
  }
70
75
 
@@ -74,29 +79,30 @@ function formatAgentError(msg, prefix, timestamp) {
74
79
  * @param {string} prefix - Formatted message prefix
75
80
  * @param {string} timestamp - Formatted timestamp
76
81
  * @param {Set} shownNewTaskForCluster - Set tracking shown tasks
82
+ * @param {Function} [print=console.log] - Print function for output
77
83
  * @returns {boolean} True if message was handled
78
84
  */
79
- function formatIssueOpened(msg, prefix, timestamp, shownNewTaskForCluster) {
85
+ function formatIssueOpened(msg, prefix, timestamp, shownNewTaskForCluster, print = console.log) {
80
86
  // Skip duplicate - conductor re-publishes after spawning agents
81
87
  if (shownNewTaskForCluster.has(msg.cluster_id)) {
82
88
  return true;
83
89
  }
84
90
  shownNewTaskForCluster.add(msg.cluster_id);
85
91
 
86
- console.log(''); // Blank line before new task
87
- console.log(chalk.bold.blue(`${'─'.repeat(60)}`));
88
- console.log(`${prefix} ${chalk.gray(timestamp)} ${chalk.bold.blue('📋 NEW TASK')}`);
92
+ print(''); // Blank line before new task
93
+ print(chalk.bold.blue(`${'─'.repeat(60)}`));
94
+ print(`${prefix} ${chalk.gray(timestamp)} ${chalk.bold.blue('📋 NEW TASK')}`);
89
95
 
90
96
  if (msg.content?.text) {
91
97
  const lines = msg.content.text.split('\n').slice(0, 3);
92
98
  for (const line of lines) {
93
99
  if (line.trim() && line.trim() !== '# Manual Input') {
94
- console.log(`${prefix} ${chalk.white(line)}`);
100
+ print(`${prefix} ${chalk.white(line)}`);
95
101
  }
96
102
  }
97
103
  }
98
104
 
99
- console.log(chalk.bold.blue(`${'─'.repeat(60)}`));
105
+ print(chalk.bold.blue(`${'─'.repeat(60)}`));
100
106
  return true;
101
107
  }
102
108
 
@@ -105,15 +111,16 @@ function formatIssueOpened(msg, prefix, timestamp, shownNewTaskForCluster) {
105
111
  * @param {Object} msg - Message object
106
112
  * @param {string} prefix - Formatted message prefix
107
113
  * @param {string} timestamp - Formatted timestamp
114
+ * @param {Function} [print=console.log] - Print function for output
108
115
  * @returns {boolean} True if message was handled
109
116
  */
110
- function formatImplementationReady(msg, prefix, timestamp) {
111
- console.log(
117
+ function formatImplementationReady(msg, prefix, timestamp, print = console.log) {
118
+ print(
112
119
  `${prefix} ${chalk.gray(timestamp)} ${chalk.bold.yellow('✅ IMPLEMENTATION READY')}`
113
120
  );
114
121
 
115
122
  if (msg.content?.data?.commit) {
116
- console.log(
123
+ print(
117
124
  `${prefix} ${chalk.gray('Commit:')} ${chalk.cyan(msg.content.data.commit.substring(0, 8))}`
118
125
  );
119
126
  }
@@ -126,22 +133,23 @@ function formatImplementationReady(msg, prefix, timestamp) {
126
133
  * @param {Object} msg - Message object
127
134
  * @param {string} prefix - Formatted message prefix
128
135
  * @param {string} timestamp - Formatted timestamp
136
+ * @param {Function} [print=console.log] - Print function for output
129
137
  * @returns {boolean} True if message was handled
130
138
  */
131
- function formatValidationResult(msg, prefix, timestamp) {
139
+ function formatValidationResult(msg, prefix, timestamp, print = console.log) {
132
140
  const data = msg.content?.data || {};
133
141
  const approved = data.approved === true || data.approved === 'true';
134
142
  const status = approved ? chalk.bold.green('✓ APPROVED') : chalk.bold.red('✗ REJECTED');
135
143
 
136
- console.log(`${prefix} ${chalk.gray(timestamp)} ${status}`);
144
+ print(`${prefix} ${chalk.gray(timestamp)} ${status}`);
137
145
 
138
146
  // Show summary if present and not a template variable
139
147
  if (msg.content?.text && !msg.content.text.includes('{{')) {
140
- console.log(`${prefix} ${msg.content.text.substring(0, 100)}`);
148
+ print(`${prefix} ${msg.content.text.substring(0, 100)}`);
141
149
  }
142
150
 
143
151
  // Show full JSON data structure
144
- console.log(
152
+ print(
145
153
  `${prefix} ${chalk.dim(JSON.stringify(data, null, 2).split('\n').join(`\n${prefix} `))}`
146
154
  );
147
155
 
@@ -153,16 +161,17 @@ function formatValidationResult(msg, prefix, timestamp) {
153
161
  * @param {Object} msg - Message object
154
162
  * @param {string} prefix - Formatted message prefix
155
163
  * @param {string} timestamp - Formatted timestamp
164
+ * @param {Function} [print=console.log] - Print function for output
156
165
  * @returns {boolean} True if message was handled
157
166
  */
158
- function formatClusterComplete(msg, prefix, timestamp) {
159
- console.log(''); // Blank line
160
- console.log(chalk.bold.green(`${'═'.repeat(60)}`));
161
- console.log(`${prefix} ${chalk.gray(timestamp)} ${chalk.bold.green('🎉 CLUSTER COMPLETE')}`);
167
+ function formatClusterComplete(msg, prefix, timestamp, print = console.log) {
168
+ print(''); // Blank line
169
+ print(chalk.bold.green(`${'═'.repeat(60)}`));
170
+ print(`${prefix} ${chalk.gray(timestamp)} ${chalk.bold.green('🎉 CLUSTER COMPLETE')}`);
162
171
  if (msg.content?.data?.reason) {
163
- console.log(`${prefix} ${chalk.green(msg.content.data.reason)}`);
172
+ print(`${prefix} ${chalk.green(msg.content.data.reason)}`);
164
173
  }
165
- console.log(chalk.bold.green(`${'═'.repeat(60)}`));
174
+ print(chalk.bold.green(`${'═'.repeat(60)}`));
166
175
  return true;
167
176
  }
168
177
 
@@ -171,19 +180,47 @@ function formatClusterComplete(msg, prefix, timestamp) {
171
180
  * @param {Object} msg - Message object
172
181
  * @param {string} prefix - Formatted message prefix
173
182
  * @param {string} timestamp - Formatted timestamp
183
+ * @param {Function} [print=console.log] - Print function for output
174
184
  * @returns {boolean} True if message was handled
175
185
  */
176
- function formatClusterFailed(msg, prefix, timestamp) {
177
- console.log(''); // Blank line
178
- console.log(chalk.bold.red(`${'═'.repeat(60)}`));
179
- console.log(`${prefix} ${chalk.gray(timestamp)} ${chalk.bold.red('❌ CLUSTER FAILED')}`);
186
+ function formatClusterFailed(msg, prefix, timestamp, print = console.log) {
187
+ print(''); // Blank line
188
+ print(chalk.bold.red(`${'═'.repeat(60)}`));
189
+ print(`${prefix} ${chalk.gray(timestamp)} ${chalk.bold.red('❌ CLUSTER FAILED')}`);
180
190
  if (msg.content?.text) {
181
- console.log(`${prefix} ${chalk.red(msg.content.text)}`);
191
+ print(`${prefix} ${chalk.red(msg.content.text)}`);
182
192
  }
183
193
  if (msg.content?.data?.reason) {
184
- console.log(`${prefix} ${chalk.red(msg.content.data.reason)}`);
194
+ print(`${prefix} ${chalk.red(msg.content.data.reason)}`);
185
195
  }
186
- console.log(chalk.bold.red(`${'═'.repeat(60)}`));
196
+ print(chalk.bold.red(`${'═'.repeat(60)}`));
197
+ return true;
198
+ }
199
+
200
+ /**
201
+ * Format PR_CREATED events
202
+ * @param {Object} msg - Message object
203
+ * @param {string} prefix - Formatted message prefix
204
+ * @param {string} timestamp - Formatted timestamp
205
+ * @param {Function} [print=console.log] - Print function for output
206
+ * @returns {boolean} True if message was handled
207
+ */
208
+ function formatPrCreated(msg, prefix, timestamp, print = console.log) {
209
+ const prNumber = msg.content?.data?.pr_number || '';
210
+ const prUrl = msg.content?.data?.pr_url || '';
211
+
212
+ print(''); // Blank line before PR notification
213
+ print(chalk.bold.green(`${'─'.repeat(60)}`));
214
+ print(`${prefix} ${chalk.gray(timestamp)} ${chalk.bold.green('🎉 PULL REQUEST CREATED')}`);
215
+
216
+ if (prNumber) {
217
+ print(`${prefix} ${chalk.gray('PR:')} ${chalk.cyan(`#${prNumber}`)}`);
218
+ }
219
+ if (prUrl) {
220
+ print(`${prefix} ${chalk.gray('URL:')} ${chalk.blue(prUrl)}`);
221
+ }
222
+
223
+ print(chalk.bold.green(`${'─'.repeat(60)}`));
187
224
  return true;
188
225
  }
189
226
 
@@ -192,12 +229,13 @@ function formatClusterFailed(msg, prefix, timestamp) {
192
229
  * @param {Object} msg - Message object
193
230
  * @param {string} prefix - Formatted message prefix
194
231
  * @param {string} timestamp - Formatted timestamp
232
+ * @param {Function} [print=console.log] - Print function for output
195
233
  * @returns {boolean} True if message was handled
196
234
  */
197
- function formatGenericMessage(msg, prefix, timestamp) {
198
- console.log(`${prefix} ${chalk.gray(timestamp)} ${chalk.bold(msg.topic)}`);
235
+ function formatGenericMessage(msg, prefix, timestamp, print = console.log) {
236
+ print(`${prefix} ${chalk.gray(timestamp)} ${chalk.bold(msg.topic)}`);
199
237
  if (msg.content?.text) {
200
- console.log(`${prefix} ${msg.content.text}`);
238
+ print(`${prefix} ${msg.content.text}`);
201
239
  }
202
240
  return true;
203
241
  }
@@ -208,6 +246,7 @@ module.exports = {
208
246
  formatIssueOpened,
209
247
  formatImplementationReady,
210
248
  formatValidationResult,
249
+ formatPrCreated,
211
250
  formatClusterComplete,
212
251
  formatClusterFailed,
213
252
  formatGenericMessage,