@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.
- package/CHANGELOG.md +20 -0
- package/LICENSE +24 -0
- package/README.md +769 -0
- package/package.json +58 -0
- package/src/agent.lib.mjs +705 -0
- package/src/agent.prompts.lib.mjs +196 -0
- package/src/buildUserMention.lib.mjs +71 -0
- package/src/claude-limits.lib.mjs +389 -0
- package/src/claude.lib.mjs +1445 -0
- package/src/claude.prompts.lib.mjs +203 -0
- package/src/codex.lib.mjs +552 -0
- package/src/codex.prompts.lib.mjs +194 -0
- package/src/config.lib.mjs +207 -0
- package/src/contributing-guidelines.lib.mjs +268 -0
- package/src/exit-handler.lib.mjs +205 -0
- package/src/git.lib.mjs +145 -0
- package/src/github-issue-creator.lib.mjs +246 -0
- package/src/github-linking.lib.mjs +152 -0
- package/src/github.batch.lib.mjs +272 -0
- package/src/github.graphql.lib.mjs +258 -0
- package/src/github.lib.mjs +1479 -0
- package/src/hive.config.lib.mjs +254 -0
- package/src/hive.mjs +1500 -0
- package/src/instrument.mjs +191 -0
- package/src/interactive-mode.lib.mjs +1000 -0
- package/src/lenv-reader.lib.mjs +206 -0
- package/src/lib.mjs +490 -0
- package/src/lino.lib.mjs +176 -0
- package/src/local-ci-checks.lib.mjs +324 -0
- package/src/memory-check.mjs +419 -0
- package/src/model-mapping.lib.mjs +145 -0
- package/src/model-validation.lib.mjs +278 -0
- package/src/opencode.lib.mjs +479 -0
- package/src/opencode.prompts.lib.mjs +194 -0
- package/src/protect-branch.mjs +159 -0
- package/src/review.mjs +433 -0
- package/src/reviewers-hive.mjs +643 -0
- package/src/sentry.lib.mjs +284 -0
- package/src/solve.auto-continue.lib.mjs +568 -0
- package/src/solve.auto-pr.lib.mjs +1374 -0
- package/src/solve.branch-errors.lib.mjs +341 -0
- package/src/solve.branch.lib.mjs +230 -0
- package/src/solve.config.lib.mjs +342 -0
- package/src/solve.error-handlers.lib.mjs +256 -0
- package/src/solve.execution.lib.mjs +291 -0
- package/src/solve.feedback.lib.mjs +436 -0
- package/src/solve.mjs +1128 -0
- package/src/solve.preparation.lib.mjs +210 -0
- package/src/solve.repo-setup.lib.mjs +114 -0
- package/src/solve.repository.lib.mjs +961 -0
- package/src/solve.results.lib.mjs +558 -0
- package/src/solve.session.lib.mjs +135 -0
- package/src/solve.validation.lib.mjs +325 -0
- package/src/solve.watch.lib.mjs +572 -0
- package/src/start-screen.mjs +324 -0
- package/src/task.mjs +308 -0
- package/src/telegram-bot.mjs +1481 -0
- package/src/telegram-markdown.lib.mjs +64 -0
- package/src/usage-limit.lib.mjs +218 -0
- package/src/version.lib.mjs +41 -0
- package/src/youtrack/solve.youtrack.lib.mjs +116 -0
- package/src/youtrack/youtrack-sync.mjs +219 -0
- 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
|
+
};
|
package/src/git.lib.mjs
ADDED
|
@@ -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
|
+
};
|