mcp-rubber-duck 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/.dockerignore +19 -0
  2. package/.env.desktop.example +145 -0
  3. package/.env.example +45 -0
  4. package/.env.pi.example +106 -0
  5. package/.env.template +165 -0
  6. package/.eslintrc.json +40 -0
  7. package/.github/ISSUE_TEMPLATE/bug_report.md +65 -0
  8. package/.github/ISSUE_TEMPLATE/feature_request.md +58 -0
  9. package/.github/ISSUE_TEMPLATE/question.md +67 -0
  10. package/.github/pull_request_template.md +111 -0
  11. package/.github/workflows/docker-build.yml +138 -0
  12. package/.github/workflows/release.yml +182 -0
  13. package/.github/workflows/security.yml +141 -0
  14. package/.github/workflows/semantic-release.yml +89 -0
  15. package/.prettierrc +10 -0
  16. package/.releaserc.json +66 -0
  17. package/CHANGELOG.md +95 -0
  18. package/CONTRIBUTING.md +242 -0
  19. package/Dockerfile +62 -0
  20. package/LICENSE +21 -0
  21. package/README.md +803 -0
  22. package/audit-ci.json +8 -0
  23. package/config/claude_desktop.json +14 -0
  24. package/config/config.example.json +91 -0
  25. package/dist/config/config.d.ts +51 -0
  26. package/dist/config/config.d.ts.map +1 -0
  27. package/dist/config/config.js +301 -0
  28. package/dist/config/config.js.map +1 -0
  29. package/dist/config/types.d.ts +356 -0
  30. package/dist/config/types.d.ts.map +1 -0
  31. package/dist/config/types.js +41 -0
  32. package/dist/config/types.js.map +1 -0
  33. package/dist/index.d.ts +3 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +109 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/providers/duck-provider-enhanced.d.ts +29 -0
  38. package/dist/providers/duck-provider-enhanced.d.ts.map +1 -0
  39. package/dist/providers/duck-provider-enhanced.js +230 -0
  40. package/dist/providers/duck-provider-enhanced.js.map +1 -0
  41. package/dist/providers/enhanced-manager.d.ts +54 -0
  42. package/dist/providers/enhanced-manager.d.ts.map +1 -0
  43. package/dist/providers/enhanced-manager.js +217 -0
  44. package/dist/providers/enhanced-manager.js.map +1 -0
  45. package/dist/providers/manager.d.ts +28 -0
  46. package/dist/providers/manager.d.ts.map +1 -0
  47. package/dist/providers/manager.js +204 -0
  48. package/dist/providers/manager.js.map +1 -0
  49. package/dist/providers/provider.d.ts +29 -0
  50. package/dist/providers/provider.d.ts.map +1 -0
  51. package/dist/providers/provider.js +179 -0
  52. package/dist/providers/provider.js.map +1 -0
  53. package/dist/providers/types.d.ts +69 -0
  54. package/dist/providers/types.d.ts.map +1 -0
  55. package/dist/providers/types.js +2 -0
  56. package/dist/providers/types.js.map +1 -0
  57. package/dist/server.d.ts +24 -0
  58. package/dist/server.d.ts.map +1 -0
  59. package/dist/server.js +501 -0
  60. package/dist/server.js.map +1 -0
  61. package/dist/services/approval.d.ts +44 -0
  62. package/dist/services/approval.d.ts.map +1 -0
  63. package/dist/services/approval.js +159 -0
  64. package/dist/services/approval.js.map +1 -0
  65. package/dist/services/cache.d.ts +21 -0
  66. package/dist/services/cache.d.ts.map +1 -0
  67. package/dist/services/cache.js +63 -0
  68. package/dist/services/cache.js.map +1 -0
  69. package/dist/services/conversation.d.ts +24 -0
  70. package/dist/services/conversation.d.ts.map +1 -0
  71. package/dist/services/conversation.js +108 -0
  72. package/dist/services/conversation.js.map +1 -0
  73. package/dist/services/function-bridge.d.ts +41 -0
  74. package/dist/services/function-bridge.d.ts.map +1 -0
  75. package/dist/services/function-bridge.js +259 -0
  76. package/dist/services/function-bridge.js.map +1 -0
  77. package/dist/services/health.d.ts +17 -0
  78. package/dist/services/health.d.ts.map +1 -0
  79. package/dist/services/health.js +77 -0
  80. package/dist/services/health.js.map +1 -0
  81. package/dist/services/mcp-client-manager.d.ts +49 -0
  82. package/dist/services/mcp-client-manager.d.ts.map +1 -0
  83. package/dist/services/mcp-client-manager.js +279 -0
  84. package/dist/services/mcp-client-manager.js.map +1 -0
  85. package/dist/tools/approve-mcp-request.d.ts +9 -0
  86. package/dist/tools/approve-mcp-request.d.ts.map +1 -0
  87. package/dist/tools/approve-mcp-request.js +111 -0
  88. package/dist/tools/approve-mcp-request.js.map +1 -0
  89. package/dist/tools/ask-duck.d.ts +9 -0
  90. package/dist/tools/ask-duck.d.ts.map +1 -0
  91. package/dist/tools/ask-duck.js +43 -0
  92. package/dist/tools/ask-duck.js.map +1 -0
  93. package/dist/tools/chat-duck.d.ts +9 -0
  94. package/dist/tools/chat-duck.d.ts.map +1 -0
  95. package/dist/tools/chat-duck.js +57 -0
  96. package/dist/tools/chat-duck.js.map +1 -0
  97. package/dist/tools/clear-conversations.d.ts +8 -0
  98. package/dist/tools/clear-conversations.d.ts.map +1 -0
  99. package/dist/tools/clear-conversations.js +17 -0
  100. package/dist/tools/clear-conversations.js.map +1 -0
  101. package/dist/tools/compare-ducks.d.ts +8 -0
  102. package/dist/tools/compare-ducks.d.ts.map +1 -0
  103. package/dist/tools/compare-ducks.js +49 -0
  104. package/dist/tools/compare-ducks.js.map +1 -0
  105. package/dist/tools/duck-council.d.ts +8 -0
  106. package/dist/tools/duck-council.d.ts.map +1 -0
  107. package/dist/tools/duck-council.js +69 -0
  108. package/dist/tools/duck-council.js.map +1 -0
  109. package/dist/tools/get-pending-approvals.d.ts +15 -0
  110. package/dist/tools/get-pending-approvals.d.ts.map +1 -0
  111. package/dist/tools/get-pending-approvals.js +74 -0
  112. package/dist/tools/get-pending-approvals.js.map +1 -0
  113. package/dist/tools/list-ducks.d.ts +9 -0
  114. package/dist/tools/list-ducks.d.ts.map +1 -0
  115. package/dist/tools/list-ducks.js +47 -0
  116. package/dist/tools/list-ducks.js.map +1 -0
  117. package/dist/tools/list-models.d.ts +8 -0
  118. package/dist/tools/list-models.d.ts.map +1 -0
  119. package/dist/tools/list-models.js +72 -0
  120. package/dist/tools/list-models.js.map +1 -0
  121. package/dist/tools/mcp-status.d.ts +17 -0
  122. package/dist/tools/mcp-status.d.ts.map +1 -0
  123. package/dist/tools/mcp-status.js +100 -0
  124. package/dist/tools/mcp-status.js.map +1 -0
  125. package/dist/utils/ascii-art.d.ts +19 -0
  126. package/dist/utils/ascii-art.d.ts.map +1 -0
  127. package/dist/utils/ascii-art.js +73 -0
  128. package/dist/utils/ascii-art.js.map +1 -0
  129. package/dist/utils/logger.d.ts +3 -0
  130. package/dist/utils/logger.d.ts.map +1 -0
  131. package/dist/utils/logger.js +86 -0
  132. package/dist/utils/logger.js.map +1 -0
  133. package/dist/utils/safe-logger.d.ts +23 -0
  134. package/dist/utils/safe-logger.d.ts.map +1 -0
  135. package/dist/utils/safe-logger.js +145 -0
  136. package/dist/utils/safe-logger.js.map +1 -0
  137. package/docker-compose.yml +161 -0
  138. package/jest.config.js +26 -0
  139. package/package.json +65 -0
  140. package/scripts/build-multiarch.sh +290 -0
  141. package/scripts/deploy-raspbian.sh +410 -0
  142. package/scripts/deploy.sh +322 -0
  143. package/scripts/gh-deploy.sh +343 -0
  144. package/scripts/setup-docker-raspbian.sh +530 -0
  145. package/server.json +8 -0
  146. package/src/config/config.ts +357 -0
  147. package/src/config/types.ts +89 -0
  148. package/src/index.ts +114 -0
  149. package/src/providers/duck-provider-enhanced.ts +294 -0
  150. package/src/providers/enhanced-manager.ts +290 -0
  151. package/src/providers/manager.ts +257 -0
  152. package/src/providers/provider.ts +207 -0
  153. package/src/providers/types.ts +78 -0
  154. package/src/server.ts +603 -0
  155. package/src/services/approval.ts +225 -0
  156. package/src/services/cache.ts +79 -0
  157. package/src/services/conversation.ts +146 -0
  158. package/src/services/function-bridge.ts +329 -0
  159. package/src/services/health.ts +107 -0
  160. package/src/services/mcp-client-manager.ts +362 -0
  161. package/src/tools/approve-mcp-request.ts +126 -0
  162. package/src/tools/ask-duck.ts +74 -0
  163. package/src/tools/chat-duck.ts +82 -0
  164. package/src/tools/clear-conversations.ts +24 -0
  165. package/src/tools/compare-ducks.ts +67 -0
  166. package/src/tools/duck-council.ts +88 -0
  167. package/src/tools/get-pending-approvals.ts +90 -0
  168. package/src/tools/list-ducks.ts +65 -0
  169. package/src/tools/list-models.ts +101 -0
  170. package/src/tools/mcp-status.ts +117 -0
  171. package/src/utils/ascii-art.ts +85 -0
  172. package/src/utils/logger.ts +116 -0
  173. package/src/utils/safe-logger.ts +165 -0
  174. package/systemd/mcp-rubber-duck-with-ollama.service +55 -0
  175. package/systemd/mcp-rubber-duck.service +58 -0
  176. package/test-functionality.js +147 -0
  177. package/test-mcp-interface.js +221 -0
  178. package/tests/ascii-art.test.ts +36 -0
  179. package/tests/config.test.ts +239 -0
  180. package/tests/conversation.test.ts +308 -0
  181. package/tests/mcp-bridge.test.ts +291 -0
  182. package/tests/providers.test.ts +269 -0
  183. package/tests/tools/clear-conversations.test.ts +163 -0
  184. package/tsconfig.json +26 -0
@@ -0,0 +1,221 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Test script for MCP interface (simulates Claude Desktop communication)
4
+ import 'dotenv/config';
5
+ import { spawn } from 'child_process';
6
+ import { Readable, Writable } from 'stream';
7
+
8
+ console.log('🦆 Testing MCP Rubber Duck via MCP Interface\n');
9
+
10
+ class MCPClient {
11
+ constructor() {
12
+ this.messageId = 0;
13
+ this.responses = new Map();
14
+ }
15
+
16
+ start() {
17
+ return new Promise((resolve, reject) => {
18
+ // Spawn the MCP server as Claude Desktop would
19
+ this.process = spawn('node', ['dist/index.js'], {
20
+ env: {
21
+ ...process.env,
22
+ MCP_SERVER: 'true',
23
+ LOG_LEVEL: 'error'
24
+ }
25
+ });
26
+
27
+ this.process.stderr.on('data', (data) => {
28
+ console.error(`Server error: ${data}`);
29
+ });
30
+
31
+ this.process.on('error', reject);
32
+ this.process.on('exit', (code) => {
33
+ console.log(`Server exited with code ${code}`);
34
+ });
35
+
36
+ // Set up JSON-RPC communication
37
+ this.setupCommunication();
38
+
39
+ // Initialize the connection
40
+ setTimeout(() => {
41
+ this.sendRequest('initialize', {
42
+ protocolVersion: '2024-11-05',
43
+ capabilities: {},
44
+ clientInfo: {
45
+ name: 'test-client',
46
+ version: '1.0.0'
47
+ }
48
+ }).then(resolve).catch(reject);
49
+ }, 100);
50
+ });
51
+ }
52
+
53
+ setupCommunication() {
54
+ let buffer = '';
55
+
56
+ this.process.stdout.on('data', (chunk) => {
57
+ buffer += chunk.toString();
58
+ const lines = buffer.split('\n');
59
+ buffer = lines.pop() || '';
60
+
61
+ for (const line of lines) {
62
+ if (line.trim()) {
63
+ try {
64
+ const message = JSON.parse(line);
65
+ this.handleMessage(message);
66
+ } catch (e) {
67
+ console.error('Failed to parse message:', line);
68
+ }
69
+ }
70
+ }
71
+ });
72
+ }
73
+
74
+ handleMessage(message) {
75
+ if (message.id && this.responses.has(message.id)) {
76
+ const { resolve, reject } = this.responses.get(message.id);
77
+ this.responses.delete(message.id);
78
+
79
+ if (message.error) {
80
+ reject(new Error(message.error.message));
81
+ } else {
82
+ resolve(message.result);
83
+ }
84
+ }
85
+ }
86
+
87
+ sendRequest(method, params = {}) {
88
+ return new Promise((resolve, reject) => {
89
+ const id = `msg_${++this.messageId}`;
90
+ const request = {
91
+ jsonrpc: '2.0',
92
+ id,
93
+ method,
94
+ params
95
+ };
96
+
97
+ this.responses.set(id, { resolve, reject });
98
+ this.process.stdin.write(JSON.stringify(request) + '\n');
99
+
100
+ // Timeout after 10 seconds
101
+ setTimeout(() => {
102
+ if (this.responses.has(id)) {
103
+ this.responses.delete(id);
104
+ reject(new Error('Request timeout'));
105
+ }
106
+ }, 10000);
107
+ });
108
+ }
109
+
110
+ async callTool(toolName, args) {
111
+ return this.sendRequest('tools/call', {
112
+ name: toolName,
113
+ arguments: args
114
+ });
115
+ }
116
+
117
+ async listTools() {
118
+ return this.sendRequest('tools/list');
119
+ }
120
+
121
+ stop() {
122
+ if (this.process) {
123
+ this.process.kill();
124
+ }
125
+ }
126
+ }
127
+
128
+ async function runMCPTests() {
129
+ const client = new MCPClient();
130
+
131
+ try {
132
+ // Initialize connection
133
+ console.log('📡 Initializing MCP connection...');
134
+ const initResult = await client.start();
135
+ console.log('✅ Connected to MCP server');
136
+ console.log(` Protocol version: ${initResult.protocolVersion}`);
137
+ console.log(` Server: ${initResult.serverInfo?.name || 'Unknown'} v${initResult.serverInfo?.version || 'Unknown'}\n`);
138
+
139
+ // Test 1: List available tools
140
+ console.log('📋 Test 1: List available tools');
141
+ const toolsResult = await client.listTools();
142
+ console.log(`Found ${toolsResult.tools.length} tools:`);
143
+ for (const tool of toolsResult.tools) {
144
+ console.log(` - ${tool.name}: ${tool.description}`);
145
+ }
146
+ console.log();
147
+
148
+ // Test 2: List ducks
149
+ console.log('🦆 Test 2: List ducks via MCP');
150
+ const ducksResult = await client.callTool('list_ducks', {
151
+ check_health: false
152
+ });
153
+ console.log('Response received (truncated):',
154
+ ducksResult.content[0].text.substring(0, 200) + '...\n');
155
+
156
+ // Test 3: Ask a duck
157
+ console.log('💬 Test 3: Ask OpenAI via MCP');
158
+ const askResult = await client.callTool('ask_duck', {
159
+ prompt: 'What is MCP? Answer in one sentence.',
160
+ provider: 'openai'
161
+ });
162
+ console.log('Response:', askResult.content[0].text.split('\n')[0] + '\n');
163
+
164
+ // Test 4: List models
165
+ console.log('📚 Test 4: List models via MCP');
166
+ const modelsResult = await client.callTool('list_models', {
167
+ provider: 'openai'
168
+ });
169
+ const modelLines = modelsResult.content[0].text.split('\n').slice(0, 10);
170
+ console.log('First few models:', modelLines.join('\n') + '...\n');
171
+
172
+ // Test 5: Compare ducks
173
+ console.log('🔍 Test 5: Compare ducks via MCP');
174
+ const compareResult = await client.callTool('compare_ducks', {
175
+ prompt: 'Is water wet? Answer yes or no.'
176
+ });
177
+ console.log('Comparison started (truncated):',
178
+ compareResult.content[0].text.substring(0, 300) + '...\n');
179
+
180
+ // Test 6: Error handling
181
+ console.log('❌ Test 6: Error handling');
182
+ try {
183
+ await client.callTool('nonexistent_tool', {});
184
+ console.log('ERROR: Should have thrown an error!');
185
+ } catch (error) {
186
+ console.log('✅ Correctly handled invalid tool error:', error.message);
187
+ }
188
+ console.log();
189
+
190
+ // Test 7: Chat with context
191
+ console.log('💬 Test 7: Stateful conversation via MCP');
192
+ await client.callTool('chat_with_duck', {
193
+ conversation_id: 'mcp-test',
194
+ message: 'Remember this number: 42',
195
+ provider: 'openai'
196
+ });
197
+
198
+ const chatResult = await client.callTool('chat_with_duck', {
199
+ conversation_id: 'mcp-test',
200
+ message: 'What number did I ask you to remember?'
201
+ });
202
+ console.log('Context test:', chatResult.content[0].text.split('\n')[0] + '\n');
203
+
204
+ console.log('✅ All MCP interface tests completed successfully!');
205
+
206
+ } catch (error) {
207
+ console.error('❌ Test failed:', error.message);
208
+ } finally {
209
+ client.stop();
210
+ process.exit(0);
211
+ }
212
+ }
213
+
214
+ // Add timeout to prevent hanging
215
+ setTimeout(() => {
216
+ console.error('Test timeout - forcefully exiting');
217
+ process.exit(1);
218
+ }, 30000);
219
+
220
+ // Run the tests
221
+ runMCPTests().catch(console.error);
@@ -0,0 +1,36 @@
1
+ import { describe, it, expect } from '@jest/globals';
2
+ import { formatDuckResponse, getRandomDuckMessage, duckMessages } from '../src/utils/ascii-art';
3
+
4
+ describe('formatDuckResponse', () => {
5
+ it('should format response without model', () => {
6
+ const result = formatDuckResponse('GPT Duck', 'Hello world');
7
+ expect(result).toBe('🦆 [GPT Duck]: Hello world');
8
+ });
9
+
10
+ it('should format response with model', () => {
11
+ const result = formatDuckResponse('GPT Duck', 'Hello world', 'gpt-4o-mini');
12
+ expect(result).toBe('🦆 [GPT Duck | gpt-4o-mini]: Hello world');
13
+ });
14
+
15
+ it('should handle undefined model', () => {
16
+ const result = formatDuckResponse('GPT Duck', 'Hello world', undefined);
17
+ expect(result).toBe('🦆 [GPT Duck]: Hello world');
18
+ });
19
+ });
20
+
21
+ describe('getRandomDuckMessage', () => {
22
+ it('should return a message from startup messages', () => {
23
+ const result = getRandomDuckMessage('startup');
24
+ expect(duckMessages.startup).toContain(result);
25
+ });
26
+
27
+ it('should return a message from error messages', () => {
28
+ const result = getRandomDuckMessage('error');
29
+ expect(duckMessages.error).toContain(result);
30
+ });
31
+
32
+ it('should return a message from success messages', () => {
33
+ const result = getRandomDuckMessage('success');
34
+ expect(duckMessages.success).toContain(result);
35
+ });
36
+ });
@@ -0,0 +1,239 @@
1
+ import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals';
2
+ import { ConfigManager } from '../src/config/config';
3
+
4
+ // Mock logger to avoid console noise during tests
5
+ jest.mock('../src/utils/logger');
6
+
7
+ describe('ConfigManager - Custom Providers', () => {
8
+ let originalEnv: NodeJS.ProcessEnv;
9
+
10
+ beforeEach(() => {
11
+ // Save original environment
12
+ originalEnv = { ...process.env };
13
+
14
+ // Clear custom provider environment variables
15
+ Object.keys(process.env).forEach(key => {
16
+ if (key.startsWith('CUSTOM_')) {
17
+ delete process.env[key];
18
+ }
19
+ });
20
+
21
+ // Clear other provider keys that might interfere
22
+ delete process.env.OPENAI_API_KEY;
23
+ delete process.env.GEMINI_API_KEY;
24
+ delete process.env.GROQ_API_KEY;
25
+ delete process.env.TOGETHER_API_KEY;
26
+ delete process.env.PERPLEXITY_API_KEY;
27
+ });
28
+
29
+ afterEach(() => {
30
+ // Restore original environment
31
+ process.env = originalEnv;
32
+ });
33
+
34
+ describe('getCustomProvidersFromEnv', () => {
35
+ it('should parse single custom provider from environment', () => {
36
+ // Set up environment variables
37
+ process.env.OPENAI_API_KEY = 'dummy-key'; // Ensure getDefaultProviders is called
38
+ process.env.CUSTOM_MYAPI_API_KEY = 'test-key-123';
39
+ process.env.CUSTOM_MYAPI_BASE_URL = 'https://my-api.com/v1';
40
+ process.env.CUSTOM_MYAPI_MODELS = 'model1,model2,model3';
41
+ process.env.CUSTOM_MYAPI_DEFAULT_MODEL = 'model1';
42
+ process.env.CUSTOM_MYAPI_NICKNAME = 'My Custom Duck';
43
+
44
+ const configManager = new ConfigManager();
45
+ const providers = configManager.getAllProviders();
46
+
47
+ expect(providers.myapi).toBeDefined();
48
+ expect(providers.myapi).toEqual({
49
+ api_key: 'test-key-123',
50
+ base_url: 'https://my-api.com/v1',
51
+ models: ['model1', 'model2', 'model3'],
52
+ default_model: 'model1',
53
+ nickname: 'My Custom Duck',
54
+ });
55
+ });
56
+
57
+ it('should parse multiple custom providers from environment', () => {
58
+ // Set up multiple providers
59
+ process.env.OPENAI_API_KEY = 'dummy-key'; // Ensure getDefaultProviders is called
60
+ process.env.CUSTOM_API1_API_KEY = 'key1';
61
+ process.env.CUSTOM_API1_BASE_URL = 'https://api1.com/v1';
62
+ process.env.CUSTOM_API1_NICKNAME = 'API 1 Duck';
63
+
64
+ process.env.CUSTOM_API2_API_KEY = 'key2';
65
+ process.env.CUSTOM_API2_BASE_URL = 'https://api2.com/v1';
66
+ process.env.CUSTOM_API2_MODELS = 'model-a,model-b';
67
+ process.env.CUSTOM_API2_DEFAULT_MODEL = 'model-a';
68
+
69
+ const configManager = new ConfigManager();
70
+ const providers = configManager.getAllProviders();
71
+
72
+ // Check first provider
73
+ expect(providers.api1).toBeDefined();
74
+ expect(providers.api1.api_key).toBe('key1');
75
+ expect(providers.api1.base_url).toBe('https://api1.com/v1');
76
+ expect(providers.api1.nickname).toBe('API 1 Duck');
77
+ expect(providers.api1.models).toEqual(['custom-model']); // default
78
+ expect(providers.api1.default_model).toBe('custom-model'); // default
79
+
80
+ // Check second provider
81
+ expect(providers.api2).toBeDefined();
82
+ expect(providers.api2.api_key).toBe('key2');
83
+ expect(providers.api2.base_url).toBe('https://api2.com/v1');
84
+ expect(providers.api2.models).toEqual(['model-a', 'model-b']);
85
+ expect(providers.api2.default_model).toBe('model-a');
86
+ expect(providers.api2.nickname).toBe('API2 Duck'); // auto-generated
87
+ });
88
+
89
+ it('should convert provider names to lowercase', () => {
90
+ process.env.OPENAI_API_KEY = 'dummy-key'; // Ensure getDefaultProviders is called
91
+ process.env.CUSTOM_MYUPPERAPI_API_KEY = 'test-key';
92
+ process.env.CUSTOM_MYUPPERAPI_BASE_URL = 'https://upper.com/v1';
93
+
94
+ const configManager = new ConfigManager();
95
+ const providers = configManager.getAllProviders();
96
+
97
+ expect(providers.myupperapi).toBeDefined();
98
+ expect(providers.MYUPPERAPI).toBeUndefined();
99
+ expect(providers.myupperapi.nickname).toBe('MYUPPERAPI Duck');
100
+ });
101
+
102
+ it('should require both API_KEY and BASE_URL', () => {
103
+ // Only API_KEY, missing BASE_URL
104
+ process.env.OPENAI_API_KEY = 'dummy-key'; // Ensure getDefaultProviders is called
105
+ process.env.CUSTOM_INCOMPLETE1_API_KEY = 'test-key';
106
+
107
+ // Only BASE_URL, missing API_KEY
108
+ process.env.CUSTOM_INCOMPLETE2_BASE_URL = 'https://test.com/v1';
109
+
110
+ const configManager = new ConfigManager();
111
+ const providers = configManager.getAllProviders();
112
+
113
+ // Neither should be created
114
+ expect(providers.incomplete1).toBeUndefined();
115
+ expect(providers.incomplete2).toBeUndefined();
116
+ });
117
+
118
+ it('should handle comma-separated models list', () => {
119
+ process.env.OPENAI_API_KEY = 'dummy-key'; // Ensure getDefaultProviders is called
120
+ process.env.CUSTOM_TESTMODELS_API_KEY = 'test-key';
121
+ process.env.CUSTOM_TESTMODELS_BASE_URL = 'https://test.com/v1';
122
+ process.env.CUSTOM_TESTMODELS_MODELS = ' model1 , model2 , model3 ';
123
+
124
+ const configManager = new ConfigManager();
125
+ const providers = configManager.getAllProviders();
126
+
127
+ expect(providers.testmodels.models).toEqual(['model1', 'model2', 'model3']);
128
+ });
129
+
130
+ it('should use default values for optional fields', () => {
131
+ process.env.OPENAI_API_KEY = 'dummy-key'; // Ensure getDefaultProviders is called
132
+ process.env.CUSTOM_MINIMAL_API_KEY = 'test-key';
133
+ process.env.CUSTOM_MINIMAL_BASE_URL = 'https://minimal.com/v1';
134
+
135
+ const configManager = new ConfigManager();
136
+ const providers = configManager.getAllProviders();
137
+
138
+ expect(providers.minimal).toBeDefined();
139
+ expect(providers.minimal).toEqual({
140
+ api_key: 'test-key',
141
+ base_url: 'https://minimal.com/v1',
142
+ models: ['custom-model'], // default
143
+ default_model: 'custom-model', // default
144
+ nickname: 'MINIMAL Duck', // auto-generated
145
+ });
146
+ });
147
+
148
+ it('should handle custom nicknames', () => {
149
+ process.env.OPENAI_API_KEY = 'dummy-key'; // Ensure getDefaultProviders is called
150
+ process.env.CUSTOM_NICKNAMED_API_KEY = 'test-key';
151
+ process.env.CUSTOM_NICKNAMED_BASE_URL = 'https://test.com/v1';
152
+ process.env.CUSTOM_NICKNAMED_NICKNAME = 'My Special Test Duck 🦆';
153
+
154
+ const configManager = new ConfigManager();
155
+ const providers = configManager.getAllProviders();
156
+
157
+ expect(providers.nicknamed.nickname).toBe('My Special Test Duck 🦆');
158
+ });
159
+
160
+ it('should handle empty models string', () => {
161
+ process.env.OPENAI_API_KEY = 'dummy-key'; // Ensure getDefaultProviders is called
162
+ process.env.CUSTOM_EMPTYMODELS_API_KEY = 'test-key';
163
+ process.env.CUSTOM_EMPTYMODELS_BASE_URL = 'https://test.com/v1';
164
+ process.env.CUSTOM_EMPTYMODELS_MODELS = '';
165
+
166
+ const configManager = new ConfigManager();
167
+ const providers = configManager.getAllProviders();
168
+
169
+ // Empty models should fall back to default
170
+ expect(providers.emptymodels.models).toEqual(['custom-model']);
171
+ });
172
+
173
+ it('should integrate with existing providers without conflicts', () => {
174
+ // Set up API keys for built-in providers
175
+ process.env.OPENAI_API_KEY = 'openai-key';
176
+ process.env.GEMINI_API_KEY = 'gemini-key';
177
+
178
+ // Add custom provider
179
+ process.env.CUSTOM_INTEGRATION_API_KEY = 'custom-key';
180
+ process.env.CUSTOM_INTEGRATION_BASE_URL = 'https://custom.com/v1';
181
+
182
+ const configManager = new ConfigManager();
183
+ const providers = configManager.getAllProviders();
184
+
185
+ // Should have built-in providers
186
+ expect(providers.openai).toBeDefined();
187
+ expect(providers.gemini).toBeDefined();
188
+
189
+ // Should have custom provider
190
+ expect(providers.integration).toBeDefined();
191
+
192
+ // Custom provider shouldn't override built-ins
193
+ expect(providers.openai.base_url).toBe('https://api.openai.com/v1');
194
+ expect(providers.integration.base_url).toBe('https://custom.com/v1');
195
+ });
196
+
197
+ it('should handle local LLM configuration via custom providers', () => {
198
+ // Test common local LLM setups
199
+ process.env.OPENAI_API_KEY = 'dummy-key'; // Ensure getDefaultProviders is called
200
+ process.env.CUSTOM_OLLAMA_API_KEY = 'not-needed';
201
+ process.env.CUSTOM_OLLAMA_BASE_URL = 'http://localhost:11434/v1';
202
+ process.env.CUSTOM_OLLAMA_MODELS = 'llama3.2,mistral,codellama';
203
+ process.env.CUSTOM_OLLAMA_NICKNAME = 'Local Ollama Duck';
204
+
205
+ process.env.CUSTOM_LMSTUDIO_API_KEY = 'not-needed';
206
+ process.env.CUSTOM_LMSTUDIO_BASE_URL = 'http://localhost:1234/v1';
207
+ process.env.CUSTOM_LMSTUDIO_NICKNAME = 'LM Studio Duck';
208
+
209
+ const configManager = new ConfigManager();
210
+ const providers = configManager.getAllProviders();
211
+
212
+ expect(providers.ollama).toBeDefined();
213
+ expect(providers.ollama.api_key).toBe('not-needed');
214
+ expect(providers.ollama.base_url).toBe('http://localhost:11434/v1');
215
+ expect(providers.ollama.models).toEqual(['llama3.2', 'mistral', 'codellama']);
216
+
217
+ expect(providers.lmstudio).toBeDefined();
218
+ expect(providers.lmstudio.api_key).toBe('not-needed');
219
+ expect(providers.lmstudio.base_url).toBe('http://localhost:1234/v1');
220
+ });
221
+ });
222
+
223
+ describe('Integration with ProviderManager', () => {
224
+ it('should allow custom providers to be used by ProviderManager', () => {
225
+ process.env.OPENAI_API_KEY = 'dummy-key'; // Ensure getDefaultProviders is called
226
+ process.env.CUSTOM_TESTPROVIDER_API_KEY = 'test-key';
227
+ process.env.CUSTOM_TESTPROVIDER_BASE_URL = 'https://test.com/v1';
228
+ process.env.CUSTOM_TESTPROVIDER_NICKNAME = 'Test Provider Duck';
229
+
230
+ const configManager = new ConfigManager();
231
+ const config = configManager.getConfig();
232
+
233
+ expect(config.providers.testprovider).toBeDefined();
234
+ expect(config.providers.testprovider.nickname).toBe('Test Provider Duck');
235
+ expect(config.providers.testprovider.api_key).toBe('test-key');
236
+ expect(config.providers.testprovider.base_url).toBe('https://test.com/v1');
237
+ });
238
+ });
239
+ });