@eldrforge/kodrdriv 1.2.21 → 1.2.22

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 (67) hide show
  1. package/WORKFLOW-PRECHECK-IMPLEMENTATION.md +239 -0
  2. package/WORKFLOW-SKIP-SUMMARY.md +121 -0
  3. package/dist/arguments.js +2 -2
  4. package/dist/arguments.js.map +1 -1
  5. package/dist/commands/audio-commit.js +15 -6
  6. package/dist/commands/audio-commit.js.map +1 -1
  7. package/dist/commands/audio-review.js +31 -15
  8. package/dist/commands/audio-review.js.map +1 -1
  9. package/dist/commands/commit.js +30 -19
  10. package/dist/commands/commit.js.map +1 -1
  11. package/dist/commands/link.js +27 -27
  12. package/dist/commands/link.js.map +1 -1
  13. package/dist/commands/publish.js +74 -21
  14. package/dist/commands/publish.js.map +1 -1
  15. package/dist/commands/release.js +30 -17
  16. package/dist/commands/release.js.map +1 -1
  17. package/dist/commands/review.js +33 -26
  18. package/dist/commands/review.js.map +1 -1
  19. package/dist/commands/select-audio.js +4 -4
  20. package/dist/commands/select-audio.js.map +1 -1
  21. package/dist/commands/tree.js +122 -35
  22. package/dist/commands/tree.js.map +1 -1
  23. package/dist/commands/unlink.js +13 -13
  24. package/dist/commands/unlink.js.map +1 -1
  25. package/dist/commands/updates.js +21 -0
  26. package/dist/commands/updates.js.map +1 -1
  27. package/dist/commands/versions.js +5 -5
  28. package/dist/commands/versions.js.map +1 -1
  29. package/dist/constants.js +4 -4
  30. package/dist/constants.js.map +1 -1
  31. package/dist/content/files.js +4 -4
  32. package/dist/content/files.js.map +1 -1
  33. package/dist/logging.js +3 -3
  34. package/dist/logging.js.map +1 -1
  35. package/dist/util/aiAdapter.js +28 -0
  36. package/dist/util/aiAdapter.js.map +1 -0
  37. package/dist/util/general.js +5 -5
  38. package/dist/util/general.js.map +1 -1
  39. package/dist/util/interactive.js +6 -437
  40. package/dist/util/interactive.js.map +1 -1
  41. package/dist/util/loggerAdapter.js +24 -0
  42. package/dist/util/loggerAdapter.js.map +1 -0
  43. package/dist/util/performance.js +4 -4
  44. package/dist/util/performance.js.map +1 -1
  45. package/dist/util/safety.js +4 -4
  46. package/dist/util/safety.js.map +1 -1
  47. package/dist/util/storage.js +2 -2
  48. package/dist/util/storage.js.map +1 -1
  49. package/dist/util/storageAdapter.js +25 -0
  50. package/dist/util/storageAdapter.js.map +1 -0
  51. package/package.json +5 -4
  52. package/GITHUB-TOOLS-INTEGRATION.md +0 -323
  53. package/INTEGRATION-SUMMARY.md +0 -232
  54. package/TEST-STATUS.md +0 -168
  55. package/dist/prompt/commit.js +0 -76
  56. package/dist/prompt/commit.js.map +0 -1
  57. package/dist/prompt/instructions/commit.md +0 -133
  58. package/dist/prompt/instructions/release.md +0 -188
  59. package/dist/prompt/instructions/review.md +0 -169
  60. package/dist/prompt/personas/releaser.md +0 -24
  61. package/dist/prompt/personas/you.md +0 -55
  62. package/dist/prompt/release.js +0 -100
  63. package/dist/prompt/release.js.map +0 -1
  64. package/dist/prompt/review.js +0 -64
  65. package/dist/prompt/review.js.map +0 -1
  66. package/dist/util/openai.js +0 -365
  67. package/dist/util/openai.js.map +0 -1
@@ -1,444 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import { getDryRunLogger } from '../logging.js';
3
- import { spawnSync } from 'child_process';
4
- import * as path from 'path';
5
- import * as os from 'os';
6
- import * as fs from 'fs/promises';
3
+ export { STANDARD_CHOICES, SecureTempFile, cleanupTempFile, createSecureTempFile, editContentInEditor, getLLMFeedbackInEditor, getUserChoice, getUserTextInput, requireTTY } from '@eldrforge/ai-service';
7
4
 
8
- function _define_property(obj, key, value) {
9
- if (key in obj) {
10
- Object.defineProperty(obj, key, {
11
- value: value,
12
- enumerable: true,
13
- configurable: true,
14
- writable: true
15
- });
16
- } else {
17
- obj[key] = value;
18
- }
19
- return obj;
20
- }
21
- /**
22
- * Get user choice interactively from terminal input
23
- * @param prompt The prompt message to display
24
- * @param choices Array of available choices
25
- * @param options Additional options for customizing behavior
26
- * @returns Promise resolving to the selected choice key
27
- */ async function getUserChoice(prompt, choices, options = {}) {
28
- const logger = getDryRunLogger(false);
29
- logger.info(prompt);
30
- choices.forEach((choice)=>{
31
- logger.info(` [${choice.key}] ${choice.label}`);
32
- });
33
- logger.info('');
34
- // Check if stdin is a TTY (terminal) or piped
35
- if (!process.stdin.isTTY) {
36
- logger.error('⚠️ STDIN is piped but interactive mode is enabled');
37
- logger.error(' Interactive prompts cannot be used when input is piped');
38
- logger.error(' Solutions:');
39
- logger.error(' • Use terminal input instead of piping');
40
- // Add any additional suggestions
41
- if (options.nonTtyErrorSuggestions) {
42
- options.nonTtyErrorSuggestions.forEach((suggestion)=>{
43
- logger.error(` • ${suggestion}`);
44
- });
45
- }
46
- return 's'; // Default to skip
47
- }
48
- return new Promise((resolve, reject)=>{
49
- let isResolved = false;
50
- let dataHandler = null;
51
- let errorHandler = null;
52
- const cleanup = ()=>{
53
- if (dataHandler) {
54
- process.stdin.removeListener('data', dataHandler);
55
- }
56
- if (errorHandler) {
57
- process.stdin.removeListener('error', errorHandler);
58
- }
59
- try {
60
- if (process.stdin.setRawMode) {
61
- process.stdin.setRawMode(false);
62
- }
63
- process.stdin.pause();
64
- // Detach stdin again now that we're done
65
- if (typeof process.stdin.unref === 'function') {
66
- process.stdin.unref();
67
- }
68
- } catch {
69
- // Ignore cleanup errors
70
- }
71
- };
72
- const safeResolve = (value)=>{
73
- if (!isResolved) {
74
- isResolved = true;
75
- cleanup();
76
- resolve(value);
77
- }
78
- };
79
- const safeReject = (error)=>{
80
- if (!isResolved) {
81
- isResolved = true;
82
- cleanup();
83
- reject(error);
84
- }
85
- };
86
- try {
87
- // Ensure stdin is referenced so the process doesn't exit while waiting for input
88
- if (typeof process.stdin.ref === 'function') {
89
- process.stdin.ref();
90
- }
91
- process.stdin.setRawMode(true);
92
- process.stdin.resume();
93
- dataHandler = (key)=>{
94
- try {
95
- const keyStr = key.toString().toLowerCase();
96
- const choice = choices.find((c)=>c.key === keyStr);
97
- if (choice) {
98
- logger.info(`Selected: ${choice.label}\n`);
99
- safeResolve(choice.key);
100
- }
101
- } catch (error) {
102
- safeReject(error instanceof Error ? error : new Error('Unknown error processing input'));
103
- }
104
- };
105
- errorHandler = (error)=>{
106
- safeReject(error);
107
- };
108
- process.stdin.on('data', dataHandler);
109
- process.stdin.on('error', errorHandler);
110
- } catch (error) {
111
- safeReject(error instanceof Error ? error : new Error('Failed to setup input handlers'));
112
- }
113
- });
114
- }
115
- /**
116
- * Secure temporary file handle that prevents TOCTOU vulnerabilities
117
- */ class SecureTempFile {
118
- /**
119
- * Create a secure temporary file with proper permissions and atomic operations
120
- * @param prefix Prefix for the temporary filename
121
- * @param extension File extension (e.g., '.txt', '.md')
122
- * @returns Promise resolving to SecureTempFile instance
123
- */ static async create(prefix = 'kodrdriv', extension = '.txt') {
124
- const tmpDir = os.tmpdir();
125
- // Ensure temp directory exists and is writable (skip check in test environments)
126
- if (!process.env.VITEST) {
127
- try {
128
- await fs.access(tmpDir, fs.constants.W_OK);
129
- } catch (error) {
130
- // Try to create the directory if it doesn't exist
131
- try {
132
- await fs.mkdir(tmpDir, {
133
- recursive: true,
134
- mode: 0o700
135
- });
136
- } catch (mkdirError) {
137
- throw new Error(`Temp directory not writable: ${tmpDir} - ${error.message}. Failed to create: ${mkdirError.message}`);
138
- }
139
- }
140
- }
141
- const tmpFilePath = path.join(tmpDir, `${prefix}_${Date.now()}_${Math.random().toString(36).substring(7)}${extension}`);
142
- // Create file with exclusive access and restrictive permissions (owner read/write only)
143
- // Using 'wx' flag ensures exclusive creation (fails if file exists)
144
- let fd;
145
- try {
146
- fd = await fs.open(tmpFilePath, 'wx', 0o600);
147
- } catch (error) {
148
- if (error.code === 'EEXIST') {
149
- // Highly unlikely with timestamp + random suffix, but handle it
150
- throw new Error(`Temporary file already exists: ${tmpFilePath}`);
151
- }
152
- throw new Error(`Failed to create temporary file: ${error.message}`);
153
- }
154
- return new SecureTempFile(tmpFilePath, fd);
155
- }
156
- /**
157
- * Get the file path (use with caution in external commands)
158
- */ get path() {
159
- if (this.isCleanedUp) {
160
- throw new Error('Temp file has been cleaned up');
161
- }
162
- return this.filePath;
163
- }
164
- /**
165
- * Write content to the temporary file
166
- */ async writeContent(content) {
167
- if (!this.fd || this.isCleanedUp) {
168
- throw new Error('Temp file is not available for writing');
169
- }
170
- await this.fd.writeFile(content, 'utf8');
171
- }
172
- /**
173
- * Read content from the temporary file
174
- */ async readContent() {
175
- if (!this.fd || this.isCleanedUp) {
176
- throw new Error('Temp file is not available for reading');
177
- }
178
- const content = await this.fd.readFile('utf8');
179
- return content;
180
- }
181
- /**
182
- * Close the file handle
183
- */ async close() {
184
- if (this.fd && !this.isCleanedUp) {
185
- await this.fd.close();
186
- this.fd = null;
187
- }
188
- }
189
- /**
190
- * Securely cleanup the temporary file - prevents TOCTOU by using file descriptor
191
- */ async cleanup() {
192
- if (this.isCleanedUp) {
193
- return; // Already cleaned up
194
- }
195
- try {
196
- // Close file descriptor first if still open
197
- if (this.fd) {
198
- await this.fd.close();
199
- this.fd = null;
200
- }
201
- // Now safely remove the file
202
- // Use fs.unlink which is safer than checking existence first
203
- await fs.unlink(this.filePath);
204
- } catch (error) {
205
- // Only ignore ENOENT (file not found) errors
206
- if (error.code !== 'ENOENT') {
207
- const logger = getDryRunLogger(false);
208
- logger.warn(`Failed to cleanup temp file ${this.filePath}: ${error.message}`);
209
- // Don't throw here to avoid masking main operations
210
- }
211
- } finally{
212
- this.isCleanedUp = true;
213
- }
214
- }
215
- constructor(filePath, fd){
216
- _define_property(this, "fd", null);
217
- _define_property(this, "filePath", void 0);
218
- _define_property(this, "isCleanedUp", false);
219
- this.filePath = filePath;
220
- this.fd = fd;
221
- }
222
- }
223
- /**
224
- * Open content in user's editor for editing
225
- * @param content Initial content to edit
226
- * @param templateLines Additional template lines to include (will be filtered out)
227
- * @param fileExtension File extension for syntax highlighting
228
- * @returns Promise resolving to the edited content
229
- */ async function editContentInEditor(content, templateLines = [], fileExtension = '.txt') {
230
- const logger = getDryRunLogger(false);
231
- const editor = process.env.EDITOR || process.env.VISUAL || 'vi';
232
- const secureTempFile = await SecureTempFile.create('kodrdriv_edit', fileExtension);
233
- try {
234
- // Build template content
235
- const templateContent = [
236
- ...templateLines,
237
- ...templateLines.length > 0 ? [
238
- ''
239
- ] : [],
240
- content,
241
- ''
242
- ].join('\n');
243
- await secureTempFile.writeContent(templateContent);
244
- await secureTempFile.close(); // Close before external editor access
245
- logger.info(`📝 Opening ${editor} to edit content...`);
246
- // Open the editor synchronously
247
- const result = spawnSync(editor, [
248
- secureTempFile.path
249
- ], {
250
- stdio: 'inherit'
251
- });
252
- if (result.error) {
253
- throw new Error(`Failed to launch editor '${editor}': ${result.error.message}`);
254
- }
255
- // Read the file back in, stripping comment lines
256
- const fileContent = (await fs.readFile(secureTempFile.path, 'utf8')).split('\n').filter((line)=>!line.trim().startsWith('#')).join('\n').trim();
257
- if (!fileContent) {
258
- throw new Error('Content is empty after editing');
259
- }
260
- logger.info('✅ Content updated successfully');
261
- return {
262
- content: fileContent,
263
- wasEdited: fileContent !== content.trim()
264
- };
265
- } finally{
266
- // Always clean up the temp file securely
267
- await secureTempFile.cleanup();
268
- }
269
- }
270
- /**
271
- * Standard choices for interactive feedback loops
272
- */ const STANDARD_CHOICES = {
273
- CONFIRM: {
274
- key: 'c',
275
- label: 'Confirm and proceed'
276
- },
277
- EDIT: {
278
- key: 'e',
279
- label: 'Edit in editor'
280
- },
281
- SKIP: {
282
- key: 's',
283
- label: 'Skip and abort'
284
- },
285
- IMPROVE: {
286
- key: 'i',
287
- label: 'Improve with LLM feedback'
288
- }
289
- };
290
- /**
291
- * Get text input from the user
292
- * @param prompt The prompt message to display
293
- * @param options Additional options for customizing behavior
294
- * @returns Promise resolving to the user's text input
295
- */ async function getUserTextInput(prompt, options = {}) {
296
- const logger = getDryRunLogger(false);
297
- // Check if stdin is a TTY (terminal) or piped
298
- if (!process.stdin.isTTY) {
299
- logger.error('⚠️ STDIN is piped but interactive text input is required');
300
- logger.error(' Interactive text input cannot be used when input is piped');
301
- logger.error(' Solutions:');
302
- logger.error(' • Use terminal input instead of piping');
303
- // Add any additional suggestions
304
- if (options.nonTtyErrorSuggestions) {
305
- options.nonTtyErrorSuggestions.forEach((suggestion)=>{
306
- logger.error(` • ${suggestion}`);
307
- });
308
- }
309
- throw new Error('Interactive text input requires a terminal');
310
- }
311
- logger.info(prompt);
312
- logger.info('(Press Enter when done, or type Ctrl+C to cancel)');
313
- logger.info('');
314
- return new Promise((resolve, reject)=>{
315
- let inputBuffer = '';
316
- let isResolved = false;
317
- let dataHandler = null;
318
- let errorHandler = null;
319
- const cleanup = ()=>{
320
- if (dataHandler) {
321
- process.stdin.removeListener('data', dataHandler);
322
- }
323
- if (errorHandler) {
324
- process.stdin.removeListener('error', errorHandler);
325
- }
326
- try {
327
- process.stdin.pause();
328
- // Detach stdin again now that we're done
329
- if (typeof process.stdin.unref === 'function') {
330
- process.stdin.unref();
331
- }
332
- } catch {
333
- // Ignore cleanup errors
334
- }
335
- };
336
- const safeResolve = (value)=>{
337
- if (!isResolved) {
338
- isResolved = true;
339
- cleanup();
340
- resolve(value);
341
- }
342
- };
343
- const safeReject = (error)=>{
344
- if (!isResolved) {
345
- isResolved = true;
346
- cleanup();
347
- reject(error);
348
- }
349
- };
350
- try {
351
- // Ensure stdin is referenced so the process doesn't exit while waiting for input
352
- if (typeof process.stdin.ref === 'function') {
353
- process.stdin.ref();
354
- }
355
- process.stdin.setEncoding('utf8');
356
- process.stdin.resume();
357
- dataHandler = (chunk)=>{
358
- try {
359
- inputBuffer += chunk;
360
- // Check if user pressed Enter (newline character)
361
- if (inputBuffer.includes('\n')) {
362
- const userInput = inputBuffer.replace(/\n$/, '').trim();
363
- if (userInput === '') {
364
- logger.warn('Empty input received. Please provide feedback text.');
365
- safeReject(new Error('Empty input received'));
366
- } else {
367
- logger.info(`✅ Received feedback: "${userInput}"\n`);
368
- safeResolve(userInput);
369
- }
370
- }
371
- } catch (error) {
372
- safeReject(error instanceof Error ? error : new Error('Unknown error processing input'));
373
- }
374
- };
375
- errorHandler = (error)=>{
376
- safeReject(error);
377
- };
378
- process.stdin.on('data', dataHandler);
379
- process.stdin.on('error', errorHandler);
380
- } catch (error) {
381
- safeReject(error instanceof Error ? error : new Error('Failed to setup input handlers'));
382
- }
383
- });
384
- }
385
- /**
386
- * Get LLM improvement feedback from the user using the editor
387
- * @param contentType Type of content being improved (e.g., 'commit message', 'release notes')
388
- * @param currentContent The current content to be improved
389
- * @returns Promise resolving to the user's feedback text
390
- */ async function getLLMFeedbackInEditor(contentType, currentContent) {
391
- const templateLines = [
392
- '# Provide Your Instructions and Guidance for a Revision Here',
393
- '#',
394
- '# Type your guidance above this line. Be specific about what you want changed,',
395
- '# added, or improved. You can also edit the original content below directly',
396
- '# to provide examples or show desired changes.',
397
- '#',
398
- '# Lines starting with "#" will be ignored.',
399
- '',
400
- '### YOUR FEEDBACK AND GUIDANCE:',
401
- '',
402
- '# (Type your improvement instructions here)',
403
- '',
404
- `### ORIGINAL ${contentType.toUpperCase()}:`,
405
- ''
406
- ];
407
- const result = await editContentInEditor(currentContent, templateLines, '.md');
408
- // Extract just the feedback section (everything before the original content)
409
- const lines = result.content.split('\n');
410
- const originalSectionIndex = lines.findIndex((line)=>line.trim().toLowerCase().startsWith('### original'));
411
- let feedback;
412
- if (originalSectionIndex >= 0) {
413
- // Take everything before the "### ORIGINAL" section
414
- feedback = lines.slice(0, originalSectionIndex).join('\n').trim();
415
- } else {
416
- // If no original section found, take everything
417
- feedback = result.content.trim();
418
- }
419
- // Remove the feedback header if it exists
420
- feedback = feedback.replace(/^### YOUR FEEDBACK AND GUIDANCE:\s*/i, '').trim();
421
- if (!feedback) {
422
- throw new Error('No feedback provided. Please provide improvement instructions.');
423
- }
424
- return feedback;
425
- }
426
- /**
427
- * Check if interactive mode is available (TTY check)
428
- * @param errorMessage Custom error message to throw if TTY not available
429
- * @throws Error if not in TTY environment
430
- */ function requireTTY(errorMessage = 'Interactive mode requires a terminal. Use --dry-run instead.') {
431
- if (!process.stdin.isTTY) {
432
- const logger = getDryRunLogger(false);
433
- logger.error('❌ Interactive mode requires a terminal (TTY)');
434
- logger.error(' Solutions:');
435
- logger.error(' • Run without piping input');
436
- logger.error(' • Use --dry-run to see the generated content');
437
- throw new Error(errorMessage);
438
- }
439
- }
440
5
  /**
441
6
  * Generic LLM improvement function that can be configured for different content types
7
+ *
8
+ * This is kodrdriv-specific orchestration logic that combines multiple ai-service
9
+ * primitives into a higher-level workflow.
10
+ *
442
11
  * @param currentContent The current content to improve
443
12
  * @param runConfig Runtime configuration
444
13
  * @param promptConfig Prompt configuration
@@ -459,5 +28,5 @@ function _define_property(obj, key, value) {
459
28
  return finalResult;
460
29
  }
461
30
 
462
- export { STANDARD_CHOICES, SecureTempFile, editContentInEditor, getLLMFeedbackInEditor, getUserChoice, getUserTextInput, improveContentWithLLM, requireTTY };
31
+ export { improveContentWithLLM };
463
32
  //# sourceMappingURL=interactive.js.map