ideaco 1.1.5

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 (159) hide show
  1. package/.dockerignore +33 -0
  2. package/.nvmrc +1 -0
  3. package/ARCHITECTURE.md +394 -0
  4. package/Dockerfile +50 -0
  5. package/LICENSE +29 -0
  6. package/README.md +206 -0
  7. package/bin/i18n.js +46 -0
  8. package/bin/ideaco.js +494 -0
  9. package/deploy.sh +15 -0
  10. package/docker-compose.yml +30 -0
  11. package/electron/main.cjs +986 -0
  12. package/electron/preload.cjs +14 -0
  13. package/electron/web-backends.cjs +854 -0
  14. package/jsconfig.json +8 -0
  15. package/next.config.mjs +34 -0
  16. package/package.json +134 -0
  17. package/postcss.config.mjs +6 -0
  18. package/public/demo/dashboard.png +0 -0
  19. package/public/demo/employee.png +0 -0
  20. package/public/demo/messages.png +0 -0
  21. package/public/demo/office.png +0 -0
  22. package/public/demo/requirement.png +0 -0
  23. package/public/logo.jpeg +0 -0
  24. package/public/logo.png +0 -0
  25. package/scripts/prepare-electron.js +67 -0
  26. package/scripts/release.js +76 -0
  27. package/src/app/api/agents/[agentId]/chat/route.js +70 -0
  28. package/src/app/api/agents/[agentId]/conversations/route.js +35 -0
  29. package/src/app/api/agents/[agentId]/route.js +106 -0
  30. package/src/app/api/avatar/route.js +104 -0
  31. package/src/app/api/browse-dir/route.js +44 -0
  32. package/src/app/api/chat/route.js +265 -0
  33. package/src/app/api/company/factory-reset/route.js +43 -0
  34. package/src/app/api/company/route.js +82 -0
  35. package/src/app/api/departments/[deptId]/agents/[agentId]/dismiss/route.js +19 -0
  36. package/src/app/api/departments/route.js +92 -0
  37. package/src/app/api/group-chat-loop/events/route.js +70 -0
  38. package/src/app/api/group-chat-loop/route.js +94 -0
  39. package/src/app/api/mailbox/route.js +100 -0
  40. package/src/app/api/messages/route.js +14 -0
  41. package/src/app/api/providers/[id]/configure/route.js +21 -0
  42. package/src/app/api/providers/[id]/refresh-cookie/route.js +38 -0
  43. package/src/app/api/providers/[id]/test-cookie/route.js +28 -0
  44. package/src/app/api/providers/route.js +11 -0
  45. package/src/app/api/requirements/route.js +242 -0
  46. package/src/app/api/secretary/route.js +65 -0
  47. package/src/app/api/system/cli-backends/route.js +91 -0
  48. package/src/app/api/system/cron/route.js +110 -0
  49. package/src/app/api/system/knowledge/route.js +104 -0
  50. package/src/app/api/system/plugins/route.js +40 -0
  51. package/src/app/api/system/skills/route.js +46 -0
  52. package/src/app/api/system/status/route.js +46 -0
  53. package/src/app/api/talent-market/[profileId]/recall/route.js +22 -0
  54. package/src/app/api/talent-market/[profileId]/route.js +17 -0
  55. package/src/app/api/talent-market/route.js +26 -0
  56. package/src/app/api/teams/route.js +773 -0
  57. package/src/app/api/ws-files/[departmentId]/file/route.js +27 -0
  58. package/src/app/api/ws-files/[departmentId]/files/route.js +22 -0
  59. package/src/app/globals.css +130 -0
  60. package/src/app/layout.jsx +40 -0
  61. package/src/app/page.jsx +97 -0
  62. package/src/components/AgentChatModal.jsx +164 -0
  63. package/src/components/AgentDetailModal.jsx +425 -0
  64. package/src/components/AgentSpyModal.jsx +481 -0
  65. package/src/components/AvatarGrid.jsx +29 -0
  66. package/src/components/BossProfileModal.jsx +162 -0
  67. package/src/components/CachedAvatar.jsx +77 -0
  68. package/src/components/ChatPanel.jsx +219 -0
  69. package/src/components/ChatShared.jsx +255 -0
  70. package/src/components/DepartmentDetail.jsx +842 -0
  71. package/src/components/DepartmentView.jsx +367 -0
  72. package/src/components/FileReference.jsx +260 -0
  73. package/src/components/FilesView.jsx +465 -0
  74. package/src/components/GroupChatView.jsx +799 -0
  75. package/src/components/Mailbox.jsx +926 -0
  76. package/src/components/MessagesView.jsx +112 -0
  77. package/src/components/OnboardingGuide.jsx +209 -0
  78. package/src/components/OrgTree.jsx +151 -0
  79. package/src/components/Overview.jsx +391 -0
  80. package/src/components/PixelOffice.jsx +2281 -0
  81. package/src/components/ProviderGrid.jsx +551 -0
  82. package/src/components/ProvidersBoard.jsx +16 -0
  83. package/src/components/RequirementDetail.jsx +1279 -0
  84. package/src/components/RequirementsBoard.jsx +187 -0
  85. package/src/components/SecretarySettings.jsx +295 -0
  86. package/src/components/SetupWizard.jsx +388 -0
  87. package/src/components/Sidebar.jsx +169 -0
  88. package/src/components/SystemMonitor.jsx +808 -0
  89. package/src/components/TalentMarket.jsx +183 -0
  90. package/src/components/TeamDetail.jsx +697 -0
  91. package/src/core/agent/base-agent.js +104 -0
  92. package/src/core/agent/chat-store.js +602 -0
  93. package/src/core/agent/cli-agent/backends/claude-code/README.md +52 -0
  94. package/src/core/agent/cli-agent/backends/claude-code/config.js +27 -0
  95. package/src/core/agent/cli-agent/backends/codebuddy/README.md +236 -0
  96. package/src/core/agent/cli-agent/backends/codebuddy/config.js +27 -0
  97. package/src/core/agent/cli-agent/backends/codex/README.md +51 -0
  98. package/src/core/agent/cli-agent/backends/codex/config.js +27 -0
  99. package/src/core/agent/cli-agent/backends/index.js +27 -0
  100. package/src/core/agent/cli-agent/backends/registry.js +580 -0
  101. package/src/core/agent/cli-agent/index.js +154 -0
  102. package/src/core/agent/index.js +60 -0
  103. package/src/core/agent/llm-agent/client.js +320 -0
  104. package/src/core/agent/llm-agent/index.js +97 -0
  105. package/src/core/agent/message-bus.js +211 -0
  106. package/src/core/agent/session.js +608 -0
  107. package/src/core/agent/tools.js +596 -0
  108. package/src/core/agent/web-agent/backends/base-backend.js +180 -0
  109. package/src/core/agent/web-agent/backends/chatgpt/client.js +146 -0
  110. package/src/core/agent/web-agent/backends/chatgpt/config.js +148 -0
  111. package/src/core/agent/web-agent/backends/chatgpt/dom-scripts.js +303 -0
  112. package/src/core/agent/web-agent/backends/index.js +91 -0
  113. package/src/core/agent/web-agent/index.js +278 -0
  114. package/src/core/agent/web-agent/web-client.js +407 -0
  115. package/src/core/employee/base-employee.js +1088 -0
  116. package/src/core/employee/index.js +35 -0
  117. package/src/core/employee/knowledge.js +327 -0
  118. package/src/core/employee/lifecycle.js +990 -0
  119. package/src/core/employee/memory/index.js +642 -0
  120. package/src/core/employee/memory/store.js +143 -0
  121. package/src/core/employee/performance.js +224 -0
  122. package/src/core/employee/secretary.js +625 -0
  123. package/src/core/employee/skills.js +398 -0
  124. package/src/core/index.js +38 -0
  125. package/src/core/organization/company.js +2600 -0
  126. package/src/core/organization/department.js +737 -0
  127. package/src/core/organization/group-chat-loop.js +264 -0
  128. package/src/core/organization/index.js +8 -0
  129. package/src/core/organization/persistence.js +111 -0
  130. package/src/core/organization/team.js +267 -0
  131. package/src/core/organization/workforce/hr.js +377 -0
  132. package/src/core/organization/workforce/providers.js +468 -0
  133. package/src/core/organization/workforce/role-archetypes.js +805 -0
  134. package/src/core/organization/workforce/talent-market.js +205 -0
  135. package/src/core/prompts.js +532 -0
  136. package/src/core/requirement.js +1789 -0
  137. package/src/core/system/audit.js +483 -0
  138. package/src/core/system/cron.js +449 -0
  139. package/src/core/system/index.js +7 -0
  140. package/src/core/system/plugin.js +2183 -0
  141. package/src/core/utils/json-parse.js +188 -0
  142. package/src/core/workspace.js +239 -0
  143. package/src/lib/api-i18n.js +211 -0
  144. package/src/lib/avatar.js +268 -0
  145. package/src/lib/client-store.js +1025 -0
  146. package/src/lib/config-validator.js +483 -0
  147. package/src/lib/format-time.js +22 -0
  148. package/src/lib/hooks.js +414 -0
  149. package/src/lib/i18n.js +134 -0
  150. package/src/lib/paths.js +23 -0
  151. package/src/lib/store.js +72 -0
  152. package/src/locales/de.js +393 -0
  153. package/src/locales/en.js +1054 -0
  154. package/src/locales/es.js +393 -0
  155. package/src/locales/fr.js +393 -0
  156. package/src/locales/ja.js +501 -0
  157. package/src/locales/ko.js +513 -0
  158. package/src/locales/zh.js +828 -0
  159. package/tailwind.config.mjs +11 -0
@@ -0,0 +1,468 @@
1
+ /**
2
+ * Model Provider Registry - Outsourcing vendor marketplace
3
+ * Different job categories map to different model providers
4
+ * Providers need API Key configuration before enabling recruitment
5
+ */
6
+
7
+ // Job category enum
8
+ export const JobCategory = {
9
+ GENERAL: 'general', // General positions (text processing, analysis, coding, etc.)
10
+ DRAWING: 'drawing', // Drawing/illustration positions
11
+ MUSIC: 'music', // Music positions
12
+ VIDEO: 'video', // Video positions
13
+ CLI: 'cli', // CLI coding assistant positions (local CLI tools)
14
+ BROWSER: 'browser', // Browser DOM-based web chat (no API key needed)
15
+ };
16
+
17
+ // Job category label mapping (i18n keys for frontend)
18
+ export const JobCategoryLabel = {
19
+ [JobCategory.GENERAL]: 'general',
20
+ [JobCategory.DRAWING]: 'drawing',
21
+ [JobCategory.MUSIC]: 'music',
22
+ [JobCategory.VIDEO]: 'video',
23
+ [JobCategory.CLI]: 'cli',
24
+ [JobCategory.BROWSER]: 'browser',
25
+ };
26
+
27
+ // Model Providers (outsourcing vendor marketplace)
28
+ export const ModelProviders = {
29
+ // === General Position Providers ===
30
+ OPENAI_GPT4: {
31
+ id: 'openai-gpt4',
32
+ name: 'OpenAI GPT-4',
33
+ provider: 'OpenAI',
34
+ model: 'gpt-4-turbo',
35
+ category: JobCategory.GENERAL,
36
+ capabilities: ['text-generation', 'coding', 'data-analysis', 'reasoning', 'translation'],
37
+ costPerToken: 0.03,
38
+ priceLabel: '$0.03/1K tokens',
39
+ priceLevel: 3, // 1=cheap 2=moderate 3=expensive
40
+ rating: 95, // Overall score 0-100
41
+ description: 'Most powerful general model, ideal for complex reasoning and coding tasks',
42
+ apiKey: '',
43
+ enabled: false,
44
+ },
45
+ OPENAI_GPT35: {
46
+ id: 'openai-gpt35',
47
+ name: 'OpenAI GPT-3.5',
48
+ provider: 'OpenAI',
49
+ model: 'gpt-3.5-turbo',
50
+ category: JobCategory.GENERAL,
51
+ capabilities: ['text-generation', 'simple-coding', 'translation', 'summarization'],
52
+ costPerToken: 0.002,
53
+ priceLabel: '$0.002/1K tokens',
54
+ priceLevel: 1,
55
+ rating: 72,
56
+ description: 'Cost-effective general model for simple tasks',
57
+ apiKey: '',
58
+ enabled: false,
59
+ },
60
+ ANTHROPIC_CLAUDE: {
61
+ id: 'anthropic-claude',
62
+ name: 'Anthropic Claude 3.5',
63
+ provider: 'Anthropic',
64
+ model: 'claude-3.5-sonnet',
65
+ category: JobCategory.GENERAL,
66
+ capabilities: ['text-generation', 'coding', 'data-analysis', 'long-context', 'reasoning'],
67
+ costPerToken: 0.015,
68
+ priceLabel: '$0.015/1K tokens',
69
+ priceLevel: 2,
70
+ rating: 93,
71
+ description: 'Excels at long-context understanding and precise analysis',
72
+ apiKey: '',
73
+ enabled: false,
74
+ },
75
+ DEEPSEEK: {
76
+ id: 'deepseek-v3',
77
+ name: 'DeepSeek V3',
78
+ provider: 'DeepSeek',
79
+ model: 'deepseek-chat',
80
+ category: JobCategory.GENERAL,
81
+ capabilities: ['text-generation', 'coding', 'math-reasoning', 'data-analysis'],
82
+ costPerToken: 0.001,
83
+ priceLabel: '$0.001/1K tokens',
84
+ priceLevel: 1,
85
+ rating: 88,
86
+ description: 'High cost-performance model with outstanding coding capability',
87
+ apiKey: '',
88
+ enabled: false,
89
+ },
90
+ QWEN: {
91
+ id: 'qwen-max',
92
+ name: 'Qwen Max',
93
+ provider: 'Alibaba Cloud',
94
+ model: 'qwen-max',
95
+ category: JobCategory.GENERAL,
96
+ capabilities: ['text-generation', 'coding', 'data-analysis', 'reasoning', 'translation'],
97
+ costPerToken: 0.004,
98
+ priceLabel: '$0.004/1K tokens',
99
+ priceLevel: 1,
100
+ rating: 86,
101
+ description: 'Alibaba Cloud flagship model with excellent multilingual support',
102
+ apiKey: '',
103
+ enabled: false,
104
+ },
105
+
106
+ // === Drawing Position Providers ===
107
+ DALLE3: {
108
+ id: 'openai-dalle3',
109
+ name: 'DALL·E 3',
110
+ provider: 'OpenAI',
111
+ model: 'dall-e-3',
112
+ category: JobCategory.DRAWING,
113
+ capabilities: ['text-to-image', 'illustration', 'ui-design', 'concept-art'],
114
+ costPerImage: 0.04,
115
+ priceLabel: '$0.04/image',
116
+ priceLevel: 2,
117
+ rating: 90,
118
+ description: 'High-quality text-to-image model',
119
+ apiKey: '',
120
+ enabled: false,
121
+ },
122
+ MIDJOURNEY: {
123
+ id: 'midjourney-v6',
124
+ name: 'Midjourney V6',
125
+ provider: 'Midjourney',
126
+ model: 'midjourney-v6',
127
+ category: JobCategory.DRAWING,
128
+ capabilities: ['art-creation', 'concept-art', 'stylized-illustration', 'photography-style'],
129
+ costPerImage: 0.05,
130
+ priceLabel: '$0.05/image',
131
+ priceLevel: 3,
132
+ rating: 96,
133
+ description: 'Top choice for art creation with powerful stylization',
134
+ apiKey: '',
135
+ enabled: false,
136
+ },
137
+ STABLE_DIFFUSION: {
138
+ id: 'stability-sdxl',
139
+ name: 'Stable Diffusion XL',
140
+ provider: 'Stability AI',
141
+ model: 'sdxl-1.0',
142
+ category: JobCategory.DRAWING,
143
+ capabilities: ['text-to-image', 'image-to-image', 'style-transfer', 'fine-control'],
144
+ costPerImage: 0.02,
145
+ priceLabel: '$0.02/image',
146
+ priceLevel: 1,
147
+ rating: 82,
148
+ description: 'Open-source controllable image generation model',
149
+ apiKey: '',
150
+ enabled: false,
151
+ },
152
+
153
+ // === Music Position Providers ===
154
+ SUNO: {
155
+ id: 'suno-v3',
156
+ name: 'Suno V3.5',
157
+ provider: 'Suno',
158
+ model: 'suno-v3.5',
159
+ category: JobCategory.MUSIC,
160
+ capabilities: ['songwriting', 'scoring', 'lyrics', 'multi-genre'],
161
+ costPerTrack: 0.1,
162
+ priceLabel: '$0.10/track',
163
+ priceLevel: 2,
164
+ rating: 91,
165
+ description: 'All-in-one AI music creation',
166
+ apiKey: '',
167
+ enabled: false,
168
+ },
169
+ UDIO: {
170
+ id: 'udio-v1',
171
+ name: 'Udio',
172
+ provider: 'Udio',
173
+ model: 'udio-v1.5',
174
+ category: JobCategory.MUSIC,
175
+ capabilities: ['music-generation', 'vocal-synthesis', 'arrangement', 'mixing'],
176
+ costPerTrack: 0.08,
177
+ priceLabel: '$0.08/track',
178
+ priceLevel: 1,
179
+ rating: 85,
180
+ description: 'High-fidelity music generation',
181
+ apiKey: '',
182
+ enabled: false,
183
+ },
184
+
185
+ // === Video Position Providers ===
186
+ RUNWAY: {
187
+ id: 'runway-gen3',
188
+ name: 'Runway Gen-3',
189
+ provider: 'Runway',
190
+ model: 'gen-3-alpha',
191
+ category: JobCategory.VIDEO,
192
+ capabilities: ['text-to-video', 'image-to-video', 'video-editing', 'vfx'],
193
+ costPerSecond: 0.5,
194
+ priceLabel: '$0.50/sec',
195
+ priceLevel: 3,
196
+ rating: 92,
197
+ description: 'Professional-grade AI video generation',
198
+ apiKey: '',
199
+ enabled: false,
200
+ },
201
+ PIKA: {
202
+ id: 'pika-v2',
203
+ name: 'Pika 2.0',
204
+ provider: 'Pika',
205
+ model: 'pika-2.0',
206
+ category: JobCategory.VIDEO,
207
+ capabilities: ['short-video', 'animation', 'video-effects'],
208
+ costPerSecond: 0.3,
209
+ priceLabel: '$0.30/sec',
210
+ priceLevel: 2,
211
+ rating: 84,
212
+ description: 'Lightweight video generation',
213
+ apiKey: '',
214
+ enabled: false,
215
+ },
216
+ KLING: {
217
+ id: 'kling-v1',
218
+ name: 'Kling AI',
219
+ provider: 'Kuaishou',
220
+ model: 'kling-v1',
221
+ category: JobCategory.VIDEO,
222
+ capabilities: ['text-to-video', 'image-to-video', 'video-continuation'],
223
+ costPerSecond: 0.2,
224
+ priceLabel: '$0.20/sec',
225
+ priceLevel: 1,
226
+ rating: 80,
227
+ description: 'Cost-effective video generation model',
228
+ apiKey: '',
229
+ enabled: false,
230
+ },
231
+
232
+ // === CLI Coding Assistant Providers ===
233
+ // CLI providers are auto-populated from CLI Backend Registry at runtime.
234
+ // They appear in the Brain Providers board as a separate "CLI" category.
235
+ // When enabled, HR can recruit agents that use local CLI tools as execution engines.
236
+
237
+ // === Browser (DOM-based) Providers ===
238
+ // These use Electron's hidden BrowserWindow to interact with web chat UIs via DOM scripting.
239
+ // The user logs in once via browser, then IdeaCo controls the page automatically.
240
+ CHATGPT_WEB: {
241
+ id: 'web-chatgpt-4o',
242
+ name: 'ChatGPT (Browser)',
243
+ provider: 'OpenAI Web',
244
+ model: 'chatgpt-4o-latest',
245
+ webModel: 'auto', // Model identifier used in ChatGPT web API
246
+ category: JobCategory.BROWSER,
247
+ capabilities: ['text-generation', 'coding', 'data-analysis', 'reasoning', 'translation'],
248
+ costPerToken: 0,
249
+ priceLabel: 'Free (browser)',
250
+ priceLevel: 1,
251
+ rating: 95,
252
+ description: 'ChatGPT via browser DOM automation — no API key needed, uses your existing ChatGPT subscription',
253
+ apiKey: '', // Not used — session managed by Electron's Chromium
254
+ cookie: '', // Legacy field, kept for backward compatibility
255
+ enabled: false,
256
+ isWeb: true,
257
+ },
258
+ };
259
+
260
+ /**
261
+ * Provider Registry - Manages all model providers
262
+ */
263
+ export class ProviderRegistry {
264
+ constructor() {
265
+ this.providers = new Map();
266
+ // Register all built-in providers
267
+ Object.values(ModelProviders).forEach(p => this.register({ ...p }));
268
+ }
269
+
270
+ /**
271
+ * Sync CLI backends from CLIBackendRegistry into providers.
272
+ * Called at startup and after CLI detection to keep the two systems in sync.
273
+ * CLI backends become providers under the 'cli' category.
274
+ * @param {import('./agent/cli-agent/backends/registry.js').CLIBackendRegistry} cliRegistry
275
+ */
276
+ syncCLIBackends(cliRegistry) {
277
+ if (!cliRegistry) return;
278
+ const backends = cliRegistry.listAll();
279
+ for (const b of backends) {
280
+ const providerId = `cli-${b.id}`;
281
+ const existing = this.providers.get(providerId);
282
+ // Preserve existing enabled/apiKey state if already registered
283
+ const wasEnabled = existing?.enabled || false;
284
+ this.providers.set(providerId, {
285
+ id: providerId,
286
+ name: b.name,
287
+ provider: 'Local CLI',
288
+ model: b.execCommand,
289
+ category: JobCategory.CLI,
290
+ capabilities: ['coding', 'file-operations', 'shell-execution', 'code-review', 'refactoring'],
291
+ costPerToken: 0,
292
+ priceLabel: 'Free (local)',
293
+ priceLevel: 1,
294
+ rating: b.rating || 80, // Use CLI's own rating (defined in BUILTIN_BACKENDS)
295
+ description: b.description || `${b.name} - local CLI coding assistant`,
296
+ // CLI providers don't need API keys; they use local CLI authentication
297
+ // Detected/configured CLIs are auto-enabled, otherwise keep manual toggle state
298
+ apiKey: (b.state === 'detected' || b.state === 'configured') ? 'cli-local' : '',
299
+ enabled: (b.state === 'detected' || b.state === 'configured') ? true : wasEnabled || false,
300
+ // Extra CLI metadata
301
+ isCLI: true,
302
+ cliBackendId: b.id,
303
+ cliState: b.state,
304
+ cliVersion: b.version,
305
+ cliIcon: b.icon,
306
+ // TODO: hide CodeBuddy from external users in the future
307
+ // hidden: b.id === 'codebuddy',
308
+ });
309
+ }
310
+ }
311
+
312
+ /** Register a new model provider */
313
+ register(providerConfig) {
314
+ this.providers.set(providerConfig.id, providerConfig);
315
+ }
316
+
317
+ /** Get provider by ID */
318
+ getById(id) {
319
+ return this.providers.get(id);
320
+ }
321
+
322
+ /**
323
+ * Configure a provider's API Key
324
+ * @param {string} id - Provider ID
325
+ * @param {string} apiKey - API Key
326
+ * @returns {object} Updated provider config
327
+ */
328
+ configure(id, apiKey) {
329
+ const provider = this.providers.get(id);
330
+ if (!provider) throw new Error(`Provider not found: ${id}`);
331
+ // CLI providers use toggle instead of API key
332
+ if (provider.isCLI) {
333
+ provider.enabled = !!apiKey;
334
+ return provider;
335
+ }
336
+ // Web/browser providers use DOM-based session (cookie field kept for backward compat)
337
+ if (provider.isWeb) {
338
+ provider.cookie = apiKey; // apiKey field carries the cookie string
339
+ provider.enabled = !!apiKey;
340
+ return provider;
341
+ }
342
+ provider.apiKey = apiKey;
343
+ provider.enabled = !!apiKey;
344
+ return provider;
345
+ }
346
+
347
+ /**
348
+ * Manually enable/disable a provider
349
+ */
350
+ setEnabled(id, enabled) {
351
+ const provider = this.providers.get(id);
352
+ if (!provider) throw new Error(`Provider not found: ${id}`);
353
+ // CLI providers can be toggled without API key check
354
+ if (provider.isCLI) {
355
+ provider.enabled = enabled;
356
+ return provider;
357
+ }
358
+ // Web providers use DOM-based browser session (cookie field kept for backward compat)
359
+ if (provider.isWeb) {
360
+ if (enabled && !provider.cookie) {
361
+ throw new Error(`Provider ${provider.name} has no browser session configured, cannot enable`);
362
+ }
363
+ provider.enabled = enabled;
364
+ return provider;
365
+ }
366
+ if (enabled && !provider.apiKey) {
367
+ throw new Error(`Provider ${provider.name} has no API Key configured, cannot enable`);
368
+ }
369
+ provider.enabled = enabled;
370
+ return provider;
371
+ }
372
+
373
+ /** Get available (enabled) providers by job category.
374
+ * Browser providers are also returned when querying 'general' (they share capabilities). */
375
+ getByCategory(category) {
376
+ return [...this.providers.values()].filter(
377
+ p => (p.category === category || (category === 'general' && p.category === 'browser')) && p.enabled
378
+ );
379
+ }
380
+
381
+ /** Get all providers by job category (including disabled) */
382
+ getAllByCategory(category) {
383
+ return [...this.providers.values()].filter(
384
+ p => p.category === category || (category === 'general' && p.category === 'browser')
385
+ );
386
+ }
387
+
388
+ /** Check if a category has any enabled provider */
389
+ hasCategoryEnabled(category) {
390
+ return this.getByCategory(category).length > 0;
391
+ }
392
+
393
+ /**
394
+ * Recommend best provider for a job category (from enabled ones)
395
+ * Strategy: composite score = rating * 0.6 + (100 - priceLevel*20) * 0.4
396
+ * i.e. high-rating + low-price gets priority
397
+ */
398
+ recommend(category, requirements = []) {
399
+ let candidates = this.getByCategory(category);
400
+ if (candidates.length === 0) return null;
401
+
402
+ // If specific capability requirements exist, prefer matches
403
+ if (requirements.length > 0) {
404
+ const matched = candidates.filter(p =>
405
+ requirements.every(req => p.capabilities.includes(req))
406
+ );
407
+ if (matched.length > 0) candidates = matched;
408
+ }
409
+
410
+ // Sort by cost-performance: high rating + low price
411
+ candidates.sort((a, b) => {
412
+ const scoreA = (a.rating || 80) * 0.6 + (100 - (a.priceLevel || 2) * 20) * 0.4;
413
+ const scoreB = (b.rating || 80) * 0.6 + (100 - (b.priceLevel || 2) * 20) * 0.4;
414
+ return scoreB - scoreA;
415
+ });
416
+
417
+ return candidates[0];
418
+ }
419
+
420
+ /** List all providers */
421
+ listAll() {
422
+ return [...this.providers.values()];
423
+ }
424
+
425
+ /** List all enabled providers */
426
+ listEnabled() {
427
+ return [...this.providers.values()].filter(p => p.enabled);
428
+ }
429
+
430
+ /** Get provider statistics (grouped by category, with enabled count) */
431
+ getStats() {
432
+ const stats = {};
433
+ for (const cat of Object.values(JobCategory)) {
434
+ // For stats display, use exact category match (don't merge browser into general)
435
+ const all = [...this.providers.values()].filter(p => p.category === cat);
436
+ // Skip empty CLI category if no CLI backends registered
437
+ if (cat === JobCategory.CLI && all.length === 0) continue;
438
+ const enabled = all.filter(p => p.enabled);
439
+ stats[cat] = {
440
+ label: JobCategoryLabel[cat],
441
+ total: all.length,
442
+ enabled: enabled.length,
443
+ providers: all.map(p => ({
444
+ id: p.id,
445
+ name: p.name,
446
+ provider: p.provider,
447
+ enabled: p.enabled,
448
+ hasKey: !!p.apiKey,
449
+ rating: p.rating || 0,
450
+ priceLabel: p.priceLabel || '',
451
+ priceLevel: p.priceLevel || 2,
452
+ description: p.description || '',
453
+ capabilities: p.capabilities || [],
454
+ // CLI-specific fields
455
+ isCLI: p.isCLI || false,
456
+ cliBackendId: p.cliBackendId || null,
457
+ cliState: p.cliState || null,
458
+ cliVersion: p.cliVersion || null,
459
+ cliIcon: p.cliIcon || null,
460
+ // Web/browser-specific fields
461
+ isWeb: p.isWeb || false,
462
+ hasCookie: !!(p.cookie),
463
+ })),
464
+ };
465
+ }
466
+ return stats;
467
+ }
468
+ }