@link-assistant/hive-mind 0.42.3 → 0.44.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,67 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 0.44.0
4
+
5
+ ### Minor Changes
6
+
7
+ - b72136f: Add /version command to hive-telegram-bot
8
+
9
+ Implements a new /version command that displays comprehensive version information including:
10
+
11
+ - Bot version (package version with git commit SHA in development)
12
+ - solve and hive command versions
13
+ - Node.js runtime version
14
+ - Platform information (OS and architecture)
15
+
16
+ This helps users and administrators quickly check version information without accessing logs or the server directly.
17
+
18
+ ### Patch Changes
19
+
20
+ - 445091b: Fix Perl version detection in ubuntu-24-server-install.sh
21
+
22
+ The `perlbrew available` command output was not being parsed correctly, causing the installation script to skip Perl installation with the message "Could not determine latest Perl version."
23
+
24
+ **Changes:**
25
+
26
+ - Use `grep -oE` to robustly extract Perl version strings regardless of line formatting
27
+ - Capture stderr from `perlbrew available` for better debugging
28
+ - Add debug output showing `perlbrew available` response when version detection fails
29
+ - Works with 'i' markers for already-installed versions and variable indentation
30
+
31
+ This ensures the latest Perl version is properly detected and installed via perlbrew.
32
+
33
+ Fixes #948
34
+
35
+ ## 0.43.0
36
+
37
+ ### Minor Changes
38
+
39
+ - fe002f8: Add --prompt-issue-reporting flag for automatic issue creation
40
+
41
+ This release introduces a new opt-in feature that enables the AI to automatically create GitHub issues when it spots bugs, errors, or minor issues during working sessions that are not related to the main task.
42
+
43
+ **New Features:**
44
+
45
+ - Added `--prompt-issue-reporting` CLI flag (disabled by default)
46
+ - Issues include reproducible examples, workarounds, and fix suggestions
47
+ - Supports creating issues in both current and third-party repositories
48
+ - Automatic duplicate checking before creating issues
49
+
50
+ **Usage:**
51
+
52
+ ```bash
53
+ hive solve <issue-url> --prompt-issue-reporting
54
+ solve <issue-url> --prompt-issue-reporting
55
+ ```
56
+
57
+ **Implementation:**
58
+
59
+ - New guideline in system prompt (conditional on flag)
60
+ - Flag added to both `hive` and `solve` commands
61
+ - Uses `gh` CLI for authenticated issue creation (works with private repos)
62
+
63
+ This feature helps ensure that no bugs slip through the cracks during development while giving users full control over when it's active.
64
+
3
65
  ## 0.42.3
4
66
 
5
67
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "0.42.3",
3
+ "version": "0.44.0",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -115,6 +115,7 @@ General guidelines.
115
115
  - When you execute commands, always save their logs to files for easier reading if the output becomes large.
116
116
  - When running commands, do not set a timeout yourself — let them run as long as needed (default timeout - 2 minutes is more than enough), and once they finish, review the logs in the file.
117
117
  - When running sudo commands (especially package installations like apt-get, yum, npm install, etc.), always run them in the background to avoid timeout issues and permission errors when the process needs to be killed. Use the run_in_background parameter or append & to the command.
118
+ ${argv && argv.promptIssueReporting ? ` - When you spot any errors or bugs or minor issues during working session that are not related to requirements of the main task, create issues to track them if they do not exist yet. The issue should contain reproducible examples (ideally minimum reproducible example), workarounds, and suggestions for fixing the issue in code. For issues in the current repository, use gh issue create --repo ${owner}/${repo} --title "Issue title" --body "Issue description". For issues in third-party repositories on GitHub (libraries/components used in the working repository), use gh issue create --repo owner/repo --title "Issue title" --body "Issue description". Always check first if similar issues already exist using gh issue list --repo owner/repo --search "keywords" to avoid duplicates. If a similar issue already exists, add a comment to that issue using gh issue comment <issue-number> --repo owner/repo --body "Comment text" describing your specific case, including logs (anonymized with redacted personal and sensitive data), ways to reproduce, ideally minimum reproducible example, workarounds, and suggestions for fix - similar to how you would describe the issue if it didn't exist yet.` : ''}
118
119
  - When CI is failing or user reports failures, consider adding a detailed investigation protocol to your todo list with these steps:
119
120
  Step 1: List recent runs with timestamps using: gh run list --repo ${owner}/${repo} --branch ${branchName} --limit 5 --json databaseId,conclusion,createdAt,headSha
120
121
  Step 2: Verify runs are after the latest commit by checking timestamps and SHA
@@ -251,6 +251,11 @@ export const createYargsConfig = (yargsInstance) => {
251
251
  description: 'Prompt AI to use general-purpose sub agents for processing large tasks with multiple files/folders. Only supported for --tool claude.',
252
252
  default: false
253
253
  })
254
+ .option('prompt-issue-reporting', {
255
+ type: 'boolean',
256
+ description: 'Enable automatic issue creation for spotted bugs/errors not related to main task. Issues will include reproducible examples, workarounds, and fix suggestions. Works for both current and third-party repositories. Only supported for --tool claude.',
257
+ default: false
258
+ })
254
259
  .parserConfiguration({
255
260
  'boolean-negation': true,
256
261
  'strip-dashed': false,
package/src/hive.mjs CHANGED
@@ -757,6 +757,7 @@ async function worker(workerId) {
757
757
  const prefixForkNameWithOwnerNameFlag = argv.prefixForkNameWithOwnerName ? ' --prefix-fork-name-with-owner-name' : '';
758
758
  const interactiveModeFlag = argv.interactiveMode ? ' --interactive-mode' : '';
759
759
  const promptExploreSubAgentFlag = argv.promptExploreSubAgent ? ' --prompt-explore-sub-agent' : '';
760
+ const promptIssueReportingFlag = argv.promptIssueReporting ? ' --prompt-issue-reporting' : '';
760
761
 
761
762
  // Use spawn to get real-time streaming output while avoiding command-stream's automatic quote addition
762
763
  const { spawn } = await import('child_process');
@@ -806,9 +807,10 @@ async function worker(workerId) {
806
807
  if (argv.prefixForkNameWithOwnerName) args.push('--prefix-fork-name-with-owner-name');
807
808
  if (argv.interactiveMode) args.push('--interactive-mode');
808
809
  if (argv.promptExploreSubAgent) args.push('--prompt-explore-sub-agent');
810
+ if (argv.promptIssueReporting) args.push('--prompt-issue-reporting');
809
811
 
810
812
  // Log the actual command being executed so users can investigate/reproduce
811
- const command = `${solveCommand} "${issueUrl}" --model ${argv.model}${toolFlag}${forkFlag}${autoForkFlag}${verboseFlag}${attachLogsFlag}${targetBranchFlag}${logDirFlag}${dryRunFlag}${skipToolConnectionCheckFlag}${autoContinueFlag}${thinkFlag}${promptPlanSubAgentFlag}${noSentryFlag}${watchFlag}${prefixForkNameWithOwnerNameFlag}${interactiveModeFlag}${promptExploreSubAgentFlag}`;
813
+ const command = `${solveCommand} "${issueUrl}" --model ${argv.model}${toolFlag}${forkFlag}${autoForkFlag}${verboseFlag}${attachLogsFlag}${targetBranchFlag}${logDirFlag}${dryRunFlag}${skipToolConnectionCheckFlag}${autoContinueFlag}${thinkFlag}${promptPlanSubAgentFlag}${noSentryFlag}${watchFlag}${prefixForkNameWithOwnerNameFlag}${interactiveModeFlag}${promptExploreSubAgentFlag}${promptIssueReportingFlag}`;
812
814
  await log(` 📋 Command: ${command}`);
813
815
 
814
816
  let exitCode = 0;
@@ -1496,4 +1498,4 @@ try {
1496
1498
  process.exit(1);
1497
1499
  }
1498
1500
 
1499
- } // End of main execution block
1501
+ } // End of main execution block
@@ -255,6 +255,11 @@ export const createYargsConfig = (yargsInstance) => {
255
255
  description: 'Prompt AI to use general-purpose sub agents for processing large tasks with multiple files/folders. Only supported for --tool claude.',
256
256
  default: false
257
257
  })
258
+ .option('prompt-issue-reporting', {
259
+ type: 'boolean',
260
+ description: 'Enable automatic issue creation for spotted bugs/errors not related to main task. Issues will include reproducible examples, workarounds, and fix suggestions. Works for both current and third-party repositories. Only supported for --tool claude.',
261
+ default: false
262
+ })
258
263
  .parserConfiguration({
259
264
  'boolean-negation': true
260
265
  })
@@ -47,6 +47,9 @@ const { validateModelName } = await import('./model-validation.lib.mjs');
47
47
  // Import Claude limits library for /limits command
48
48
  const { getClaudeUsageLimits, formatUsageMessage } = await import('./claude-limits.lib.mjs');
49
49
 
50
+ // Import version info library for /version command
51
+ const { getVersionInfo, formatVersionMessage } = await import('./version-info.lib.mjs');
52
+
50
53
  // Import Telegram markdown escaping utilities
51
54
  const { escapeMarkdown, escapeMarkdownV2 } = await import('./telegram-markdown.lib.mjs');
52
55
 
@@ -762,11 +765,9 @@ bot.command('help', async (ctx) => {
762
765
  }
763
766
 
764
767
  message += '*/limits* - Show Claude usage limits\n';
765
- message += 'Usage: `/limits`\n';
766
- message += 'Shows current session and weekly usage percentages\n\n';
767
-
768
+ message += '*/version* - Show bot and runtime versions\n';
768
769
  message += '*/help* - Show this help message\n\n';
769
- message += '⚠️ *Note:* /solve, /hive and /limits commands only work in group chats.\n\n';
770
+ message += '⚠️ *Note:* /solve, /hive, /limits and /version commands only work in group chats.\n\n';
770
771
  message += '🔧 *Available Options:*\n';
771
772
  message += '• `--fork` - Fork the repository\n';
772
773
  message += '• `--auto-fork` - Automatically fork public repos without write access\n';
@@ -875,7 +876,18 @@ bot.command('limits', async (ctx) => {
875
876
  { parse_mode: 'Markdown' }
876
877
  );
877
878
  });
878
-
879
+ bot.command('version', async (ctx) => {
880
+ VERBOSE && console.log('[VERBOSE] /version command received');
881
+ await addBreadcrumb({ category: 'telegram.command', message: '/version command received', level: 'info', data: { chatId: ctx.chat?.id, chatType: ctx.chat?.type, userId: ctx.from?.id, username: ctx.from?.username } });
882
+ if (isOldMessage(ctx) || isForwardedOrReply(ctx)) return;
883
+ if (!isGroupChat(ctx)) return await ctx.reply('❌ The /version command only works in group chats. Please add this bot to a group and make it an admin.', { reply_to_message_id: ctx.message.message_id });
884
+ const chatId = ctx.chat.id;
885
+ if (!isChatAuthorized(chatId)) return await ctx.reply(`❌ This chat (ID: ${chatId}) is not authorized to use this bot. Please contact the bot administrator.`, { reply_to_message_id: ctx.message.message_id });
886
+ const fetchingMessage = await ctx.reply('🔄 Gathering version information...', { reply_to_message_id: ctx.message.message_id });
887
+ const result = await getVersionInfo(VERBOSE);
888
+ if (!result.success) return await ctx.telegram.editMessageText(fetchingMessage.chat.id, fetchingMessage.message_id, undefined, `❌ ${escapeMarkdownV2(result.error, { preserveCodeBlocks: true })}`, { parse_mode: 'MarkdownV2' });
889
+ await ctx.telegram.editMessageText(fetchingMessage.chat.id, fetchingMessage.message_id, undefined, '🤖 *Version Information*\n\n' + formatVersionMessage(result.versions), { parse_mode: 'Markdown' });
890
+ });
879
891
  bot.command(/^solve$/i, async (ctx) => {
880
892
  if (VERBOSE) {
881
893
  console.log('[VERBOSE] /solve command received');
@@ -1264,14 +1276,12 @@ if (VERBOSE) {
1264
1276
  bot.catch((error, ctx) => {
1265
1277
  console.error('Unhandled error while processing update', ctx.update.update_id);
1266
1278
  console.error('Error:', error);
1267
-
1268
1279
  // Log detailed error information
1269
1280
  console.error('Error details:', {
1270
1281
  name: error.name,
1271
1282
  message: error.message,
1272
1283
  stack: error.stack?.split('\n').slice(0, 10).join('\n'),
1273
1284
  });
1274
-
1275
1285
  // Log context information for debugging
1276
1286
  if (VERBOSE) {
1277
1287
  console.log('[VERBOSE] Error context:', {
@@ -0,0 +1,319 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Version information library for hive-mind project
4
+ // Provides comprehensive version information for bot, commands, and runtime
5
+
6
+ import { getVersion } from './version.lib.mjs';
7
+ import { execSync } from 'child_process';
8
+
9
+ /**
10
+ * Execute a command and return its output, or null if it fails
11
+ * @param {string} command - Command to execute
12
+ * @param {boolean} verbose - Enable verbose logging
13
+ * @returns {string|null} Command output or null
14
+ */
15
+ function execCommand(command, verbose = false) {
16
+ try {
17
+ const result = execSync(command, { encoding: 'utf8', timeout: 5000, stdio: ['pipe', 'pipe', 'ignore'] });
18
+ const trimmed = result.trim();
19
+ // Return null if the output looks like an error message
20
+ if (trimmed.includes('not found') || trimmed.includes('command not found') || trimmed === '') {
21
+ return null;
22
+ }
23
+ return trimmed;
24
+ } catch (error) {
25
+ if (verbose) {
26
+ console.log(`[VERBOSE] Command failed: ${command}`, error.message);
27
+ }
28
+ return null;
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Get comprehensive version information for all components
34
+ * @param {boolean} verbose - Enable verbose logging
35
+ * @returns {Promise<Object>} Version information object
36
+ */
37
+ export async function getVersionInfo(verbose = false) {
38
+ try {
39
+ if (verbose) {
40
+ console.log('[VERBOSE] Gathering version information...');
41
+ }
42
+
43
+ // Get hive-mind package version
44
+ const packageVersion = await getVersion();
45
+ if (verbose) {
46
+ console.log(`[VERBOSE] Package version: ${packageVersion}`);
47
+ }
48
+
49
+ // === Agents ===
50
+
51
+ // Claude Code
52
+ const claudeVersion = execCommand('claude --version 2>&1', verbose);
53
+ if (verbose && claudeVersion) {
54
+ console.log(`[VERBOSE] Claude Code version: ${claudeVersion}`);
55
+ }
56
+
57
+ // Playwright
58
+ const playwrightVersion = execCommand('playwright --version 2>&1', verbose);
59
+ if (verbose && playwrightVersion) {
60
+ console.log(`[VERBOSE] Playwright version: ${playwrightVersion}`);
61
+ }
62
+
63
+ // Playwright MCP (check if installed via npm)
64
+ const playwrightMcpVersion = execCommand('npm list -g @playwright/mcp --depth=0 2>&1 | grep @playwright/mcp | awk \'{print $2}\'', verbose);
65
+ if (verbose && playwrightMcpVersion) {
66
+ console.log(`[VERBOSE] Playwright MCP version: ${playwrightMcpVersion}`);
67
+ }
68
+
69
+ // === Language Runtimes ===
70
+
71
+ // Node.js (from process, always available)
72
+ const nodeVersion = process.version;
73
+ if (verbose) {
74
+ console.log(`[VERBOSE] Node.js version: ${nodeVersion}`);
75
+ }
76
+
77
+ // Python
78
+ const pythonVersion = execCommand('python --version 2>&1', verbose);
79
+ if (verbose && pythonVersion) {
80
+ console.log(`[VERBOSE] Python version: ${pythonVersion}`);
81
+ }
82
+
83
+ // Pyenv
84
+ const pyenvVersion = execCommand('pyenv --version 2>&1', verbose);
85
+ if (verbose && pyenvVersion) {
86
+ console.log(`[VERBOSE] Pyenv version: ${pyenvVersion}`);
87
+ }
88
+
89
+ // Rust
90
+ const rustVersion = execCommand('rustc --version 2>&1', verbose);
91
+ if (verbose && rustVersion) {
92
+ console.log(`[VERBOSE] Rust version: ${rustVersion}`);
93
+ }
94
+
95
+ // Cargo
96
+ const cargoVersion = execCommand('cargo --version 2>&1', verbose);
97
+ if (verbose && cargoVersion) {
98
+ console.log(`[VERBOSE] Cargo version: ${cargoVersion}`);
99
+ }
100
+
101
+ // PHP
102
+ const phpVersion = execCommand('php --version 2>&1 | head -n1', verbose);
103
+ if (verbose && phpVersion) {
104
+ console.log(`[VERBOSE] PHP version: ${phpVersion}`);
105
+ }
106
+
107
+ // Bun
108
+ const bunVersion = execCommand('bun --version 2>&1', verbose);
109
+ if (verbose && bunVersion) {
110
+ console.log(`[VERBOSE] Bun version: ${bunVersion}`);
111
+ }
112
+
113
+ // .NET
114
+ const dotnetVersion = execCommand('dotnet --version 2>&1', verbose);
115
+ if (verbose && dotnetVersion) {
116
+ console.log(`[VERBOSE] .NET version: ${dotnetVersion}`);
117
+ }
118
+
119
+ // === Development Tools ===
120
+
121
+ // Git
122
+ const gitVersion = execCommand('git --version 2>&1', verbose);
123
+ if (verbose && gitVersion) {
124
+ console.log(`[VERBOSE] Git version: ${gitVersion}`);
125
+ }
126
+
127
+ // GitHub CLI
128
+ const ghVersion = execCommand('gh --version 2>&1 | head -n1', verbose);
129
+ if (verbose && ghVersion) {
130
+ console.log(`[VERBOSE] GitHub CLI version: ${ghVersion}`);
131
+ }
132
+
133
+ // NVM
134
+ const nvmVersion = execCommand('nvm --version 2>&1', verbose);
135
+ if (verbose && nvmVersion) {
136
+ console.log(`[VERBOSE] NVM version: ${nvmVersion}`);
137
+ }
138
+
139
+ // Homebrew
140
+ const brewVersion = execCommand('brew --version 2>&1 | head -n1', verbose);
141
+ if (verbose && brewVersion) {
142
+ console.log(`[VERBOSE] Homebrew version: ${brewVersion}`);
143
+ }
144
+
145
+ // NPM
146
+ const npmVersion = execCommand('npm --version 2>&1', verbose);
147
+ if (verbose && npmVersion) {
148
+ console.log(`[VERBOSE] NPM version: ${npmVersion}`);
149
+ }
150
+
151
+ // === Platform Information ===
152
+ const platform = process.platform;
153
+ const arch = process.arch;
154
+ if (verbose) {
155
+ console.log(`[VERBOSE] Platform: ${platform} (${arch})`);
156
+ }
157
+
158
+ // Build version info object
159
+ const versionInfo = {
160
+ success: true,
161
+ versions: {
162
+ // Bot components
163
+ bot: packageVersion,
164
+ solve: packageVersion,
165
+ hive: packageVersion,
166
+
167
+ // Agents
168
+ claudeCode: claudeVersion,
169
+ playwright: playwrightVersion,
170
+ playwrightMcp: playwrightMcpVersion,
171
+
172
+ // Language runtimes
173
+ node: nodeVersion,
174
+ python: pythonVersion,
175
+ rust: rustVersion,
176
+ php: phpVersion,
177
+ bun: bunVersion,
178
+ dotnet: dotnetVersion,
179
+
180
+ // Development tools
181
+ git: gitVersion,
182
+ gh: ghVersion,
183
+ npm: npmVersion,
184
+ nvm: nvmVersion,
185
+ pyenv: pyenvVersion,
186
+ cargo: cargoVersion,
187
+ brew: brewVersion,
188
+
189
+ // Platform
190
+ platform: `${platform} (${arch})`,
191
+ },
192
+ };
193
+
194
+ if (verbose) {
195
+ console.log('[VERBOSE] Version info gathered successfully:', JSON.stringify(versionInfo, null, 2));
196
+ }
197
+
198
+ return versionInfo;
199
+ } catch (error) {
200
+ if (verbose) {
201
+ console.error('[VERBOSE] Error gathering version info:', error);
202
+ }
203
+
204
+ return {
205
+ success: false,
206
+ error: error.message || 'Failed to gather version information',
207
+ };
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Format version information as a Telegram message
213
+ * @param {Object} versions - Version information object
214
+ * @returns {string} Formatted message
215
+ */
216
+ export function formatVersionMessage(versions) {
217
+ const lines = [];
218
+
219
+ // === Bot Components ===
220
+ lines.push('*🤖 Bot Components*');
221
+ if (versions.bot) {
222
+ lines.push(`• Bot: \`${versions.bot}\``);
223
+ }
224
+ if (versions.solve) {
225
+ lines.push(`• solve: \`${versions.solve}\``);
226
+ }
227
+ if (versions.hive) {
228
+ lines.push(`• hive: \`${versions.hive}\``);
229
+ }
230
+
231
+ // === Agents ===
232
+ const agentLines = [];
233
+ if (versions.claudeCode) {
234
+ agentLines.push(`• Claude Code: \`${versions.claudeCode}\``);
235
+ }
236
+ if (versions.playwright) {
237
+ agentLines.push(`• Playwright: \`${versions.playwright}\``);
238
+ }
239
+ if (versions.playwrightMcp) {
240
+ agentLines.push(`• Playwright MCP: \`${versions.playwrightMcp}\``);
241
+ }
242
+
243
+ if (agentLines.length > 0) {
244
+ lines.push('');
245
+ lines.push('*🎭 Agents*');
246
+ lines.push(...agentLines);
247
+ }
248
+
249
+ // === Language Runtimes ===
250
+ const runtimeLines = [];
251
+ if (versions.node) {
252
+ runtimeLines.push(`• Node.js: \`${versions.node}\``);
253
+ }
254
+ if (versions.python) {
255
+ runtimeLines.push(`• Python: \`${versions.python}\``);
256
+ }
257
+ if (versions.rust) {
258
+ runtimeLines.push(`• Rust: \`${versions.rust}\``);
259
+ }
260
+ if (versions.php) {
261
+ runtimeLines.push(`• PHP: \`${versions.php}\``);
262
+ }
263
+ if (versions.bun) {
264
+ runtimeLines.push(`• Bun: \`${versions.bun}\``);
265
+ }
266
+ if (versions.dotnet) {
267
+ runtimeLines.push(`• .NET: \`${versions.dotnet}\``);
268
+ }
269
+
270
+ if (runtimeLines.length > 0) {
271
+ lines.push('');
272
+ lines.push('*⚙️ Language Runtimes*');
273
+ lines.push(...runtimeLines);
274
+ }
275
+
276
+ // === Development Tools ===
277
+ const toolLines = [];
278
+ if (versions.git) {
279
+ toolLines.push(`• Git: \`${versions.git}\``);
280
+ }
281
+ if (versions.gh) {
282
+ toolLines.push(`• GitHub CLI: \`${versions.gh}\``);
283
+ }
284
+ if (versions.npm) {
285
+ toolLines.push(`• NPM: \`${versions.npm}\``);
286
+ }
287
+ if (versions.nvm) {
288
+ toolLines.push(`• NVM: \`${versions.nvm}\``);
289
+ }
290
+ if (versions.pyenv) {
291
+ toolLines.push(`• Pyenv: \`${versions.pyenv}\``);
292
+ }
293
+ if (versions.cargo) {
294
+ toolLines.push(`• Cargo: \`${versions.cargo}\``);
295
+ }
296
+ if (versions.brew) {
297
+ toolLines.push(`• Homebrew: \`${versions.brew}\``);
298
+ }
299
+
300
+ if (toolLines.length > 0) {
301
+ lines.push('');
302
+ lines.push('*🛠 Development Tools*');
303
+ lines.push(...toolLines);
304
+ }
305
+
306
+ // === Platform ===
307
+ if (versions.platform) {
308
+ lines.push('');
309
+ lines.push('*💻 Platform*');
310
+ lines.push(`• System: \`${versions.platform}\``);
311
+ }
312
+
313
+ return lines.join('\n');
314
+ }
315
+
316
+ export default {
317
+ getVersionInfo,
318
+ formatVersionMessage,
319
+ };