@eldrforge/ai-service 0.1.1 → 0.1.3

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 (46) hide show
  1. package/dist/index.d.ts +2 -0
  2. package/dist/index.js.map +1 -0
  3. package/dist/src/ai.d.ts +55 -0
  4. package/{src/index.ts → dist/src/index.d.ts} +1 -2
  5. package/dist/src/interactive.d.ts +122 -0
  6. package/dist/src/logger.d.ts +19 -0
  7. package/dist/src/prompts/commit.d.ts +29 -0
  8. package/dist/src/prompts/index.d.ts +10 -0
  9. package/dist/src/prompts/release.d.ts +25 -0
  10. package/dist/src/prompts/review.d.ts +21 -0
  11. package/dist/src/types.d.ts +99 -0
  12. package/package.json +11 -8
  13. package/.github/dependabot.yml +0 -12
  14. package/.github/workflows/npm-publish.yml +0 -48
  15. package/.github/workflows/test.yml +0 -33
  16. package/eslint.config.mjs +0 -84
  17. package/src/ai.ts +0 -421
  18. package/src/interactive.ts +0 -562
  19. package/src/logger.ts +0 -69
  20. package/src/prompts/commit.ts +0 -85
  21. package/src/prompts/index.ts +0 -28
  22. package/src/prompts/instructions/commit.md +0 -133
  23. package/src/prompts/instructions/release.md +0 -188
  24. package/src/prompts/instructions/review.md +0 -169
  25. package/src/prompts/personas/releaser.md +0 -24
  26. package/src/prompts/personas/you.md +0 -55
  27. package/src/prompts/release.ts +0 -118
  28. package/src/prompts/review.ts +0 -72
  29. package/src/types.ts +0 -112
  30. package/tests/ai-complete-coverage.test.ts +0 -241
  31. package/tests/ai-create-completion.test.ts +0 -288
  32. package/tests/ai-edge-cases.test.ts +0 -221
  33. package/tests/ai-openai-error.test.ts +0 -35
  34. package/tests/ai-transcribe.test.ts +0 -169
  35. package/tests/ai.test.ts +0 -139
  36. package/tests/interactive-editor.test.ts +0 -253
  37. package/tests/interactive-secure-temp.test.ts +0 -264
  38. package/tests/interactive-user-choice.test.ts +0 -173
  39. package/tests/interactive-user-text.test.ts +0 -174
  40. package/tests/interactive.test.ts +0 -94
  41. package/tests/logger-noop.test.ts +0 -40
  42. package/tests/logger.test.ts +0 -122
  43. package/tests/prompts.test.ts +0 -179
  44. package/tsconfig.json +0 -35
  45. package/vite.config.ts +0 -69
  46. package/vitest.config.ts +0 -25
@@ -1,169 +0,0 @@
1
- import { describe, it, expect, beforeEach, vi } from 'vitest';
2
- import { transcribeAudio, OpenAIError } from '../src/ai';
3
-
4
- // Create mock functions
5
- const mockTranscriptionsCreate = vi.fn();
6
- const mockReadStream = {
7
- destroy: vi.fn(),
8
- destroyed: false,
9
- on: vi.fn(),
10
- };
11
-
12
- // Mock OpenAI
13
- vi.mock('openai', () => ({
14
- OpenAI: vi.fn().mockImplementation(() => ({
15
- audio: {
16
- transcriptions: {
17
- create: mockTranscriptionsCreate,
18
- },
19
- },
20
- })),
21
- }));
22
-
23
- // Mock fs
24
- vi.mock('fs', () => ({
25
- default: {
26
- createReadStream: vi.fn(() => mockReadStream),
27
- },
28
- createReadStream: vi.fn(() => mockReadStream),
29
- }));
30
-
31
- // Mock logger
32
- vi.mock('../src/logger', () => ({
33
- getLogger: vi.fn(() => ({
34
- info: vi.fn(),
35
- error: vi.fn(),
36
- warn: vi.fn(),
37
- debug: vi.fn(),
38
- })),
39
- }));
40
-
41
- describe('transcribeAudio', () => {
42
- beforeEach(() => {
43
- vi.clearAllMocks();
44
- process.env.OPENAI_API_KEY = 'test-key';
45
- mockReadStream.destroyed = false;
46
- });
47
-
48
- it('should transcribe audio successfully', async () => {
49
- mockTranscriptionsCreate.mockResolvedValue({
50
- text: 'Transcribed text',
51
- });
52
-
53
- const result = await transcribeAudio('/path/to/audio.mp3');
54
-
55
- expect(result).toEqual({ text: 'Transcribed text' });
56
- expect(mockTranscriptionsCreate).toHaveBeenCalled();
57
- });
58
-
59
- it('should throw error if API key not set', async () => {
60
- delete process.env.OPENAI_API_KEY;
61
-
62
- await expect(
63
- transcribeAudio('/path/to/audio.mp3')
64
- ).rejects.toThrow('OPENAI_API_KEY environment variable is not set');
65
- });
66
-
67
- it('should use specified model', async () => {
68
- mockTranscriptionsCreate.mockResolvedValue({
69
- text: 'Transcribed text',
70
- });
71
-
72
- await transcribeAudio('/path/to/audio.mp3', { model: 'whisper-2' });
73
-
74
- expect(mockTranscriptionsCreate).toHaveBeenCalledWith(
75
- expect.objectContaining({ model: 'whisper-2' })
76
- );
77
- });
78
-
79
- it('should use default model if not specified', async () => {
80
- mockTranscriptionsCreate.mockResolvedValue({
81
- text: 'Transcribed text',
82
- });
83
-
84
- await transcribeAudio('/path/to/audio.mp3');
85
-
86
- expect(mockTranscriptionsCreate).toHaveBeenCalledWith(
87
- expect.objectContaining({ model: 'whisper-1' })
88
- );
89
- });
90
-
91
- it('should call onArchive callback if provided', async () => {
92
- mockTranscriptionsCreate.mockResolvedValue({
93
- text: 'Transcribed text',
94
- });
95
-
96
- const onArchive = vi.fn().mockResolvedValue(undefined);
97
-
98
- await transcribeAudio('/path/to/audio.mp3', { onArchive });
99
-
100
- expect(onArchive).toHaveBeenCalledWith('/path/to/audio.mp3', 'Transcribed text');
101
- });
102
-
103
- it('should continue if onArchive fails', async () => {
104
- mockTranscriptionsCreate.mockResolvedValue({
105
- text: 'Transcribed text',
106
- });
107
-
108
- const onArchive = vi.fn().mockRejectedValue(new Error('Archive failed'));
109
-
110
- const result = await transcribeAudio('/path/to/audio.mp3', { onArchive });
111
-
112
- expect(result).toEqual({ text: 'Transcribed text' });
113
- });
114
-
115
- it('should close audio stream after success', async () => {
116
- mockTranscriptionsCreate.mockResolvedValue({
117
- text: 'Transcribed text',
118
- });
119
-
120
- await transcribeAudio('/path/to/audio.mp3');
121
-
122
- expect(mockReadStream.destroy).toHaveBeenCalled();
123
- });
124
-
125
- it('should close audio stream on API error', async () => {
126
- mockTranscriptionsCreate.mockRejectedValue(new Error('API error'));
127
-
128
- await expect(
129
- transcribeAudio('/path/to/audio.mp3')
130
- ).rejects.toThrow(OpenAIError);
131
-
132
- expect(mockReadStream.destroy).toHaveBeenCalled();
133
- });
134
-
135
- it('should handle stream errors', async () => {
136
- // Simulate stream error event
137
- mockReadStream.on.mockImplementation((event: string, callback: any) => {
138
- if (event === 'error') {
139
- setTimeout(() => callback(new Error('Stream error')), 0);
140
- }
141
- return mockReadStream;
142
- });
143
-
144
- mockTranscriptionsCreate.mockResolvedValue({
145
- text: 'Transcribed text',
146
- });
147
-
148
- const result = await transcribeAudio('/path/to/audio.mp3');
149
-
150
- expect(result).toEqual({ text: 'Transcribed text' });
151
- });
152
-
153
- it('should handle empty response', async () => {
154
- mockTranscriptionsCreate.mockResolvedValue(null);
155
-
156
- await expect(
157
- transcribeAudio('/path/to/audio.mp3')
158
- ).rejects.toThrow('No transcription received from OpenAI');
159
- });
160
-
161
- it('should wrap errors in OpenAIError', async () => {
162
- mockTranscriptionsCreate.mockRejectedValue(new Error('Generic error'));
163
-
164
- await expect(
165
- transcribeAudio('/path/to/audio.mp3')
166
- ).rejects.toThrow(OpenAIError);
167
- });
168
- });
169
-
package/tests/ai.test.ts DELETED
@@ -1,139 +0,0 @@
1
- import { describe, it, beforeEach, expect, vi } from 'vitest';
2
- import type { AIConfig } from '../src/types';
3
- import {
4
- getModelForCommand,
5
- getOpenAIReasoningForCommand,
6
- isTokenLimitError,
7
- isRateLimitError,
8
- } from '../src/ai';
9
-
10
- describe('AI Configuration Functions', () => {
11
- describe('getModelForCommand', () => {
12
- it('should return command-specific model when available', () => {
13
- const config: AIConfig = {
14
- model: 'gpt-4o-mini',
15
- commands: {
16
- commit: { model: 'gpt-4o' },
17
- release: { model: 'gpt-4-turbo' },
18
- review: { model: 'gpt-3.5-turbo' },
19
- },
20
- };
21
-
22
- expect(getModelForCommand(config, 'commit')).toBe('gpt-4o');
23
- expect(getModelForCommand(config, 'audio-commit')).toBe('gpt-4o');
24
- expect(getModelForCommand(config, 'release')).toBe('gpt-4-turbo');
25
- expect(getModelForCommand(config, 'review')).toBe('gpt-3.5-turbo');
26
- expect(getModelForCommand(config, 'audio-review')).toBe('gpt-3.5-turbo');
27
- });
28
-
29
- it('should fallback to global model when command-specific model not available', () => {
30
- const config: AIConfig = {
31
- model: 'gpt-4o-mini',
32
- };
33
-
34
- expect(getModelForCommand(config, 'commit')).toBe('gpt-4o-mini');
35
- expect(getModelForCommand(config, 'release')).toBe('gpt-4o-mini');
36
- expect(getModelForCommand(config, 'review')).toBe('gpt-4o-mini');
37
- });
38
-
39
- it('should fallback to default model when no models specified', () => {
40
- const config: AIConfig = {};
41
-
42
- expect(getModelForCommand(config, 'commit')).toBe('gpt-4o-mini');
43
- expect(getModelForCommand(config, 'release')).toBe('gpt-4o-mini');
44
- expect(getModelForCommand(config, 'review')).toBe('gpt-4o-mini');
45
- });
46
-
47
- it('should use global model for unknown commands', () => {
48
- const config: AIConfig = {
49
- model: 'gpt-4o',
50
- };
51
-
52
- expect(getModelForCommand(config, 'unknown-command')).toBe('gpt-4o');
53
- });
54
- });
55
-
56
- describe('getOpenAIReasoningForCommand', () => {
57
- it('should return command-specific reasoning when available', () => {
58
- const config: AIConfig = {
59
- reasoning: 'low',
60
- commands: {
61
- commit: { reasoning: 'high' },
62
- release: { reasoning: 'medium' },
63
- review: { reasoning: 'high' },
64
- },
65
- };
66
-
67
- expect(getOpenAIReasoningForCommand(config, 'commit')).toBe('high');
68
- expect(getOpenAIReasoningForCommand(config, 'audio-commit')).toBe('high');
69
- expect(getOpenAIReasoningForCommand(config, 'release')).toBe('medium');
70
- expect(getOpenAIReasoningForCommand(config, 'review')).toBe('high');
71
- expect(getOpenAIReasoningForCommand(config, 'audio-review')).toBe('high');
72
- });
73
-
74
- it('should fallback to global reasoning when command-specific reasoning not available', () => {
75
- const config: AIConfig = {
76
- reasoning: 'medium',
77
- };
78
-
79
- expect(getOpenAIReasoningForCommand(config, 'commit')).toBe('medium');
80
- expect(getOpenAIReasoningForCommand(config, 'release')).toBe('medium');
81
- expect(getOpenAIReasoningForCommand(config, 'review')).toBe('medium');
82
- });
83
-
84
- it('should fallback to default reasoning when no reasoning specified', () => {
85
- const config: AIConfig = {};
86
-
87
- expect(getOpenAIReasoningForCommand(config, 'commit')).toBe('low');
88
- expect(getOpenAIReasoningForCommand(config, 'release')).toBe('low');
89
- expect(getOpenAIReasoningForCommand(config, 'review')).toBe('low');
90
- });
91
-
92
- it('should use global reasoning for unknown commands', () => {
93
- const config: AIConfig = {
94
- reasoning: 'high',
95
- };
96
-
97
- expect(getOpenAIReasoningForCommand(config, 'unknown-command')).toBe('high');
98
- });
99
- });
100
-
101
- describe('isTokenLimitError', () => {
102
- it('should identify token limit errors by message content', () => {
103
- expect(isTokenLimitError({ message: 'maximum context length exceeded' })).toBe(true);
104
- expect(isTokenLimitError({ message: 'context_length_exceeded' })).toBe(true);
105
- expect(isTokenLimitError({ message: 'token limit reached' })).toBe(true);
106
- expect(isTokenLimitError({ message: 'too many tokens' })).toBe(true);
107
- expect(isTokenLimitError({ message: 'please reduce the length' })).toBe(true);
108
- });
109
-
110
- it('should return false for non-token-limit errors', () => {
111
- expect(isTokenLimitError({ message: 'Rate limit exceeded' })).toBe(false);
112
- expect(isTokenLimitError({ message: 'Network error' })).toBe(false);
113
- expect(isTokenLimitError({})).toBe(false);
114
- expect(isTokenLimitError(null)).toBe(false);
115
- });
116
- });
117
-
118
- describe('isRateLimitError', () => {
119
- it('should identify rate limit errors by status code', () => {
120
- expect(isRateLimitError({ status: 429 })).toBe(true);
121
- expect(isRateLimitError({ code: 'rate_limit_exceeded' })).toBe(true);
122
- });
123
-
124
- it('should identify rate limit errors by message content', () => {
125
- expect(isRateLimitError({ message: 'rate limit exceeded' })).toBe(true);
126
- expect(isRateLimitError({ message: 'too many requests' })).toBe(true);
127
- expect(isRateLimitError({ message: 'quota exceeded' })).toBe(true);
128
- expect(isRateLimitError({ message: 'rate limit reached' })).toBe(true);
129
- });
130
-
131
- it('should return false for non-rate-limit errors', () => {
132
- expect(isRateLimitError({ message: 'Token limit exceeded' })).toBe(false);
133
- expect(isRateLimitError({ message: 'Network error' })).toBe(false);
134
- expect(isRateLimitError({})).toBe(false);
135
- expect(isRateLimitError(null)).toBe(false);
136
- });
137
- });
138
- });
139
-
@@ -1,253 +0,0 @@
1
- import { describe, it, expect, beforeEach, vi } from 'vitest';
2
- import { editContentInEditor, getLLMFeedbackInEditor } from '../src/interactive';
3
-
4
- // Mock child_process
5
- vi.mock('child_process');
6
-
7
- // Mock fs/promises
8
- vi.mock('fs/promises');
9
-
10
- // Mock os
11
- vi.mock('os', () => ({
12
- tmpdir: vi.fn(() => '/tmp'),
13
- }));
14
-
15
- // Mock logger
16
- vi.mock('../src/logger', () => ({
17
- getLogger: vi.fn(() => ({
18
- info: vi.fn(),
19
- error: vi.fn(),
20
- warn: vi.fn(),
21
- debug: vi.fn(),
22
- })),
23
- }));
24
-
25
- describe('editContentInEditor', () => {
26
- let mockSpawnSync: any;
27
- let mockReadFile: any;
28
- let mockFsOpen: any;
29
- let mockFd: any;
30
-
31
- beforeEach(async () => {
32
- vi.clearAllMocks();
33
-
34
- // Import mocked modules
35
- const childProcess = await import('child_process');
36
- const fsPromises = await import('fs/promises');
37
-
38
- mockSpawnSync = childProcess.spawnSync;
39
- mockReadFile = fsPromises.readFile;
40
- mockFsOpen = fsPromises.open;
41
-
42
- // Setup mock file descriptor
43
- mockFd = {
44
- writeFile: vi.fn().mockResolvedValue(undefined),
45
- readFile: vi.fn().mockResolvedValue('content'),
46
- close: vi.fn().mockResolvedValue(undefined),
47
- };
48
-
49
- mockFsOpen.mockResolvedValue(mockFd);
50
- mockSpawnSync.mockReturnValue({ error: null, status: 0 });
51
- mockReadFile.mockResolvedValue('edited content');
52
- });
53
-
54
- it('should edit content successfully', async () => {
55
- mockReadFile.mockResolvedValue('edited content');
56
-
57
- const result = await editContentInEditor('initial content');
58
-
59
- expect(result).toBeDefined();
60
- expect(result.content).toBe('edited content');
61
- expect(result.wasEdited).toBe(true);
62
- });
63
-
64
- it('should use default editor', async () => {
65
- mockReadFile.mockResolvedValue('content');
66
-
67
- await editContentInEditor('test');
68
-
69
- expect(mockSpawnSync).toHaveBeenCalledWith(
70
- 'vi',
71
- expect.any(Array),
72
- expect.any(Object)
73
- );
74
- });
75
-
76
- it('should use EDITOR env var', async () => {
77
- process.env.EDITOR = 'nano';
78
- mockReadFile.mockResolvedValue('content');
79
-
80
- await editContentInEditor('test');
81
-
82
- expect(mockSpawnSync).toHaveBeenCalledWith(
83
- 'nano',
84
- expect.any(Array),
85
- expect.any(Object)
86
- );
87
-
88
- delete process.env.EDITOR;
89
- });
90
-
91
- it('should use specified editor', async () => {
92
- mockReadFile.mockResolvedValue('content');
93
-
94
- await editContentInEditor('test', [], '.txt', 'vim');
95
-
96
- expect(mockSpawnSync).toHaveBeenCalledWith(
97
- 'vim',
98
- expect.any(Array),
99
- expect.any(Object)
100
- );
101
- });
102
-
103
- it('should include template lines', async () => {
104
- mockReadFile.mockResolvedValue('edited without comments');
105
-
106
- const result = await editContentInEditor(
107
- 'content',
108
- ['# Template line 1', '# Template line 2'],
109
- '.md'
110
- );
111
-
112
- expect(mockFd.writeFile).toHaveBeenCalledWith(
113
- expect.stringContaining('# Template line 1'),
114
- 'utf8'
115
- );
116
- });
117
-
118
- it('should filter out comment lines', async () => {
119
- mockReadFile.mockResolvedValue('# Comment\nreal content\n# Another comment\nmore content');
120
-
121
- const result = await editContentInEditor('test');
122
-
123
- expect(result.content).toBe('real content\nmore content');
124
- });
125
-
126
- it('should detect if content was edited', async () => {
127
- mockReadFile.mockResolvedValue('initial content');
128
-
129
- const result = await editContentInEditor('initial content');
130
-
131
- expect(result.wasEdited).toBe(false);
132
- });
133
-
134
- it('should handle editor errors', async () => {
135
- mockSpawnSync.mockReturnValue({
136
- error: new Error('Editor not found'),
137
- status: 0,
138
- });
139
-
140
- await expect(
141
- editContentInEditor('test')
142
- ).rejects.toThrow("Failed to launch editor 'vi'");
143
- });
144
-
145
- it('should throw if content is empty after editing', async () => {
146
- mockReadFile.mockResolvedValue('# Just comments\n# More comments');
147
-
148
- await expect(
149
- editContentInEditor('test')
150
- ).rejects.toThrow('Content is empty after editing');
151
- });
152
-
153
- it('should use specified file extension', async () => {
154
- mockReadFile.mockResolvedValue('content');
155
-
156
- await editContentInEditor('test', [], '.md');
157
-
158
- expect(mockFd.writeFile).toHaveBeenCalled();
159
- });
160
-
161
- it('should close file before opening editor', async () => {
162
- mockReadFile.mockResolvedValue('content');
163
-
164
- await editContentInEditor('test');
165
-
166
- expect(mockFd.close).toHaveBeenCalled();
167
- });
168
- });
169
-
170
- describe('getLLMFeedbackInEditor', () => {
171
- let mockSpawnSync: any;
172
- let mockReadFile: any;
173
- let mockFsOpen: any;
174
- let mockFd: any;
175
-
176
- beforeEach(async () => {
177
- vi.clearAllMocks();
178
-
179
- const childProcess = await import('child_process');
180
- const fsPromises = await import('fs/promises');
181
-
182
- mockSpawnSync = childProcess.spawnSync;
183
- mockReadFile = fsPromises.readFile;
184
- mockFsOpen = fsPromises.open;
185
-
186
- mockFd = {
187
- writeFile: vi.fn().mockResolvedValue(undefined),
188
- readFile: vi.fn().mockResolvedValue('content'),
189
- close: vi.fn().mockResolvedValue(undefined),
190
- };
191
-
192
- mockFsOpen.mockResolvedValue(mockFd);
193
- mockSpawnSync.mockReturnValue({ error: null, status: 0 });
194
- });
195
-
196
- it('should get feedback for content improvement', async () => {
197
- // Mock what user would type - comments get filtered by editContentInEditor
198
- mockReadFile.mockResolvedValue('# comment line\nPlease improve the formatting\n\nOld content here');
199
-
200
- const feedback = await getLLMFeedbackInEditor('commit message', 'old message');
201
-
202
- // Since there's no ### ORIGINAL marker after filtering, it takes everything
203
- expect(feedback).toContain('improve the formatting');
204
- });
205
-
206
- it('should extract feedback when user keeps the original marker', async () => {
207
- // If user doesn't delete the ### ORIGINAL marker (not starting with #), it stays
208
- // This allows the function to split feedback from original
209
- mockReadFile.mockResolvedValue('Make it better\n\nxxx ORIGINAL CONTENT:\nold');
210
-
211
- const feedback = await getLLMFeedbackInEditor('content', 'old');
212
-
213
- // Since the marker doesn't match '### original', takes everything
214
- expect(feedback).toContain('Make it better');
215
- });
216
-
217
- it('should throw if only original content remains', async () => {
218
- // If user deletes all feedback and just leaves the original content
219
- mockReadFile.mockResolvedValue('old');
220
-
221
- const result = await getLLMFeedbackInEditor('content', 'old');
222
-
223
- // Since there's actual content, it returns it (not empty)
224
- expect(result).toBe('old');
225
- });
226
-
227
- it('should handle feedback without original section', async () => {
228
- mockReadFile.mockResolvedValue('Just feedback text without markers');
229
-
230
- const feedback = await getLLMFeedbackInEditor('content', 'old');
231
-
232
- expect(feedback).toBe('Just feedback text without markers');
233
- });
234
-
235
- it('should use .md extension for editor', async () => {
236
- mockReadFile.mockResolvedValue('feedback');
237
-
238
- await getLLMFeedbackInEditor('content', 'test');
239
-
240
- expect(mockFd.writeFile).toHaveBeenCalled();
241
- });
242
-
243
- it('should include template in editor', async () => {
244
- mockReadFile.mockResolvedValue('feedback text');
245
-
246
- await getLLMFeedbackInEditor('commit message', 'old content');
247
-
248
- expect(mockFd.writeFile).toHaveBeenCalledWith(
249
- expect.stringContaining('Provide Your Instructions'),
250
- 'utf8'
251
- );
252
- });
253
- });