@eldrforge/kodrdriv 0.0.13 ā 0.0.14
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/.kodrdriv/context/content.md +7 -1
- package/RELEASE_NOTES.md +14 -0
- package/dist/arguments.js +50 -3
- package/dist/arguments.js.map +1 -1
- package/dist/commands/audio-commit.js +275 -0
- package/dist/commands/audio-commit.js.map +1 -0
- package/dist/commands/audio-review.js +724 -0
- package/dist/commands/audio-review.js.map +1 -0
- package/dist/commands/clean.js +36 -0
- package/dist/commands/clean.js.map +1 -0
- package/dist/commands/commit.js +28 -3
- package/dist/commands/commit.js.map +1 -1
- package/dist/commands/publish.js +15 -7
- package/dist/commands/publish.js.map +1 -1
- package/dist/commands/release.js +31 -2
- package/dist/commands/release.js.map +1 -1
- package/dist/constants.js +25 -3
- package/dist/constants.js.map +1 -1
- package/dist/main.js +20 -10
- package/dist/main.js.map +1 -1
- package/dist/prompt/instructions/audio-review.md +102 -0
- package/dist/prompt/personas/reviewer.md +29 -0
- package/dist/prompt/prompts.js +31 -2
- package/dist/prompt/prompts.js.map +1 -1
- package/dist/types.js +13 -0
- package/dist/types.js.map +1 -1
- package/dist/util/general.js +29 -2
- package/dist/util/general.js.map +1 -1
- package/dist/util/github.js +54 -1
- package/dist/util/github.js.map +1 -1
- package/dist/util/openai.js +68 -4
- package/dist/util/openai.js.map +1 -1
- package/dist/util/storage.js +20 -1
- package/dist/util/storage.js.map +1 -1
- package/output/kodrdriv/250701-1442-release-notes.md +3 -0
- package/package.json +3 -2
- package/pnpm-workspace.yaml +2 -0
- package/vitest.config.ts +3 -3
|
@@ -0,0 +1,724 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs/promises';
|
|
4
|
+
import { getLogger } from '../logging.js';
|
|
5
|
+
import { transcribeAudio, createCompletion } from '../util/openai.js';
|
|
6
|
+
import { create as create$3 } from '../prompt/prompts.js';
|
|
7
|
+
import { run } from '../util/child.js';
|
|
8
|
+
import { create } from '../content/log.js';
|
|
9
|
+
import { create as create$1 } from '../content/diff.js';
|
|
10
|
+
import { DEFAULT_EXCLUDED_PATTERNS, DEFAULT_OUTPUT_DIRECTORY } from '../constants.js';
|
|
11
|
+
import { getOutputPath, getTimestampedResponseFilename, getTimestampedRequestFilename } from '../util/general.js';
|
|
12
|
+
import { create as create$2 } from '../util/storage.js';
|
|
13
|
+
import { getOpenIssues, createIssue } from '../util/github.js';
|
|
14
|
+
|
|
15
|
+
// Enhanced exclusion patterns specifically for audio review context
|
|
16
|
+
// These focus on excluding large files, binaries, and content that doesn't help with issue analysis
|
|
17
|
+
const getAudioReviewExcludedPatterns = (basePatterns)=>{
|
|
18
|
+
const audioReviewSpecificExclusions = [
|
|
19
|
+
// Lock files and dependency files (often massive)
|
|
20
|
+
"*lock*",
|
|
21
|
+
"*.lock",
|
|
22
|
+
"pnpm-lock.yaml",
|
|
23
|
+
"package-lock.json",
|
|
24
|
+
"yarn.lock",
|
|
25
|
+
"bun.lockb",
|
|
26
|
+
"composer.lock",
|
|
27
|
+
"Cargo.lock",
|
|
28
|
+
"Gemfile.lock",
|
|
29
|
+
"Pipfile.lock",
|
|
30
|
+
"poetry.lock",
|
|
31
|
+
// Image files (binary and large)
|
|
32
|
+
"*.png",
|
|
33
|
+
"*.jpg",
|
|
34
|
+
"*.jpeg",
|
|
35
|
+
"*.gif",
|
|
36
|
+
"*.bmp",
|
|
37
|
+
"*.tiff",
|
|
38
|
+
"*.webp",
|
|
39
|
+
"*.svg",
|
|
40
|
+
"*.ico",
|
|
41
|
+
"*.icns",
|
|
42
|
+
// Video and audio files
|
|
43
|
+
"*.mp4",
|
|
44
|
+
"*.avi",
|
|
45
|
+
"*.mov",
|
|
46
|
+
"*.wmv",
|
|
47
|
+
"*.flv",
|
|
48
|
+
"*.mp3",
|
|
49
|
+
"*.wav",
|
|
50
|
+
"*.flac",
|
|
51
|
+
// Archives and compressed files
|
|
52
|
+
"*.zip",
|
|
53
|
+
"*.tar",
|
|
54
|
+
"*.tar.gz",
|
|
55
|
+
"*.tgz",
|
|
56
|
+
"*.rar",
|
|
57
|
+
"*.7z",
|
|
58
|
+
"*.bz2",
|
|
59
|
+
"*.xz",
|
|
60
|
+
// Binary executables and libraries
|
|
61
|
+
"*.exe",
|
|
62
|
+
"*.dll",
|
|
63
|
+
"*.so",
|
|
64
|
+
"*.dylib",
|
|
65
|
+
"*.bin",
|
|
66
|
+
"*.app",
|
|
67
|
+
// Database files
|
|
68
|
+
"*.db",
|
|
69
|
+
"*.sqlite",
|
|
70
|
+
"*.sqlite3",
|
|
71
|
+
"*.mdb",
|
|
72
|
+
// Large generated files
|
|
73
|
+
"*.map",
|
|
74
|
+
"*.min.js",
|
|
75
|
+
"*.min.css",
|
|
76
|
+
"bundle.*",
|
|
77
|
+
"vendor.*",
|
|
78
|
+
// Documentation that's often large
|
|
79
|
+
"*.pdf",
|
|
80
|
+
"*.doc",
|
|
81
|
+
"*.docx",
|
|
82
|
+
"*.ppt",
|
|
83
|
+
"*.pptx",
|
|
84
|
+
// IDE and OS generated files
|
|
85
|
+
".DS_Store",
|
|
86
|
+
"Thumbs.db",
|
|
87
|
+
"*.swp",
|
|
88
|
+
"*.tmp",
|
|
89
|
+
// Certificate and key files
|
|
90
|
+
"*.pem",
|
|
91
|
+
"*.crt",
|
|
92
|
+
"*.key",
|
|
93
|
+
"*.p12",
|
|
94
|
+
"*.pfx",
|
|
95
|
+
// Large config/data files that are often auto-generated
|
|
96
|
+
"tsconfig.tsbuildinfo",
|
|
97
|
+
"*.cache",
|
|
98
|
+
".eslintcache"
|
|
99
|
+
];
|
|
100
|
+
// Combine base patterns with audio review specific exclusions, removing duplicates
|
|
101
|
+
const combinedPatterns = [
|
|
102
|
+
...new Set([
|
|
103
|
+
...basePatterns,
|
|
104
|
+
...audioReviewSpecificExclusions
|
|
105
|
+
])
|
|
106
|
+
];
|
|
107
|
+
return combinedPatterns;
|
|
108
|
+
};
|
|
109
|
+
// Function to truncate overly large diff content while preserving structure
|
|
110
|
+
const truncateLargeDiff = (diffContent, maxLength = 5000)=>{
|
|
111
|
+
if (diffContent.length <= maxLength) {
|
|
112
|
+
return diffContent;
|
|
113
|
+
}
|
|
114
|
+
const lines = diffContent.split('\n');
|
|
115
|
+
const truncatedLines = [];
|
|
116
|
+
let currentLength = 0;
|
|
117
|
+
let truncated = false;
|
|
118
|
+
for (const line of lines){
|
|
119
|
+
if (currentLength + line.length + 1 > maxLength) {
|
|
120
|
+
truncated = true;
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
truncatedLines.push(line);
|
|
124
|
+
currentLength += line.length + 1; // +1 for newline
|
|
125
|
+
}
|
|
126
|
+
if (truncated) {
|
|
127
|
+
truncatedLines.push('');
|
|
128
|
+
truncatedLines.push(`... [TRUNCATED: Original diff was ${diffContent.length} characters, showing first ${currentLength}] ...`);
|
|
129
|
+
}
|
|
130
|
+
return truncatedLines.join('\n');
|
|
131
|
+
};
|
|
132
|
+
// Function to find and read recent release notes
|
|
133
|
+
const findRecentReleaseNotes = async (limit, outputDirectory)=>{
|
|
134
|
+
const logger = getLogger();
|
|
135
|
+
const releaseNotes = [];
|
|
136
|
+
if (limit <= 0) {
|
|
137
|
+
return releaseNotes;
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
// Common release notes file patterns
|
|
141
|
+
const patterns = [
|
|
142
|
+
'RELEASE_NOTES.md',
|
|
143
|
+
'CHANGELOG.md',
|
|
144
|
+
'CHANGES.md',
|
|
145
|
+
'HISTORY.md',
|
|
146
|
+
'RELEASES.md'
|
|
147
|
+
];
|
|
148
|
+
// If outputDirectory is specified, check there first for RELEASE_NOTES.md
|
|
149
|
+
if (outputDirectory) {
|
|
150
|
+
try {
|
|
151
|
+
const outputReleaseNotesPath = getOutputPath(outputDirectory, 'RELEASE_NOTES.md');
|
|
152
|
+
const content = await fs.readFile(outputReleaseNotesPath, 'utf-8');
|
|
153
|
+
if (content.trim()) {
|
|
154
|
+
const truncatedContent = truncateLargeDiff(content, 3000);
|
|
155
|
+
releaseNotes.push(`=== ${outputReleaseNotesPath} ===\n${truncatedContent}`);
|
|
156
|
+
logger.debug(`Found release notes in output directory: ${outputReleaseNotesPath} (%d characters)`, content.length);
|
|
157
|
+
if (releaseNotes.length >= limit) {
|
|
158
|
+
return releaseNotes.slice(0, limit);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
} catch {
|
|
162
|
+
// File doesn't exist in output directory, continue with other patterns
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
for (const pattern of patterns){
|
|
166
|
+
try {
|
|
167
|
+
const content = await fs.readFile(pattern, 'utf-8');
|
|
168
|
+
if (content.trim()) {
|
|
169
|
+
// Truncate very large release notes files
|
|
170
|
+
const truncatedContent = truncateLargeDiff(content, 3000); // Smaller limit for release notes
|
|
171
|
+
releaseNotes.push(`=== ${pattern} ===\n${truncatedContent}`);
|
|
172
|
+
if (truncatedContent.length < content.length) {
|
|
173
|
+
logger.debug(`Found release notes in ${pattern} (%d characters, truncated from %d)`, truncatedContent.length, content.length);
|
|
174
|
+
} else {
|
|
175
|
+
logger.debug(`Found release notes in ${pattern} (%d characters)`, content.length);
|
|
176
|
+
}
|
|
177
|
+
// For now, just take the first file found
|
|
178
|
+
// Could be enhanced to parse multiple releases from a single file
|
|
179
|
+
if (releaseNotes.length >= limit) {
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
} catch {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (releaseNotes.length === 0) {
|
|
188
|
+
logger.debug('No release notes files found');
|
|
189
|
+
}
|
|
190
|
+
} catch (error) {
|
|
191
|
+
logger.warn('Error searching for release notes: %s', error.message);
|
|
192
|
+
}
|
|
193
|
+
return releaseNotes.slice(0, limit);
|
|
194
|
+
};
|
|
195
|
+
const execute = async (runConfig)=>{
|
|
196
|
+
var _runConfig_audioReview, _runConfig_audioReview1, _runConfig_audioReview2, _runConfig_audioReview3, _runConfig_audioReview4, _runConfig_audioReview5, _runConfig_audioReview6, _runConfig_audioReview7, _runConfig_audioReview8, _runConfig_audioReview9, _runConfig_audioReview10, _runConfig_audioReview11, _runConfig_audioReview12;
|
|
197
|
+
const logger = getLogger();
|
|
198
|
+
const isDryRun = runConfig.dryRun || false;
|
|
199
|
+
// Show configuration even in dry-run mode
|
|
200
|
+
logger.debug('Audio review context configuration:');
|
|
201
|
+
logger.debug(' Include commit history: %s', (_runConfig_audioReview = runConfig.audioReview) === null || _runConfig_audioReview === void 0 ? void 0 : _runConfig_audioReview.includeCommitHistory);
|
|
202
|
+
logger.debug(' Include recent diffs: %s', (_runConfig_audioReview1 = runConfig.audioReview) === null || _runConfig_audioReview1 === void 0 ? void 0 : _runConfig_audioReview1.includeRecentDiffs);
|
|
203
|
+
logger.debug(' Include release notes: %s', (_runConfig_audioReview2 = runConfig.audioReview) === null || _runConfig_audioReview2 === void 0 ? void 0 : _runConfig_audioReview2.includeReleaseNotes);
|
|
204
|
+
logger.debug(' Include GitHub issues: %s', (_runConfig_audioReview3 = runConfig.audioReview) === null || _runConfig_audioReview3 === void 0 ? void 0 : _runConfig_audioReview3.includeGithubIssues);
|
|
205
|
+
logger.debug(' Commit history limit: %d', (_runConfig_audioReview4 = runConfig.audioReview) === null || _runConfig_audioReview4 === void 0 ? void 0 : _runConfig_audioReview4.commitHistoryLimit);
|
|
206
|
+
logger.debug(' Diff history limit: %d', (_runConfig_audioReview5 = runConfig.audioReview) === null || _runConfig_audioReview5 === void 0 ? void 0 : _runConfig_audioReview5.diffHistoryLimit);
|
|
207
|
+
logger.debug(' Release notes limit: %d', (_runConfig_audioReview6 = runConfig.audioReview) === null || _runConfig_audioReview6 === void 0 ? void 0 : _runConfig_audioReview6.releaseNotesLimit);
|
|
208
|
+
logger.debug(' GitHub issues limit: %d', (_runConfig_audioReview7 = runConfig.audioReview) === null || _runConfig_audioReview7 === void 0 ? void 0 : _runConfig_audioReview7.githubIssuesLimit);
|
|
209
|
+
logger.debug(' Sendit mode (auto-create issues): %s', (_runConfig_audioReview8 = runConfig.audioReview) === null || _runConfig_audioReview8 === void 0 ? void 0 : _runConfig_audioReview8.sendit);
|
|
210
|
+
if (isDryRun) {
|
|
211
|
+
var _runConfig_audioReview13, _runConfig_audioReview14;
|
|
212
|
+
logger.info('DRY RUN: Would start audio recording for review analysis');
|
|
213
|
+
logger.info('DRY RUN: Would gather additional context based on configuration above');
|
|
214
|
+
logger.info('DRY RUN: Would analyze transcription and identify issues');
|
|
215
|
+
if ((_runConfig_audioReview13 = runConfig.audioReview) === null || _runConfig_audioReview13 === void 0 ? void 0 : _runConfig_audioReview13.sendit) {
|
|
216
|
+
logger.info('DRY RUN: Would automatically create GitHub issues (sendit mode enabled)');
|
|
217
|
+
} else {
|
|
218
|
+
logger.info('DRY RUN: Would prompt for confirmation before creating GitHub issues');
|
|
219
|
+
}
|
|
220
|
+
// Show what exclusion patterns would be used in dry-run mode
|
|
221
|
+
if ((_runConfig_audioReview14 = runConfig.audioReview) === null || _runConfig_audioReview14 === void 0 ? void 0 : _runConfig_audioReview14.includeRecentDiffs) {
|
|
222
|
+
var _runConfig_excludedPatterns;
|
|
223
|
+
const basePatterns = (_runConfig_excludedPatterns = runConfig.excludedPatterns) !== null && _runConfig_excludedPatterns !== void 0 ? _runConfig_excludedPatterns : DEFAULT_EXCLUDED_PATTERNS;
|
|
224
|
+
const audioReviewExcluded = getAudioReviewExcludedPatterns(basePatterns);
|
|
225
|
+
logger.info('DRY RUN: Would use %d exclusion patterns for diff context', audioReviewExcluded.length);
|
|
226
|
+
logger.debug('DRY RUN: Sample exclusions: %s', audioReviewExcluded.slice(0, 15).join(', ') + (audioReviewExcluded.length > 15 ? '...' : ''));
|
|
227
|
+
}
|
|
228
|
+
return 'DRY RUN: Audio review command would record, transcribe, analyze audio, and create GitHub issues';
|
|
229
|
+
}
|
|
230
|
+
// Gather additional context based on configuration
|
|
231
|
+
let additionalContext = '';
|
|
232
|
+
// Fetch commit history if enabled
|
|
233
|
+
if ((_runConfig_audioReview9 = runConfig.audioReview) === null || _runConfig_audioReview9 === void 0 ? void 0 : _runConfig_audioReview9.includeCommitHistory) {
|
|
234
|
+
try {
|
|
235
|
+
logger.debug('Fetching recent commit history...');
|
|
236
|
+
const log = await create({
|
|
237
|
+
limit: runConfig.audioReview.commitHistoryLimit
|
|
238
|
+
});
|
|
239
|
+
const logContent = await log.get();
|
|
240
|
+
if (logContent.trim()) {
|
|
241
|
+
additionalContext += `\n\n[Recent Commit History]\n${logContent}`;
|
|
242
|
+
logger.debug('Added commit history to context (%d characters)', logContent.length);
|
|
243
|
+
}
|
|
244
|
+
} catch (error) {
|
|
245
|
+
logger.warn('Failed to fetch commit history: %s', error.message);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// Fetch recent diffs if enabled
|
|
249
|
+
if ((_runConfig_audioReview10 = runConfig.audioReview) === null || _runConfig_audioReview10 === void 0 ? void 0 : _runConfig_audioReview10.includeRecentDiffs) {
|
|
250
|
+
try {
|
|
251
|
+
logger.debug('Fetching recent commit diffs...');
|
|
252
|
+
const diffLimit = runConfig.audioReview.diffHistoryLimit || 5;
|
|
253
|
+
var _runConfig_excludedPatterns1;
|
|
254
|
+
// Get enhanced exclusion patterns for audio review context
|
|
255
|
+
const basePatterns = (_runConfig_excludedPatterns1 = runConfig.excludedPatterns) !== null && _runConfig_excludedPatterns1 !== void 0 ? _runConfig_excludedPatterns1 : DEFAULT_EXCLUDED_PATTERNS;
|
|
256
|
+
const audioReviewExcluded = getAudioReviewExcludedPatterns(basePatterns);
|
|
257
|
+
logger.debug('Using %d exclusion patterns for diff context (including %d audio-review specific)', audioReviewExcluded.length, audioReviewExcluded.length - basePatterns.length);
|
|
258
|
+
logger.debug('Sample exclusions: %s', audioReviewExcluded.slice(0, 10).join(', ') + (audioReviewExcluded.length > 10 ? '...' : ''));
|
|
259
|
+
// Get recent commits and their diffs
|
|
260
|
+
for(let i = 0; i < diffLimit; i++){
|
|
261
|
+
try {
|
|
262
|
+
const diffRange = i === 0 ? 'HEAD~1' : `HEAD~${i + 1}..HEAD~${i}`;
|
|
263
|
+
const diff = await create$1({
|
|
264
|
+
from: `HEAD~${i + 1}`,
|
|
265
|
+
to: `HEAD~${i}`,
|
|
266
|
+
excludedPatterns: audioReviewExcluded
|
|
267
|
+
});
|
|
268
|
+
const diffContent = await diff.get();
|
|
269
|
+
if (diffContent.trim()) {
|
|
270
|
+
const truncatedDiff = truncateLargeDiff(diffContent);
|
|
271
|
+
additionalContext += `\n\n[Recent Diff ${i + 1} (${diffRange})]\n${truncatedDiff}`;
|
|
272
|
+
if (truncatedDiff.length < diffContent.length) {
|
|
273
|
+
logger.debug('Added diff %d to context (%d characters, truncated from %d)', i + 1, truncatedDiff.length, diffContent.length);
|
|
274
|
+
} else {
|
|
275
|
+
logger.debug('Added diff %d to context (%d characters)', i + 1, diffContent.length);
|
|
276
|
+
}
|
|
277
|
+
} else {
|
|
278
|
+
logger.debug('Diff %d was empty after exclusions', i + 1);
|
|
279
|
+
}
|
|
280
|
+
} catch (error) {
|
|
281
|
+
logger.debug('Could not fetch diff %d: %s', i + 1, error.message);
|
|
282
|
+
break; // Stop if we can't fetch more diffs
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
} catch (error) {
|
|
286
|
+
logger.warn('Failed to fetch recent diffs: %s', error.message);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
// Fetch release notes if enabled
|
|
290
|
+
if ((_runConfig_audioReview11 = runConfig.audioReview) === null || _runConfig_audioReview11 === void 0 ? void 0 : _runConfig_audioReview11.includeReleaseNotes) {
|
|
291
|
+
try {
|
|
292
|
+
logger.debug('Fetching recent release notes...');
|
|
293
|
+
const outputDirectory = runConfig.outputDirectory || DEFAULT_OUTPUT_DIRECTORY;
|
|
294
|
+
const releaseNotes = await findRecentReleaseNotes(runConfig.audioReview.releaseNotesLimit || 3, outputDirectory);
|
|
295
|
+
if (releaseNotes.length > 0) {
|
|
296
|
+
additionalContext += `\n\n[Recent Release Notes]\n${releaseNotes.join('\n\n')}`;
|
|
297
|
+
logger.debug('Added %d release notes files to context', releaseNotes.length);
|
|
298
|
+
}
|
|
299
|
+
} catch (error) {
|
|
300
|
+
logger.warn('Failed to fetch release notes: %s', error.message);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
// Fetch GitHub issues if enabled
|
|
304
|
+
if ((_runConfig_audioReview12 = runConfig.audioReview) === null || _runConfig_audioReview12 === void 0 ? void 0 : _runConfig_audioReview12.includeGithubIssues) {
|
|
305
|
+
try {
|
|
306
|
+
logger.debug('Fetching open GitHub issues...');
|
|
307
|
+
const issuesLimit = Math.min(runConfig.audioReview.githubIssuesLimit || 20, 20); // Cap at 20
|
|
308
|
+
const githubIssues = await getOpenIssues(issuesLimit);
|
|
309
|
+
if (githubIssues.trim()) {
|
|
310
|
+
additionalContext += `\n\n[Open GitHub Issues]\n${githubIssues}`;
|
|
311
|
+
logger.debug('Added GitHub issues to context (%d characters)', githubIssues.length);
|
|
312
|
+
} else {
|
|
313
|
+
logger.debug('No open GitHub issues found');
|
|
314
|
+
}
|
|
315
|
+
} catch (error) {
|
|
316
|
+
logger.warn('Failed to fetch GitHub issues: %s', error.message);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (additionalContext) {
|
|
320
|
+
logger.debug('Total additional context gathered: %d characters', additionalContext.length);
|
|
321
|
+
} else {
|
|
322
|
+
logger.debug('No additional context gathered');
|
|
323
|
+
}
|
|
324
|
+
logger.info('Starting audio review session...');
|
|
325
|
+
logger.info('This command will use your system\'s default audio recording tool');
|
|
326
|
+
logger.info('Press Ctrl+C after you finish speaking to analyze the audio');
|
|
327
|
+
// Create temporary file for audio recording
|
|
328
|
+
const outputDirectory = runConfig.outputDirectory || DEFAULT_OUTPUT_DIRECTORY;
|
|
329
|
+
const storage = create$2({
|
|
330
|
+
log: logger.info
|
|
331
|
+
});
|
|
332
|
+
await storage.ensureDirectory(outputDirectory);
|
|
333
|
+
const tempDir = await fs.mkdtemp(path.join(outputDirectory, '.temp-audio-'));
|
|
334
|
+
const audioFilePath = path.join(tempDir, 'recording.wav');
|
|
335
|
+
try {
|
|
336
|
+
var _runConfig_audioReview15;
|
|
337
|
+
// Use system recording tool - cross-platform approach
|
|
338
|
+
logger.info('š¤ Starting recording... Speak now!');
|
|
339
|
+
logger.info('Recording will stop automatically after 30 seconds or when you press Ctrl+C');
|
|
340
|
+
let recordingProcess;
|
|
341
|
+
let recordingFinished = false;
|
|
342
|
+
// Determine which recording command to use based on platform
|
|
343
|
+
let recordCommand;
|
|
344
|
+
if (process.platform === 'darwin') {
|
|
345
|
+
// macOS - try ffmpeg first, then fall back to manual recording
|
|
346
|
+
try {
|
|
347
|
+
// Check if ffmpeg is available
|
|
348
|
+
await run('which ffmpeg');
|
|
349
|
+
recordCommand = `ffmpeg -f avfoundation -i ":0" -t 30 -y "${audioFilePath}"`;
|
|
350
|
+
} catch {
|
|
351
|
+
// ffmpeg not available, try sox/rec
|
|
352
|
+
try {
|
|
353
|
+
await run('which rec');
|
|
354
|
+
recordCommand = `rec -r 44100 -c 1 -t wav "${audioFilePath}" trim 0 30`;
|
|
355
|
+
} catch {
|
|
356
|
+
// Neither available, use manual fallback
|
|
357
|
+
throw new Error('MANUAL_RECORDING_NEEDED');
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
} else if (process.platform === 'win32') {
|
|
361
|
+
// Windows - use ffmpeg if available, otherwise fallback
|
|
362
|
+
try {
|
|
363
|
+
await run('where ffmpeg');
|
|
364
|
+
recordCommand = `ffmpeg -f dshow -i audio="Microphone" -t 30 -y "${audioFilePath}"`;
|
|
365
|
+
} catch {
|
|
366
|
+
throw new Error('MANUAL_RECORDING_NEEDED');
|
|
367
|
+
}
|
|
368
|
+
} else {
|
|
369
|
+
// Linux - use arecord (ALSA) or ffmpeg
|
|
370
|
+
try {
|
|
371
|
+
await run('which arecord');
|
|
372
|
+
recordCommand = `arecord -f cd -t wav -d 30 "${audioFilePath}"`;
|
|
373
|
+
} catch {
|
|
374
|
+
try {
|
|
375
|
+
await run('which ffmpeg');
|
|
376
|
+
recordCommand = `ffmpeg -f alsa -i default -t 30 -y "${audioFilePath}"`;
|
|
377
|
+
} catch {
|
|
378
|
+
throw new Error('MANUAL_RECORDING_NEEDED');
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
// Start recording as a background process
|
|
383
|
+
try {
|
|
384
|
+
recordingProcess = run(recordCommand);
|
|
385
|
+
} catch (error) {
|
|
386
|
+
if (error.message === 'MANUAL_RECORDING_NEEDED') {
|
|
387
|
+
// Provide helpful instructions for manual recording
|
|
388
|
+
logger.warn('ā ļø Automatic recording not available on this system.');
|
|
389
|
+
logger.warn('š± Please record audio manually using your system\'s built-in tools:');
|
|
390
|
+
logger.warn('');
|
|
391
|
+
if (process.platform === 'darwin') {
|
|
392
|
+
logger.warn('š macOS options:');
|
|
393
|
+
logger.warn(' 1. Use QuickTime Player: File ā New Audio Recording');
|
|
394
|
+
logger.warn(' 2. Use Voice Memos app');
|
|
395
|
+
logger.warn(' 3. Install ffmpeg: brew install ffmpeg');
|
|
396
|
+
logger.warn(' 4. Install sox: brew install sox');
|
|
397
|
+
} else if (process.platform === 'win32') {
|
|
398
|
+
logger.warn('šŖ Windows options:');
|
|
399
|
+
logger.warn(' 1. Use Voice Recorder app');
|
|
400
|
+
logger.warn(' 2. Install ffmpeg: https://ffmpeg.org/download.html');
|
|
401
|
+
} else {
|
|
402
|
+
logger.warn('š§ Linux options:');
|
|
403
|
+
logger.warn(' 1. Install alsa-utils: sudo apt install alsa-utils');
|
|
404
|
+
logger.warn(' 2. Install ffmpeg: sudo apt install ffmpeg');
|
|
405
|
+
}
|
|
406
|
+
logger.warn('');
|
|
407
|
+
logger.warn(`š¾ Save your recording as: ${audioFilePath}`);
|
|
408
|
+
logger.warn('šµ Recommended format: WAV, 44.1kHz, mono or stereo');
|
|
409
|
+
logger.warn('');
|
|
410
|
+
logger.warn('āØļø Press ENTER when you have saved the audio file...');
|
|
411
|
+
// Wait for user input
|
|
412
|
+
await new Promise((resolve)=>{
|
|
413
|
+
process.stdin.setRawMode(true);
|
|
414
|
+
process.stdin.resume();
|
|
415
|
+
process.stdin.on('data', (key)=>{
|
|
416
|
+
if (key[0] === 13) {
|
|
417
|
+
process.stdin.setRawMode(false);
|
|
418
|
+
process.stdin.pause();
|
|
419
|
+
resolve(void 0);
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
} else {
|
|
424
|
+
throw error;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
// Set up graceful shutdown
|
|
428
|
+
const stopRecording = async ()=>{
|
|
429
|
+
if (!recordingFinished) {
|
|
430
|
+
recordingFinished = true;
|
|
431
|
+
if (recordingProcess && recordingProcess.kill) {
|
|
432
|
+
recordingProcess.kill();
|
|
433
|
+
}
|
|
434
|
+
logger.info('š Recording stopped');
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
// Listen for Ctrl+C
|
|
438
|
+
process.on('SIGINT', stopRecording);
|
|
439
|
+
process.on('SIGTERM', stopRecording);
|
|
440
|
+
// Wait for recording to complete (either by timeout or user interruption)
|
|
441
|
+
if (recordingProcess) {
|
|
442
|
+
try {
|
|
443
|
+
await recordingProcess;
|
|
444
|
+
} catch (error) {
|
|
445
|
+
// Check if this is just a normal interruption (expected behavior)
|
|
446
|
+
if (error.message.includes('signal 15') || error.message.includes('SIGTERM') || error.message.includes('Exiting normally')) {
|
|
447
|
+
// This is expected when we interrupt ffmpeg - not an actual error
|
|
448
|
+
logger.debug('Recording interrupted as expected: %s', error.message);
|
|
449
|
+
} else {
|
|
450
|
+
// This might be a real error, but let's check if we got an audio file anyway
|
|
451
|
+
logger.warn('Recording process exited with error, but checking for audio file: %s', error.message);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
// Check if audio file exists
|
|
456
|
+
try {
|
|
457
|
+
await fs.access(audioFilePath);
|
|
458
|
+
} catch {
|
|
459
|
+
throw new Error('No audio file was created. Please ensure your system has audio recording capabilities.');
|
|
460
|
+
}
|
|
461
|
+
const stats = await fs.stat(audioFilePath);
|
|
462
|
+
if (stats.size === 0) {
|
|
463
|
+
throw new Error('Audio file is empty. Please check your microphone permissions and try again.');
|
|
464
|
+
}
|
|
465
|
+
logger.info('š¾ Audio recorded successfully');
|
|
466
|
+
// Transcribe audio using Whisper
|
|
467
|
+
logger.info('š¤ Transcribing audio with Whisper...');
|
|
468
|
+
const transcription = await transcribeAudio(audioFilePath, {
|
|
469
|
+
model: 'whisper-1',
|
|
470
|
+
debug: runConfig.debug,
|
|
471
|
+
debugRequestFile: runConfig.debug ? getOutputPath(outputDirectory, getTimestampedRequestFilename('audio-transcription')) : undefined,
|
|
472
|
+
debugResponseFile: runConfig.debug ? getOutputPath(outputDirectory, getTimestampedResponseFilename('audio-transcription')) : undefined
|
|
473
|
+
});
|
|
474
|
+
logger.info('š Transcription completed');
|
|
475
|
+
logger.debug('Transcription: %s', transcription.text);
|
|
476
|
+
// Analyze transcription for issues using OpenAI
|
|
477
|
+
logger.info('š¤ Analyzing transcription for project issues...');
|
|
478
|
+
const prompts = create$3(runConfig.model, runConfig);
|
|
479
|
+
// Combine additional context with user-provided context
|
|
480
|
+
let finalContext = additionalContext;
|
|
481
|
+
if ((_runConfig_audioReview15 = runConfig.audioReview) === null || _runConfig_audioReview15 === void 0 ? void 0 : _runConfig_audioReview15.context) {
|
|
482
|
+
finalContext = runConfig.audioReview.context + finalContext;
|
|
483
|
+
}
|
|
484
|
+
const analysisPrompt = await prompts.createAudioReviewPrompt(transcription.text, finalContext || undefined);
|
|
485
|
+
const request = prompts.format(analysisPrompt);
|
|
486
|
+
const result = await createCompletion(request.messages, {
|
|
487
|
+
model: runConfig.model,
|
|
488
|
+
responseFormat: {
|
|
489
|
+
type: 'json_object'
|
|
490
|
+
},
|
|
491
|
+
debug: runConfig.debug,
|
|
492
|
+
debugRequestFile: runConfig.debug ? getOutputPath(outputDirectory, getTimestampedRequestFilename('audio-analysis')) : undefined,
|
|
493
|
+
debugResponseFile: runConfig.debug ? getOutputPath(outputDirectory, getTimestampedResponseFilename('audio-analysis')) : undefined
|
|
494
|
+
});
|
|
495
|
+
logger.info('ā
Analysis completed');
|
|
496
|
+
// Handle GitHub issue creation if there are issues to create
|
|
497
|
+
if (result.issues && result.issues.length > 0) {
|
|
498
|
+
var _runConfig_audioReview16;
|
|
499
|
+
const senditMode = ((_runConfig_audioReview16 = runConfig.audioReview) === null || _runConfig_audioReview16 === void 0 ? void 0 : _runConfig_audioReview16.sendit) || false;
|
|
500
|
+
const createdIssues = [];
|
|
501
|
+
logger.info(`š Found ${result.issues.length} issues to potentially create as GitHub issues`);
|
|
502
|
+
for(let i = 0; i < result.issues.length; i++){
|
|
503
|
+
const issue = result.issues[i];
|
|
504
|
+
let shouldCreateIssue = senditMode;
|
|
505
|
+
if (!senditMode) {
|
|
506
|
+
// Interactive confirmation for each issue
|
|
507
|
+
logger.info(`\nš Issue ${i + 1} of ${result.issues.length}:`);
|
|
508
|
+
logger.info(` Title: ${issue.title}`);
|
|
509
|
+
logger.info(` Priority: ${issue.priority} | Category: ${issue.category}`);
|
|
510
|
+
logger.info(` Description: ${issue.description}`);
|
|
511
|
+
if (issue.suggestions && issue.suggestions.length > 0) {
|
|
512
|
+
logger.info(` Suggestions: ${issue.suggestions.join(', ')}`);
|
|
513
|
+
}
|
|
514
|
+
// Get user choice
|
|
515
|
+
const choice = await getUserChoice('\nWhat would you like to do with this issue?', [
|
|
516
|
+
{
|
|
517
|
+
key: 'c',
|
|
518
|
+
label: 'Create GitHub issue'
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
key: 's',
|
|
522
|
+
label: 'Skip this issue'
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
key: 'e',
|
|
526
|
+
label: 'Edit issue details'
|
|
527
|
+
}
|
|
528
|
+
]);
|
|
529
|
+
if (choice === 'c') {
|
|
530
|
+
shouldCreateIssue = true;
|
|
531
|
+
} else if (choice === 'e') {
|
|
532
|
+
// Allow user to edit the issue
|
|
533
|
+
const editedIssue = await editIssueInteractively(issue);
|
|
534
|
+
result.issues[i] = editedIssue;
|
|
535
|
+
shouldCreateIssue = true;
|
|
536
|
+
}
|
|
537
|
+
// If choice is 's', shouldCreateIssue remains false
|
|
538
|
+
}
|
|
539
|
+
if (shouldCreateIssue) {
|
|
540
|
+
try {
|
|
541
|
+
logger.info(`š Creating GitHub issue: "${issue.title}"`);
|
|
542
|
+
// Format issue body with additional details
|
|
543
|
+
const issueBody = formatIssueBody(issue);
|
|
544
|
+
// Create labels based on priority and category
|
|
545
|
+
const labels = [
|
|
546
|
+
`priority-${issue.priority}`,
|
|
547
|
+
`category-${issue.category}`,
|
|
548
|
+
'audio-review'
|
|
549
|
+
];
|
|
550
|
+
const createdIssue = await createIssue(issue.title, issueBody, labels);
|
|
551
|
+
createdIssues.push({
|
|
552
|
+
issue,
|
|
553
|
+
githubUrl: createdIssue.html_url,
|
|
554
|
+
number: createdIssue.number
|
|
555
|
+
});
|
|
556
|
+
logger.info(`ā
Created GitHub issue #${createdIssue.number}: ${createdIssue.html_url}`);
|
|
557
|
+
} catch (error) {
|
|
558
|
+
logger.error(`ā Failed to create GitHub issue for "${issue.title}": ${error.message}`);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
// Update the result summary to include created issues
|
|
563
|
+
if (createdIssues.length > 0) {
|
|
564
|
+
return formatAudioReviewResultsWithIssues(result, createdIssues);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
// Format and return results (original behavior if no issues created)
|
|
568
|
+
return formatAudioReviewResults(result);
|
|
569
|
+
} catch (error) {
|
|
570
|
+
logger.error('Error during audio review: %s', error.message);
|
|
571
|
+
throw error;
|
|
572
|
+
} finally{
|
|
573
|
+
// Cleanup temporary files
|
|
574
|
+
try {
|
|
575
|
+
await fs.rm(tempDir, {
|
|
576
|
+
recursive: true
|
|
577
|
+
});
|
|
578
|
+
} catch (cleanupError) {
|
|
579
|
+
logger.warn('Failed to cleanup temporary directory: %s', cleanupError);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
// Helper function to get user choice interactively
|
|
584
|
+
async function getUserChoice(prompt, choices) {
|
|
585
|
+
const logger = getLogger();
|
|
586
|
+
logger.info(prompt);
|
|
587
|
+
choices.forEach((choice)=>{
|
|
588
|
+
logger.info(` [${choice.key}] ${choice.label}`);
|
|
589
|
+
});
|
|
590
|
+
logger.info('');
|
|
591
|
+
return new Promise((resolve)=>{
|
|
592
|
+
process.stdin.setRawMode(true);
|
|
593
|
+
process.stdin.resume();
|
|
594
|
+
process.stdin.on('data', (key)=>{
|
|
595
|
+
const keyStr = key.toString().toLowerCase();
|
|
596
|
+
const choice = choices.find((c)=>c.key === keyStr);
|
|
597
|
+
if (choice) {
|
|
598
|
+
process.stdin.setRawMode(false);
|
|
599
|
+
process.stdin.pause();
|
|
600
|
+
logger.info(`Selected: ${choice.label}\n`);
|
|
601
|
+
resolve(choice.key);
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
// Helper function to edit issue interactively
|
|
607
|
+
async function editIssueInteractively(issue) {
|
|
608
|
+
const logger = getLogger();
|
|
609
|
+
const readline = await import('readline');
|
|
610
|
+
const rl = readline.createInterface({
|
|
611
|
+
input: process.stdin,
|
|
612
|
+
output: process.stdout
|
|
613
|
+
});
|
|
614
|
+
const question = (prompt)=>{
|
|
615
|
+
return new Promise((resolve)=>{
|
|
616
|
+
rl.question(prompt, resolve);
|
|
617
|
+
});
|
|
618
|
+
};
|
|
619
|
+
try {
|
|
620
|
+
logger.info('š Edit issue details (press Enter to keep current value):');
|
|
621
|
+
const newTitle = await question(`Title [${issue.title}]: `);
|
|
622
|
+
const newDescription = await question(`Description [${issue.description}]: `);
|
|
623
|
+
const newPriority = await question(`Priority (low/medium/high) [${issue.priority}]: `);
|
|
624
|
+
const newCategory = await question(`Category (ui/content/functionality/accessibility/performance/other) [${issue.category}]: `);
|
|
625
|
+
const updatedIssue = {
|
|
626
|
+
title: newTitle.trim() || issue.title,
|
|
627
|
+
description: newDescription.trim() || issue.description,
|
|
628
|
+
priority: newPriority.trim() || issue.priority,
|
|
629
|
+
category: newCategory.trim() || issue.category,
|
|
630
|
+
suggestions: issue.suggestions
|
|
631
|
+
};
|
|
632
|
+
logger.info('ā
Issue updated successfully');
|
|
633
|
+
return updatedIssue;
|
|
634
|
+
} finally{
|
|
635
|
+
rl.close();
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
// Helper function to format issue body for GitHub
|
|
639
|
+
function formatIssueBody(issue) {
|
|
640
|
+
let body = `## Description\n\n${issue.description}\n\n`;
|
|
641
|
+
body += `## Details\n\n`;
|
|
642
|
+
body += `- **Priority:** ${issue.priority}\n`;
|
|
643
|
+
body += `- **Category:** ${issue.category}\n`;
|
|
644
|
+
body += `- **Source:** Audio Review\n\n`;
|
|
645
|
+
if (issue.suggestions && issue.suggestions.length > 0) {
|
|
646
|
+
body += `## Suggestions\n\n`;
|
|
647
|
+
issue.suggestions.forEach((suggestion)=>{
|
|
648
|
+
body += `- ${suggestion}\n`;
|
|
649
|
+
});
|
|
650
|
+
body += '\n';
|
|
651
|
+
}
|
|
652
|
+
body += `---\n\n`;
|
|
653
|
+
body += `*This issue was automatically created from an audio review session.*`;
|
|
654
|
+
return body;
|
|
655
|
+
}
|
|
656
|
+
// Helper function to format results with created GitHub issues
|
|
657
|
+
function formatAudioReviewResultsWithIssues(result, createdIssues) {
|
|
658
|
+
let output = `š¤ Audio Review Results\n\n`;
|
|
659
|
+
output += `š Summary: ${result.summary}\n`;
|
|
660
|
+
output += `š Total Issues Found: ${result.totalIssues}\n`;
|
|
661
|
+
output += `š GitHub Issues Created: ${createdIssues.length}\n\n`;
|
|
662
|
+
if (result.issues && result.issues.length > 0) {
|
|
663
|
+
output += `š Issues Identified:\n\n`;
|
|
664
|
+
result.issues.forEach((issue, index)=>{
|
|
665
|
+
const priorityEmoji = issue.priority === 'high' ? 'š“' : issue.priority === 'medium' ? 'š”' : 'š¢';
|
|
666
|
+
const categoryEmoji = issue.category === 'ui' ? 'šØ' : issue.category === 'content' ? 'š' : issue.category === 'functionality' ? 'āļø' : issue.category === 'accessibility' ? 'āæ' : issue.category === 'performance' ? 'ā”' : 'š§';
|
|
667
|
+
output += `${index + 1}. ${priorityEmoji} ${issue.title}\n`;
|
|
668
|
+
output += ` ${categoryEmoji} Category: ${issue.category} | Priority: ${issue.priority}\n`;
|
|
669
|
+
output += ` š Description: ${issue.description}\n`;
|
|
670
|
+
// Check if this issue was created as a GitHub issue
|
|
671
|
+
const createdIssue = createdIssues.find((ci)=>ci.issue === issue);
|
|
672
|
+
if (createdIssue) {
|
|
673
|
+
output += ` š GitHub Issue: #${createdIssue.number} - ${createdIssue.githubUrl}\n`;
|
|
674
|
+
}
|
|
675
|
+
if (issue.suggestions && issue.suggestions.length > 0) {
|
|
676
|
+
output += ` š” Suggestions:\n`;
|
|
677
|
+
issue.suggestions.forEach((suggestion)=>{
|
|
678
|
+
output += ` ⢠${suggestion}\n`;
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
output += `\n`;
|
|
682
|
+
});
|
|
683
|
+
} else {
|
|
684
|
+
output += `ā
No specific issues identified from the audio review.\n\n`;
|
|
685
|
+
}
|
|
686
|
+
if (createdIssues.length > 0) {
|
|
687
|
+
output += `\nšÆ Created GitHub Issues:\n`;
|
|
688
|
+
createdIssues.forEach((createdIssue)=>{
|
|
689
|
+
output += `⢠#${createdIssue.number}: ${createdIssue.issue.title} - ${createdIssue.githubUrl}\n`;
|
|
690
|
+
});
|
|
691
|
+
output += `\n`;
|
|
692
|
+
}
|
|
693
|
+
output += `š Next Steps: Review the created GitHub issues and prioritize them in your development workflow.`;
|
|
694
|
+
return output;
|
|
695
|
+
}
|
|
696
|
+
function formatAudioReviewResults(result) {
|
|
697
|
+
let output = `š¤ Audio Review Results\n\n`;
|
|
698
|
+
output += `š Summary: ${result.summary}\n`;
|
|
699
|
+
output += `š Total Issues Found: ${result.totalIssues}\n\n`;
|
|
700
|
+
if (result.issues && result.issues.length > 0) {
|
|
701
|
+
output += `š Issues Identified:\n\n`;
|
|
702
|
+
result.issues.forEach((issue, index)=>{
|
|
703
|
+
const priorityEmoji = issue.priority === 'high' ? 'š“' : issue.priority === 'medium' ? 'š”' : 'š¢';
|
|
704
|
+
const categoryEmoji = issue.category === 'ui' ? 'šØ' : issue.category === 'content' ? 'š' : issue.category === 'functionality' ? 'āļø' : issue.category === 'accessibility' ? 'āæ' : issue.category === 'performance' ? 'ā”' : 'š§';
|
|
705
|
+
output += `${index + 1}. ${priorityEmoji} ${issue.title}\n`;
|
|
706
|
+
output += ` ${categoryEmoji} Category: ${issue.category} | Priority: ${issue.priority}\n`;
|
|
707
|
+
output += ` š Description: ${issue.description}\n`;
|
|
708
|
+
if (issue.suggestions && issue.suggestions.length > 0) {
|
|
709
|
+
output += ` š” Suggestions:\n`;
|
|
710
|
+
issue.suggestions.forEach((suggestion)=>{
|
|
711
|
+
output += ` ⢠${suggestion}\n`;
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
output += `\n`;
|
|
715
|
+
});
|
|
716
|
+
} else {
|
|
717
|
+
output += `ā
No specific issues identified from the audio review.\n\n`;
|
|
718
|
+
}
|
|
719
|
+
output += `š Next Steps: Review the identified issues and prioritize them for your development workflow.`;
|
|
720
|
+
return output;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
export { execute };
|
|
724
|
+
//# sourceMappingURL=audio-review.js.map
|