@link-assistant/hive-mind 0.39.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.
Files changed (63) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/LICENSE +24 -0
  3. package/README.md +769 -0
  4. package/package.json +58 -0
  5. package/src/agent.lib.mjs +705 -0
  6. package/src/agent.prompts.lib.mjs +196 -0
  7. package/src/buildUserMention.lib.mjs +71 -0
  8. package/src/claude-limits.lib.mjs +389 -0
  9. package/src/claude.lib.mjs +1445 -0
  10. package/src/claude.prompts.lib.mjs +203 -0
  11. package/src/codex.lib.mjs +552 -0
  12. package/src/codex.prompts.lib.mjs +194 -0
  13. package/src/config.lib.mjs +207 -0
  14. package/src/contributing-guidelines.lib.mjs +268 -0
  15. package/src/exit-handler.lib.mjs +205 -0
  16. package/src/git.lib.mjs +145 -0
  17. package/src/github-issue-creator.lib.mjs +246 -0
  18. package/src/github-linking.lib.mjs +152 -0
  19. package/src/github.batch.lib.mjs +272 -0
  20. package/src/github.graphql.lib.mjs +258 -0
  21. package/src/github.lib.mjs +1479 -0
  22. package/src/hive.config.lib.mjs +254 -0
  23. package/src/hive.mjs +1500 -0
  24. package/src/instrument.mjs +191 -0
  25. package/src/interactive-mode.lib.mjs +1000 -0
  26. package/src/lenv-reader.lib.mjs +206 -0
  27. package/src/lib.mjs +490 -0
  28. package/src/lino.lib.mjs +176 -0
  29. package/src/local-ci-checks.lib.mjs +324 -0
  30. package/src/memory-check.mjs +419 -0
  31. package/src/model-mapping.lib.mjs +145 -0
  32. package/src/model-validation.lib.mjs +278 -0
  33. package/src/opencode.lib.mjs +479 -0
  34. package/src/opencode.prompts.lib.mjs +194 -0
  35. package/src/protect-branch.mjs +159 -0
  36. package/src/review.mjs +433 -0
  37. package/src/reviewers-hive.mjs +643 -0
  38. package/src/sentry.lib.mjs +284 -0
  39. package/src/solve.auto-continue.lib.mjs +568 -0
  40. package/src/solve.auto-pr.lib.mjs +1374 -0
  41. package/src/solve.branch-errors.lib.mjs +341 -0
  42. package/src/solve.branch.lib.mjs +230 -0
  43. package/src/solve.config.lib.mjs +342 -0
  44. package/src/solve.error-handlers.lib.mjs +256 -0
  45. package/src/solve.execution.lib.mjs +291 -0
  46. package/src/solve.feedback.lib.mjs +436 -0
  47. package/src/solve.mjs +1128 -0
  48. package/src/solve.preparation.lib.mjs +210 -0
  49. package/src/solve.repo-setup.lib.mjs +114 -0
  50. package/src/solve.repository.lib.mjs +961 -0
  51. package/src/solve.results.lib.mjs +558 -0
  52. package/src/solve.session.lib.mjs +135 -0
  53. package/src/solve.validation.lib.mjs +325 -0
  54. package/src/solve.watch.lib.mjs +572 -0
  55. package/src/start-screen.mjs +324 -0
  56. package/src/task.mjs +308 -0
  57. package/src/telegram-bot.mjs +1481 -0
  58. package/src/telegram-markdown.lib.mjs +64 -0
  59. package/src/usage-limit.lib.mjs +218 -0
  60. package/src/version.lib.mjs +41 -0
  61. package/src/youtrack/solve.youtrack.lib.mjs +116 -0
  62. package/src/youtrack/youtrack-sync.mjs +219 -0
  63. package/src/youtrack/youtrack.lib.mjs +425 -0
@@ -0,0 +1,205 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Centralized exit handler to ensure log path is always displayed
5
+ * This module ensures that the absolute log path is shown whenever
6
+ * the process exits, whether due to normal completion, errors, or signals.
7
+ */
8
+
9
+ // Lazy-load Sentry to avoid keeping the event loop alive when not needed
10
+ let Sentry = null;
11
+ const getSentry = async () => {
12
+ if (Sentry === null) {
13
+ try {
14
+ Sentry = await import('@sentry/node');
15
+ } catch {
16
+ // If Sentry is not available, just return null
17
+ Sentry = { close: async () => {} };
18
+ }
19
+ }
20
+ return Sentry;
21
+ };
22
+
23
+ // Keep track of whether we've already shown the exit message
24
+ let exitMessageShown = false;
25
+ let getLogPathFunction = null;
26
+ let logFunction = null;
27
+ let cleanupFunction = null;
28
+
29
+ /**
30
+ * Initialize the exit handler with required dependencies
31
+ * @param {Function} getLogPath - Function that returns the current log path
32
+ * @param {Function} log - Logging function
33
+ * @param {Function} cleanup - Optional cleanup function to call on exit
34
+ */
35
+ export const initializeExitHandler = (getLogPath, log, cleanup = null) => {
36
+ getLogPathFunction = getLogPath;
37
+ logFunction = log;
38
+ cleanupFunction = cleanup;
39
+ };
40
+
41
+ /**
42
+ * Display the exit message with log path
43
+ */
44
+ const showExitMessage = async (reason = 'Process exiting', code = 0) => {
45
+ if (exitMessageShown || !getLogPathFunction || !logFunction) {
46
+ return;
47
+ }
48
+
49
+ exitMessageShown = true;
50
+
51
+ // Get the current log path dynamically
52
+ const currentLogPath = await getLogPathFunction();
53
+
54
+ // Always show the log path on exit
55
+ await logFunction('');
56
+ if (code === 0) {
57
+ await logFunction(`āœ… ${reason}`);
58
+ } else {
59
+ await logFunction(`āŒ ${reason}`, { level: 'error' });
60
+ }
61
+ await logFunction(`šŸ“ Full log file: ${currentLogPath}`);
62
+ };
63
+
64
+ /**
65
+ * Safe exit function that ensures log path is shown
66
+ */
67
+ export const safeExit = async (code = 0, reason = 'Process completed') => {
68
+ await showExitMessage(reason, code);
69
+
70
+ // Close Sentry to flush any pending events and allow the process to exit cleanly
71
+ try {
72
+ const sentry = await getSentry();
73
+ if (sentry && sentry.close) {
74
+ await sentry.close(2000); // Wait up to 2 seconds for pending events to be sent
75
+ }
76
+ } catch {
77
+ // Ignore Sentry.close() errors - exit anyway
78
+ }
79
+
80
+ process.exit(code);
81
+ };
82
+
83
+ /**
84
+ * Install global exit handlers to ensure log path is always shown
85
+ */
86
+ export const installGlobalExitHandlers = () => {
87
+ // Handle normal exit
88
+ process.on('exit', (code) => {
89
+ // Synchronous fallback - can't use async here
90
+ if (!exitMessageShown && getLogPathFunction) {
91
+ try {
92
+ // Try to get the current log path synchronously if possible
93
+ const currentLogPath = getLogPathFunction();
94
+ if (currentLogPath && typeof currentLogPath === 'string') {
95
+ console.log('');
96
+ if (code === 0) {
97
+ console.log('āœ… Process completed');
98
+ } else {
99
+ console.log(`āŒ Process exited with code ${code}`);
100
+ }
101
+ console.log(`šŸ“ Full log file: ${currentLogPath}`);
102
+ }
103
+ } catch {
104
+ // If we can't get the log path synchronously, skip showing it
105
+ }
106
+ }
107
+ });
108
+
109
+ // Handle SIGINT (CTRL+C)
110
+ process.on('SIGINT', async () => {
111
+ if (cleanupFunction) {
112
+ try {
113
+ await cleanupFunction();
114
+ } catch {
115
+ // Ignore cleanup errors on signal
116
+ }
117
+ }
118
+ await showExitMessage('Interrupted (CTRL+C)', 130);
119
+ try {
120
+ const sentry = await getSentry();
121
+ if (sentry && sentry.close) {
122
+ await sentry.close(2000);
123
+ }
124
+ } catch {
125
+ // Ignore Sentry.close() errors
126
+ }
127
+ process.exit(130);
128
+ });
129
+
130
+ // Handle SIGTERM
131
+ process.on('SIGTERM', async () => {
132
+ if (cleanupFunction) {
133
+ try {
134
+ await cleanupFunction();
135
+ } catch {
136
+ // Ignore cleanup errors on signal
137
+ }
138
+ }
139
+ await showExitMessage('Terminated', 143);
140
+ try {
141
+ const sentry = await getSentry();
142
+ if (sentry && sentry.close) {
143
+ await sentry.close(2000);
144
+ }
145
+ } catch {
146
+ // Ignore Sentry.close() errors
147
+ }
148
+ process.exit(143);
149
+ });
150
+
151
+ // Handle uncaught exceptions
152
+ process.on('uncaughtException', async (error) => {
153
+ if (cleanupFunction) {
154
+ try {
155
+ await cleanupFunction();
156
+ } catch {
157
+ // Ignore cleanup errors on exception
158
+ }
159
+ }
160
+ if (logFunction) {
161
+ await logFunction(`\nāŒ Uncaught Exception: ${error.message}`, { level: 'error' });
162
+ }
163
+ await showExitMessage('Uncaught exception occurred', 1);
164
+ try {
165
+ const sentry = await getSentry();
166
+ if (sentry && sentry.close) {
167
+ await sentry.close(2000);
168
+ }
169
+ } catch {
170
+ // Ignore Sentry.close() errors
171
+ }
172
+ process.exit(1);
173
+ });
174
+
175
+ // Handle unhandled rejections
176
+ process.on('unhandledRejection', async (reason) => {
177
+ if (cleanupFunction) {
178
+ try {
179
+ await cleanupFunction();
180
+ } catch {
181
+ // Ignore cleanup errors on rejection
182
+ }
183
+ }
184
+ if (logFunction) {
185
+ await logFunction(`\nāŒ Unhandled Rejection: ${reason}`, { level: 'error' });
186
+ }
187
+ await showExitMessage('Unhandled rejection occurred', 1);
188
+ try {
189
+ const sentry = await getSentry();
190
+ if (sentry && sentry.close) {
191
+ await sentry.close(2000);
192
+ }
193
+ } catch {
194
+ // Ignore Sentry.close() errors
195
+ }
196
+ process.exit(1);
197
+ });
198
+ };
199
+
200
+ /**
201
+ * Reset the exit message flag (useful for testing)
202
+ */
203
+ export const resetExitHandler = () => {
204
+ exitMessageShown = false;
205
+ };
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { exec } from 'child_process';
4
+ import { promisify } from 'util';
5
+
6
+ const execAsync = promisify(exec);
7
+
8
+ // Git-related library functions for hive-mind project
9
+
10
+ // Helper function to check if we're in a git repository
11
+ export const isGitRepository = async (execFunc = execAsync) => {
12
+ try {
13
+ await execFunc('git rev-parse --git-dir', {
14
+ encoding: 'utf8',
15
+ env: process.env
16
+ });
17
+ return true;
18
+ } catch {
19
+ return false;
20
+ }
21
+ };
22
+
23
+ // Helper function to get git tag for current HEAD
24
+ export const getGitTag = async (execFunc = execAsync) => {
25
+ try {
26
+ const { stdout } = await execFunc('git describe --exact-match --tags HEAD', {
27
+ encoding: 'utf8',
28
+ env: process.env
29
+ });
30
+ return stdout.trim();
31
+ } catch {
32
+ return null;
33
+ }
34
+ };
35
+
36
+ // Helper function to get latest git tag
37
+ export const getLatestGitTag = async (execFunc = execAsync) => {
38
+ try {
39
+ const { stdout } = await execFunc('git describe --tags --abbrev=0', {
40
+ encoding: 'utf8',
41
+ env: process.env
42
+ });
43
+ return stdout.trim().replace(/^v/, '');
44
+ } catch {
45
+ return null;
46
+ }
47
+ };
48
+
49
+ // Helper function to get short commit SHA
50
+ export const getCommitSha = async (execFunc = execAsync) => {
51
+ try {
52
+ const { stdout } = await execFunc('git rev-parse --short HEAD', {
53
+ encoding: 'utf8',
54
+ env: process.env
55
+ });
56
+ return stdout.trim();
57
+ } catch {
58
+ return null;
59
+ }
60
+ };
61
+
62
+ // Helper function to get version string based on git state
63
+ export const getGitVersion = async (execFunc = execAsync, currentVersion) => {
64
+ // First check if we're in a git repository
65
+ if (!await isGitRepository(execFunc)) {
66
+ return currentVersion;
67
+ }
68
+
69
+ // Check if this is a release version (has a git tag)
70
+ const gitTag = await getGitTag(execFunc);
71
+ if (gitTag) {
72
+ // It's a tagged release, use the version from package.json
73
+ return currentVersion;
74
+ }
75
+
76
+ // Not a tagged release, get the latest tag and commit SHA
77
+ const latestTag = await getLatestGitTag(execFunc);
78
+ const commitSha = await getCommitSha(execFunc);
79
+
80
+ if (latestTag && commitSha) {
81
+ return `${latestTag}.${commitSha}`;
82
+ }
83
+
84
+ // Fallback to package.json version if git commands fail
85
+ return currentVersion;
86
+ };
87
+
88
+ // Helper function for async git operations with zx
89
+ export const getGitVersionAsync = async ($, currentVersion) => {
90
+ // First check if we're in a git repository to avoid "fatal: not a git repository" errors
91
+ // Redirect stderr to /dev/null at shell level to prevent error messages from appearing
92
+ try {
93
+ const gitCheckResult = await $`git rev-parse --git-dir 2>/dev/null || true`;
94
+ const output = gitCheckResult.stdout.toString().trim();
95
+ if (!output || gitCheckResult.code !== 0) {
96
+ // Not in a git repository, use package.json version
97
+ return currentVersion;
98
+ }
99
+ } catch {
100
+ // Not in a git repository, use package.json version
101
+ return currentVersion;
102
+ }
103
+
104
+ // We're in a git repo, proceed with version detection
105
+ // Check if this is a release version (has a git tag)
106
+ // Redirect stderr to /dev/null at shell level to prevent error messages from appearing
107
+ try {
108
+ const gitTagResult = await $`git describe --exact-match --tags HEAD 2>/dev/null || true`;
109
+ if (gitTagResult.code === 0 && gitTagResult.stdout.toString().trim()) {
110
+ // It's a tagged release, use the version from package.json
111
+ return currentVersion;
112
+ }
113
+ } catch {
114
+ // Ignore error - will try next method
115
+ }
116
+
117
+ // Not a tagged release, get the latest tag and commit SHA
118
+ // Redirect stderr to /dev/null at shell level to prevent error messages from appearing
119
+ try {
120
+ const latestTagResult = await $`git describe --tags --abbrev=0 2>/dev/null || true`;
121
+ const commitShaResult = await $`git rev-parse --short HEAD 2>/dev/null || true`;
122
+
123
+ const latestTag = latestTagResult.stdout.toString().trim().replace(/^v/, '');
124
+ const commitSha = commitShaResult.stdout.toString().trim();
125
+
126
+ if (latestTag && commitSha && latestTagResult.code === 0 && commitShaResult.code === 0) {
127
+ return `${latestTag}.${commitSha}`;
128
+ }
129
+ } catch {
130
+ // Ignore error - will use fallback
131
+ }
132
+
133
+ // Fallback to package.json version if git commands fail
134
+ return currentVersion;
135
+ };
136
+
137
+ // Export all functions as default as well
138
+ export default {
139
+ isGitRepository,
140
+ getGitTag,
141
+ getLatestGitTag,
142
+ getCommitSha,
143
+ getGitVersion,
144
+ getGitVersionAsync
145
+ };
@@ -0,0 +1,246 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Automatic GitHub issue creation for error reporting
5
+ */
6
+
7
+ import { createInterface } from 'readline';
8
+ import { log, cleanErrorMessage, getAbsoluteLogPath } from './lib.mjs';
9
+ import { reportError, isSentryEnabled } from './sentry.lib.mjs';
10
+
11
+ if (typeof globalThis.use === 'undefined') {
12
+ globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
13
+ }
14
+
15
+ const fs = (await use('fs')).promises;
16
+ const { $ } = await use('command-stream');
17
+
18
+ const GITHUB_ISSUE_BODY_MAX_SIZE = 60000;
19
+ const GITHUB_FILE_MAX_SIZE = 10 * 1024 * 1024;
20
+
21
+ /**
22
+ * Prompt user for confirmation to create GitHub issue
23
+ * @param {string} errorMessage - The error message to display
24
+ * @returns {Promise<boolean>} True if user agrees, false otherwise
25
+ */
26
+ export const promptUserForIssueCreation = async (errorMessage) => {
27
+ return new Promise((resolve) => {
28
+ const rl = createInterface({
29
+ input: process.stdin,
30
+ output: process.stdout
31
+ });
32
+
33
+ console.log('\nāŒ An error occurred:');
34
+ console.log(` ${errorMessage}`);
35
+
36
+ if (isSentryEnabled()) {
37
+ console.log('\nāœ… Error reported to Sentry successfully');
38
+ }
39
+
40
+ rl.question('\nā“ Would you like to create a GitHub issue for this error? (y/n): ', (answer) => {
41
+ rl.close();
42
+ resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
43
+ });
44
+ });
45
+ };
46
+
47
+ /**
48
+ * Get current GitHub user
49
+ * @returns {Promise<string|null>} GitHub username or null
50
+ */
51
+ const getCurrentGitHubUser = async () => {
52
+ try {
53
+ const result = await $`gh api user --jq .login`;
54
+ if (result.exitCode === 0) {
55
+ return result.stdout.toString().trim();
56
+ }
57
+ } catch (error) {
58
+ reportError(error, {
59
+ context: 'get_github_user',
60
+ operation: 'gh_api_user'
61
+ });
62
+ }
63
+ return null;
64
+ };
65
+
66
+ /**
67
+ * Create a secret gist with log content
68
+ * @param {string} logContent - Content to upload
69
+ * @param {string} filename - Filename for the gist
70
+ * @returns {Promise<string|null>} Gist URL or null on failure
71
+ */
72
+ const createSecretGist = async (logContent, filename) => {
73
+ try {
74
+ const tempFile = `/tmp/${filename}`;
75
+ await fs.writeFile(tempFile, logContent);
76
+
77
+ const result = await $`gh gist create ${tempFile} --secret --desc "Error log for hive-mind"`;
78
+ if (result.exitCode === 0) {
79
+ const gistUrl = result.stdout.toString().trim();
80
+ await fs.unlink(tempFile).catch(() => {});
81
+ return gistUrl;
82
+ }
83
+ } catch (error) {
84
+ reportError(error, {
85
+ context: 'create_secret_gist',
86
+ operation: 'gh_gist_create'
87
+ });
88
+ }
89
+ return null;
90
+ };
91
+
92
+ /**
93
+ * Format log content for issue body
94
+ * @param {string} logContent - Log file content
95
+ * @param {string} logFilePath - Path to log file
96
+ * @returns {Promise<Object>} Object with formatted content and attachment method
97
+ */
98
+ export const formatLogForIssue = async (logContent, logFilePath) => {
99
+ const logSize = Buffer.byteLength(logContent, 'utf8');
100
+
101
+ if (logSize < GITHUB_ISSUE_BODY_MAX_SIZE) {
102
+ return {
103
+ method: 'inline',
104
+ content: `\`\`\`\n${logContent}\n\`\`\``
105
+ };
106
+ }
107
+
108
+ if (logSize < GITHUB_FILE_MAX_SIZE) {
109
+ return {
110
+ method: 'file',
111
+ content: `Log file is too large to include inline. Please see the attached log file.\n\nLog file path: \`${logFilePath}\``
112
+ };
113
+ }
114
+
115
+ const gistUrl = await createSecretGist(logContent, `hive-mind-error-${Date.now()}.log`);
116
+ if (gistUrl) {
117
+ return {
118
+ method: 'gist',
119
+ content: `Log file is too large for inline attachment.\n\nšŸ“„ View full log: ${gistUrl}`
120
+ };
121
+ }
122
+
123
+ return {
124
+ method: 'truncated',
125
+ content: `Log file is too large. Showing last 5000 characters:\n\n\`\`\`\n${logContent.slice(-5000)}\n\`\`\``
126
+ };
127
+ };
128
+
129
+ /**
130
+ * Create GitHub issue for error
131
+ * @param {Object} options - Issue creation options
132
+ * @param {Error} options.error - The error object
133
+ * @param {string} options.errorType - Type of error (uncaughtException, unhandledRejection, execution)
134
+ * @param {string} options.logFile - Path to log file
135
+ * @param {Object} options.context - Additional context about the error
136
+ * @returns {Promise<string|null>} Issue URL or null on failure
137
+ */
138
+ export const createIssueForError = async (options) => {
139
+ const { error, errorType, logFile, context = {} } = options;
140
+
141
+ try {
142
+ const currentUser = await getCurrentGitHubUser();
143
+ if (!currentUser) {
144
+ await log('āš ļø Could not determine GitHub user. Cannot create error report issue.', { level: 'warning' });
145
+ return null;
146
+ }
147
+
148
+ const errorMessage = cleanErrorMessage(error);
149
+ const shouldCreateIssue = await promptUserForIssueCreation(errorMessage);
150
+
151
+ if (!shouldCreateIssue) {
152
+ await log('ā„¹ļø Issue creation cancelled by user');
153
+ return null;
154
+ }
155
+
156
+ await log('\nšŸ”„ Creating GitHub issue...');
157
+
158
+ const issueTitle = error.message || errorMessage || `${errorType} in hive-mind`;
159
+
160
+ let issueBody = '## Error Details\n\n';
161
+ issueBody += `**Type**: ${errorType}\n`;
162
+ issueBody += `**Message**: ${errorMessage}\n\n`;
163
+
164
+ if (error.stack) {
165
+ issueBody += `### Stack Trace\n\n\`\`\`\n${error.stack}\n\`\`\`\n\n`;
166
+ }
167
+
168
+ if (Object.keys(context).length > 0) {
169
+ issueBody += `### Context\n\n\`\`\`json\n${JSON.stringify(context, null, 2)}\n\`\`\`\n\n`;
170
+ }
171
+
172
+ if (logFile) {
173
+ try {
174
+ const logContent = await fs.readFile(logFile, 'utf8');
175
+ const { method, content } = await formatLogForIssue(logContent, logFile);
176
+
177
+ issueBody += `### Log File\n\n${content}\n\n`;
178
+ await log(`šŸ“„ Log attached via: ${method}`);
179
+ } catch (readError) {
180
+ reportError(readError, {
181
+ context: 'read_log_file',
182
+ operation: 'fs_read_file',
183
+ logFile
184
+ });
185
+ issueBody += `### Log File\n\nCould not read log file: ${logFile}\n\n`;
186
+ }
187
+ }
188
+
189
+ issueBody += '---\n';
190
+ issueBody += `*This issue was automatically created by @${currentUser} using hive-mind error reporting*\n`;
191
+
192
+ const tempBodyFile = `/tmp/hive-mind-issue-body-${Date.now()}.md`;
193
+ await fs.writeFile(tempBodyFile, issueBody);
194
+
195
+ const result = await $`gh issue create --repo link-assistant/hive-mind --title ${issueTitle} --body-file ${tempBodyFile} --label bug`;
196
+
197
+ await fs.unlink(tempBodyFile).catch(() => {});
198
+
199
+ if (result.exitCode === 0) {
200
+ const issueUrl = result.stdout.toString().trim();
201
+ await log(`āœ… Issue created: ${issueUrl}`);
202
+ return issueUrl;
203
+ } else {
204
+ await log(`āŒ Failed to create issue: ${result.stderr || 'Unknown error'}`, { level: 'error' });
205
+ return null;
206
+ }
207
+ } catch (createError) {
208
+ reportError(createError, {
209
+ context: 'create_github_issue',
210
+ operation: 'gh_issue_create',
211
+ originalError: error.message
212
+ });
213
+ await log(`āŒ Error creating issue: ${cleanErrorMessage(createError)}`, { level: 'error' });
214
+ return null;
215
+ }
216
+ };
217
+
218
+ /**
219
+ * Handle error with optional automatic issue creation
220
+ * @param {Object} options - Error handling options
221
+ * @param {Error} options.error - The error object
222
+ * @param {string} options.errorType - Type of error
223
+ * @param {string} options.logFile - Path to log file
224
+ * @param {Object} options.context - Additional context
225
+ * @param {boolean} options.skipPrompt - Skip user prompt (for non-interactive mode)
226
+ * @returns {Promise<string|null>} Issue URL if created, null otherwise
227
+ */
228
+ export const handleErrorWithIssueCreation = async (options) => {
229
+ const { error, errorType, logFile, context = {}, skipPrompt = false } = options;
230
+
231
+ if (skipPrompt) {
232
+ return null;
233
+ }
234
+
235
+ if (!process.stdin.isTTY) {
236
+ await log('ā„¹ļø Non-interactive mode detected. Skipping issue creation prompt.');
237
+ return null;
238
+ }
239
+
240
+ return await createIssueForError({
241
+ error,
242
+ errorType,
243
+ logFile: logFile || await getAbsoluteLogPath(),
244
+ context
245
+ });
246
+ };