wogiflow 1.0.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 (221) hide show
  1. package/.workflow/agents/reviewer.md +81 -0
  2. package/.workflow/agents/security.md +94 -0
  3. package/.workflow/agents/story-writer.md +58 -0
  4. package/.workflow/bridges/base-bridge.js +395 -0
  5. package/.workflow/bridges/claude-bridge.js +434 -0
  6. package/.workflow/bridges/index.js +130 -0
  7. package/.workflow/lib/assumption-detector.js +481 -0
  8. package/.workflow/lib/config-substitution.js +371 -0
  9. package/.workflow/lib/failure-categories.js +478 -0
  10. package/.workflow/state/app-map.md.template +15 -0
  11. package/.workflow/state/architecture.md.template +24 -0
  12. package/.workflow/state/component-index.json.template +5 -0
  13. package/.workflow/state/decisions.md.template +15 -0
  14. package/.workflow/state/feedback-patterns.md.template +9 -0
  15. package/.workflow/state/knowledge-sync.json.template +6 -0
  16. package/.workflow/state/progress.md.template +14 -0
  17. package/.workflow/state/ready.json.template +7 -0
  18. package/.workflow/state/request-log.md.template +14 -0
  19. package/.workflow/state/session-state.json.template +11 -0
  20. package/.workflow/state/stack.md.template +33 -0
  21. package/.workflow/state/testing.md.template +36 -0
  22. package/.workflow/templates/claude-md.hbs +257 -0
  23. package/.workflow/templates/correction-report.md +67 -0
  24. package/.workflow/templates/gemini-md.hbs +52 -0
  25. package/README.md +1802 -0
  26. package/bin/flow +205 -0
  27. package/lib/index.js +33 -0
  28. package/lib/installer.js +467 -0
  29. package/lib/release-channel.js +269 -0
  30. package/lib/skill-registry.js +526 -0
  31. package/lib/upgrader.js +401 -0
  32. package/lib/utils.js +305 -0
  33. package/package.json +64 -0
  34. package/scripts/flow +985 -0
  35. package/scripts/flow-adaptive-learning.js +1259 -0
  36. package/scripts/flow-aggregate.js +488 -0
  37. package/scripts/flow-archive +133 -0
  38. package/scripts/flow-auto-context.js +1015 -0
  39. package/scripts/flow-auto-learn.js +615 -0
  40. package/scripts/flow-bridge.js +223 -0
  41. package/scripts/flow-browser-suggest.js +316 -0
  42. package/scripts/flow-bug.js +247 -0
  43. package/scripts/flow-cascade.js +711 -0
  44. package/scripts/flow-changelog +85 -0
  45. package/scripts/flow-checkpoint.js +483 -0
  46. package/scripts/flow-cli.js +403 -0
  47. package/scripts/flow-code-intelligence.js +760 -0
  48. package/scripts/flow-complexity.js +502 -0
  49. package/scripts/flow-config-set.js +152 -0
  50. package/scripts/flow-constants.js +157 -0
  51. package/scripts/flow-context +152 -0
  52. package/scripts/flow-context-init.js +482 -0
  53. package/scripts/flow-context-monitor.js +384 -0
  54. package/scripts/flow-context-scoring.js +886 -0
  55. package/scripts/flow-correct.js +458 -0
  56. package/scripts/flow-damage-control.js +985 -0
  57. package/scripts/flow-deps +101 -0
  58. package/scripts/flow-diff.js +700 -0
  59. package/scripts/flow-done +151 -0
  60. package/scripts/flow-done.js +489 -0
  61. package/scripts/flow-durable-session.js +1541 -0
  62. package/scripts/flow-entropy-monitor.js +345 -0
  63. package/scripts/flow-export-profile +349 -0
  64. package/scripts/flow-export-scanner.js +1046 -0
  65. package/scripts/flow-figma-confirm.js +400 -0
  66. package/scripts/flow-figma-extract.js +496 -0
  67. package/scripts/flow-figma-generate.js +683 -0
  68. package/scripts/flow-figma-index.js +909 -0
  69. package/scripts/flow-figma-match.js +617 -0
  70. package/scripts/flow-figma-mcp-server.js +518 -0
  71. package/scripts/flow-figma-pipeline.js +414 -0
  72. package/scripts/flow-file-ops.js +301 -0
  73. package/scripts/flow-gate-confidence.js +825 -0
  74. package/scripts/flow-guided-edit.js +659 -0
  75. package/scripts/flow-health +185 -0
  76. package/scripts/flow-health.js +413 -0
  77. package/scripts/flow-hooks.js +556 -0
  78. package/scripts/flow-http-client.js +249 -0
  79. package/scripts/flow-hybrid-detect.js +167 -0
  80. package/scripts/flow-hybrid-interactive.js +591 -0
  81. package/scripts/flow-hybrid-test.js +152 -0
  82. package/scripts/flow-import-profile +439 -0
  83. package/scripts/flow-init +253 -0
  84. package/scripts/flow-instruction-richness.js +827 -0
  85. package/scripts/flow-jira-integration.js +579 -0
  86. package/scripts/flow-knowledge-router.js +522 -0
  87. package/scripts/flow-knowledge-sync.js +589 -0
  88. package/scripts/flow-linear-integration.js +631 -0
  89. package/scripts/flow-links.js +774 -0
  90. package/scripts/flow-log-manager.js +559 -0
  91. package/scripts/flow-loop-enforcer.js +1246 -0
  92. package/scripts/flow-loop-retry-learning.js +630 -0
  93. package/scripts/flow-lsp.js +923 -0
  94. package/scripts/flow-map-index +348 -0
  95. package/scripts/flow-map-sync +201 -0
  96. package/scripts/flow-memory-blocks.js +668 -0
  97. package/scripts/flow-memory-compactor.js +350 -0
  98. package/scripts/flow-memory-db.js +1110 -0
  99. package/scripts/flow-memory-sync.js +484 -0
  100. package/scripts/flow-metrics.js +353 -0
  101. package/scripts/flow-migrate-ids.js +370 -0
  102. package/scripts/flow-model-adapter.js +802 -0
  103. package/scripts/flow-model-router.js +884 -0
  104. package/scripts/flow-models.js +1231 -0
  105. package/scripts/flow-morning.js +517 -0
  106. package/scripts/flow-multi-approach.js +660 -0
  107. package/scripts/flow-new-feature +86 -0
  108. package/scripts/flow-onboard +1042 -0
  109. package/scripts/flow-orchestrate-llm.js +459 -0
  110. package/scripts/flow-orchestrate.js +3592 -0
  111. package/scripts/flow-output.js +123 -0
  112. package/scripts/flow-parallel-detector.js +399 -0
  113. package/scripts/flow-parallel-dispatch.js +987 -0
  114. package/scripts/flow-parallel.js +428 -0
  115. package/scripts/flow-pattern-enforcer.js +600 -0
  116. package/scripts/flow-prd-manager.js +282 -0
  117. package/scripts/flow-progress.js +323 -0
  118. package/scripts/flow-project-analyzer.js +975 -0
  119. package/scripts/flow-prompt-composer.js +487 -0
  120. package/scripts/flow-providers.js +1381 -0
  121. package/scripts/flow-queue.js +308 -0
  122. package/scripts/flow-ready +82 -0
  123. package/scripts/flow-ready.js +189 -0
  124. package/scripts/flow-regression.js +396 -0
  125. package/scripts/flow-response-parser.js +450 -0
  126. package/scripts/flow-resume.js +284 -0
  127. package/scripts/flow-rules-sync.js +439 -0
  128. package/scripts/flow-run-trace.js +718 -0
  129. package/scripts/flow-safety.js +587 -0
  130. package/scripts/flow-search +104 -0
  131. package/scripts/flow-security.js +481 -0
  132. package/scripts/flow-session-end +106 -0
  133. package/scripts/flow-session-end.js +437 -0
  134. package/scripts/flow-session-state.js +671 -0
  135. package/scripts/flow-setup-hooks +216 -0
  136. package/scripts/flow-setup-hooks.js +377 -0
  137. package/scripts/flow-skill-create.js +329 -0
  138. package/scripts/flow-skill-creator.js +572 -0
  139. package/scripts/flow-skill-generator.js +1046 -0
  140. package/scripts/flow-skill-learn.js +880 -0
  141. package/scripts/flow-skill-matcher.js +578 -0
  142. package/scripts/flow-spec-generator.js +820 -0
  143. package/scripts/flow-stack-wizard.js +895 -0
  144. package/scripts/flow-standup +162 -0
  145. package/scripts/flow-start +74 -0
  146. package/scripts/flow-start.js +235 -0
  147. package/scripts/flow-status +110 -0
  148. package/scripts/flow-status.js +301 -0
  149. package/scripts/flow-step-browser.js +83 -0
  150. package/scripts/flow-step-changelog.js +217 -0
  151. package/scripts/flow-step-comments.js +306 -0
  152. package/scripts/flow-step-complexity.js +234 -0
  153. package/scripts/flow-step-coverage.js +218 -0
  154. package/scripts/flow-step-knowledge.js +193 -0
  155. package/scripts/flow-step-pr-tests.js +364 -0
  156. package/scripts/flow-step-regression.js +89 -0
  157. package/scripts/flow-step-review.js +516 -0
  158. package/scripts/flow-step-security.js +162 -0
  159. package/scripts/flow-step-silent-failures.js +290 -0
  160. package/scripts/flow-step-simplifier.js +346 -0
  161. package/scripts/flow-story +105 -0
  162. package/scripts/flow-story.js +500 -0
  163. package/scripts/flow-suspend.js +252 -0
  164. package/scripts/flow-sync-daemon.js +654 -0
  165. package/scripts/flow-task-analyzer.js +606 -0
  166. package/scripts/flow-team-dashboard.js +748 -0
  167. package/scripts/flow-team-sync.js +752 -0
  168. package/scripts/flow-team.js +977 -0
  169. package/scripts/flow-tech-options.js +528 -0
  170. package/scripts/flow-templates.js +812 -0
  171. package/scripts/flow-tiered-learning.js +728 -0
  172. package/scripts/flow-trace +204 -0
  173. package/scripts/flow-transcript-chunking.js +1106 -0
  174. package/scripts/flow-transcript-digest.js +7918 -0
  175. package/scripts/flow-transcript-language.js +465 -0
  176. package/scripts/flow-transcript-parsing.js +1085 -0
  177. package/scripts/flow-transcript-stories.js +2194 -0
  178. package/scripts/flow-update-map +224 -0
  179. package/scripts/flow-utils.js +2242 -0
  180. package/scripts/flow-verification.js +644 -0
  181. package/scripts/flow-verify.js +1177 -0
  182. package/scripts/flow-voice-input.js +638 -0
  183. package/scripts/flow-watch +168 -0
  184. package/scripts/flow-workflow-steps.js +521 -0
  185. package/scripts/flow-workflow.js +1029 -0
  186. package/scripts/flow-worktree.js +489 -0
  187. package/scripts/hooks/adapters/base-adapter.js +102 -0
  188. package/scripts/hooks/adapters/claude-code.js +359 -0
  189. package/scripts/hooks/adapters/index.js +79 -0
  190. package/scripts/hooks/core/component-check.js +341 -0
  191. package/scripts/hooks/core/index.js +35 -0
  192. package/scripts/hooks/core/loop-check.js +241 -0
  193. package/scripts/hooks/core/session-context.js +294 -0
  194. package/scripts/hooks/core/task-gate.js +177 -0
  195. package/scripts/hooks/core/validation.js +230 -0
  196. package/scripts/hooks/entry/claude-code/post-tool-use.js +65 -0
  197. package/scripts/hooks/entry/claude-code/pre-tool-use.js +89 -0
  198. package/scripts/hooks/entry/claude-code/session-end.js +87 -0
  199. package/scripts/hooks/entry/claude-code/session-start.js +46 -0
  200. package/scripts/hooks/entry/claude-code/stop.js +43 -0
  201. package/scripts/postinstall.js +139 -0
  202. package/templates/browser-test-flow.json +56 -0
  203. package/templates/bug-report.md +43 -0
  204. package/templates/component-detail.md +42 -0
  205. package/templates/component.stories.tsx +49 -0
  206. package/templates/context/constraints.md +83 -0
  207. package/templates/context/conventions.md +177 -0
  208. package/templates/context/stack.md +60 -0
  209. package/templates/correction-report.md +90 -0
  210. package/templates/feature-proposal.md +35 -0
  211. package/templates/hybrid/_base.md +254 -0
  212. package/templates/hybrid/_patterns.md +45 -0
  213. package/templates/hybrid/create-component.md +127 -0
  214. package/templates/hybrid/create-file.md +56 -0
  215. package/templates/hybrid/create-hook.md +145 -0
  216. package/templates/hybrid/create-service.md +70 -0
  217. package/templates/hybrid/fix-bug.md +33 -0
  218. package/templates/hybrid/modify-file.md +55 -0
  219. package/templates/story.md +68 -0
  220. package/templates/task.json +56 -0
  221. package/templates/trace.md +69 -0
@@ -0,0 +1,1381 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow - Model Provider Abstraction
5
+ *
6
+ * Unified interface for local and cloud LLM providers:
7
+ * - Ollama (local)
8
+ * - LM Studio (local)
9
+ * - Anthropic (cloud)
10
+ * - OpenAI (cloud)
11
+ *
12
+ * Usage as module:
13
+ * const { createProvider, listProviders } = require('./flow-providers');
14
+ * const provider = createProvider({ type: 'anthropic', apiKey: '...' });
15
+ * const response = await provider.complete(prompt, options);
16
+ *
17
+ * Usage as CLI:
18
+ * flow providers list # List available providers
19
+ * flow providers test <type> # Test a provider
20
+ * flow providers configure <type> # Configure a provider
21
+ */
22
+
23
+ const fs = require('fs');
24
+ const path = require('path');
25
+ const https = require('https');
26
+ const http = require('http');
27
+ const { getProjectRoot, colors: c } = require('./flow-utils');
28
+
29
+ const PROJECT_ROOT = getProjectRoot();
30
+ const WORKFLOW_DIR = path.join(PROJECT_ROOT, '.workflow');
31
+ const CONFIG_PATH = path.join(WORKFLOW_DIR, 'config.json');
32
+
33
+ /**
34
+ * Provider types
35
+ */
36
+ const PROVIDER_TYPES = {
37
+ OLLAMA: 'ollama',
38
+ LM_STUDIO: 'lm-studio',
39
+ ANTHROPIC: 'anthropic',
40
+ OPENAI: 'openai',
41
+ GOOGLE: 'google'
42
+ };
43
+
44
+ /**
45
+ * Model capability heuristics
46
+ * Used as fallback when API doesn't provide capability info
47
+ */
48
+ const MODEL_CAPABILITIES = {
49
+ // High-quality code models
50
+ 'qwen': { codeQuality: 'high', instructionFollowing: 'high', contextWindow: 32768 },
51
+ 'qwen3': { codeQuality: 'excellent', instructionFollowing: 'excellent', contextWindow: 131072 },
52
+ 'codellama': { codeQuality: 'high', instructionFollowing: 'medium', contextWindow: 16384 },
53
+ 'deepseek': { codeQuality: 'excellent', instructionFollowing: 'high', contextWindow: 32768 },
54
+ 'deepseek-coder': { codeQuality: 'excellent', instructionFollowing: 'high', contextWindow: 16384 },
55
+ 'nemotron': { codeQuality: 'high', instructionFollowing: 'excellent', contextWindow: 32768 },
56
+ 'starcoder': { codeQuality: 'high', instructionFollowing: 'medium', contextWindow: 8192 },
57
+
58
+ // General purpose models
59
+ 'llama3': { codeQuality: 'medium', instructionFollowing: 'high', contextWindow: 8192 },
60
+ 'llama3.1': { codeQuality: 'high', instructionFollowing: 'high', contextWindow: 131072 },
61
+ 'llama3.2': { codeQuality: 'medium', instructionFollowing: 'high', contextWindow: 131072 },
62
+ 'mistral': { codeQuality: 'medium', instructionFollowing: 'high', contextWindow: 32768 },
63
+ 'mixtral': { codeQuality: 'high', instructionFollowing: 'high', contextWindow: 32768 },
64
+ 'phi': { codeQuality: 'medium', instructionFollowing: 'medium', contextWindow: 4096 },
65
+ 'phi3': { codeQuality: 'high', instructionFollowing: 'high', contextWindow: 128000 },
66
+ 'gemma': { codeQuality: 'medium', instructionFollowing: 'high', contextWindow: 8192 },
67
+ 'gemma2': { codeQuality: 'high', instructionFollowing: 'high', contextWindow: 8192 },
68
+
69
+ // Cloud models - Full capability
70
+ 'claude': { codeQuality: 'excellent', instructionFollowing: 'excellent', contextWindow: 200000 },
71
+ 'gpt-4': { codeQuality: 'excellent', instructionFollowing: 'excellent', contextWindow: 128000 },
72
+ 'gpt-3.5': { codeQuality: 'medium', instructionFollowing: 'high', contextWindow: 16385 },
73
+
74
+ // Cloud models - Executor tier (cheaper/faster)
75
+ 'gpt-4o-mini': { codeQuality: 'high', instructionFollowing: 'high', contextWindow: 128000, costTier: 'cheap' },
76
+ 'claude-3-haiku': { codeQuality: 'high', instructionFollowing: 'high', contextWindow: 200000, costTier: 'cheap' },
77
+ 'claude-3-5-haiku': { codeQuality: 'high', instructionFollowing: 'excellent', contextWindow: 200000, costTier: 'cheap' },
78
+ 'gemini-flash': { codeQuality: 'high', instructionFollowing: 'high', contextWindow: 1000000, costTier: 'cheap' },
79
+ 'gemini-2.0-flash': { codeQuality: 'high', instructionFollowing: 'high', contextWindow: 1000000, costTier: 'cheap' },
80
+ 'gemini-1.5-flash': { codeQuality: 'high', instructionFollowing: 'high', contextWindow: 1000000, costTier: 'cheap' },
81
+ };
82
+
83
+ /**
84
+ * Detect model capabilities from model name
85
+ */
86
+ function detectModelCapabilities(modelName) {
87
+ if (!modelName) return null;
88
+
89
+ const nameLower = modelName.toLowerCase();
90
+
91
+ // Try exact matches first
92
+ for (const [pattern, caps] of Object.entries(MODEL_CAPABILITIES)) {
93
+ if (nameLower.includes(pattern)) {
94
+ return {
95
+ ...caps,
96
+ source: 'heuristic',
97
+ matchedPattern: pattern
98
+ };
99
+ }
100
+ }
101
+
102
+ // Default fallback for unknown models
103
+ return {
104
+ codeQuality: 'unknown',
105
+ instructionFollowing: 'unknown',
106
+ contextWindow: 4096,
107
+ source: 'default'
108
+ };
109
+ }
110
+
111
+ /**
112
+ * Get recommended model for a task type
113
+ */
114
+ function getRecommendedModel(availableModels, taskType = 'code') {
115
+ if (!availableModels || availableModels.length === 0) return null;
116
+
117
+ const withCaps = availableModels.map(m => ({
118
+ ...m,
119
+ capabilities: detectModelCapabilities(m.id || m.name)
120
+ }));
121
+
122
+ // Score models based on task
123
+ const scored = withCaps.map(m => {
124
+ let score = 0;
125
+ const caps = m.capabilities;
126
+
127
+ if (taskType === 'code') {
128
+ if (caps.codeQuality === 'excellent') score += 10;
129
+ else if (caps.codeQuality === 'high') score += 7;
130
+ else if (caps.codeQuality === 'medium') score += 4;
131
+ }
132
+
133
+ if (caps.instructionFollowing === 'excellent') score += 5;
134
+ else if (caps.instructionFollowing === 'high') score += 3;
135
+
136
+ // Prefer larger context windows for complex tasks
137
+ if (caps.contextWindow >= 32768) score += 3;
138
+ else if (caps.contextWindow >= 16384) score += 2;
139
+
140
+ return { ...m, score };
141
+ });
142
+
143
+ // Sort by score descending
144
+ scored.sort((a, b) => b.score - a.score);
145
+
146
+ return scored[0] || null;
147
+ }
148
+
149
+ /**
150
+ * Default provider configurations
151
+ */
152
+ const DEFAULT_CONFIGS = {
153
+ ollama: {
154
+ endpoint: 'http://localhost:11434',
155
+ model: 'llama3.2',
156
+ temperature: 0.7,
157
+ maxTokens: 4096,
158
+ timeout: 120000
159
+ },
160
+ 'lm-studio': {
161
+ endpoint: 'http://localhost:1234/v1',
162
+ model: 'local-model',
163
+ temperature: 0.7,
164
+ maxTokens: 4096,
165
+ timeout: 120000
166
+ },
167
+ anthropic: {
168
+ endpoint: 'https://api.anthropic.com/v1',
169
+ model: 'claude-sonnet-4-20250514',
170
+ temperature: 0.7,
171
+ maxTokens: 4096,
172
+ timeout: 60000
173
+ },
174
+ openai: {
175
+ endpoint: 'https://api.openai.com/v1',
176
+ model: 'gpt-4o',
177
+ temperature: 0.7,
178
+ maxTokens: 4096,
179
+ timeout: 60000
180
+ },
181
+ google: {
182
+ endpoint: 'https://generativelanguage.googleapis.com/v1beta',
183
+ model: 'gemini-2.0-flash-exp',
184
+ temperature: 0.7,
185
+ maxTokens: 4096,
186
+ timeout: 60000
187
+ }
188
+ };
189
+
190
+ /**
191
+ * Base provider class
192
+ */
193
+ class BaseProvider {
194
+ constructor(config) {
195
+ this.config = config;
196
+ this.name = 'base';
197
+ }
198
+
199
+ async complete(prompt, options = {}) {
200
+ throw new Error('Not implemented');
201
+ }
202
+
203
+ async test() {
204
+ try {
205
+ const response = await this.complete('Say "OK" if you can hear me.', {
206
+ maxTokens: 10
207
+ });
208
+ return { success: true, response };
209
+ } catch (error) {
210
+ return { success: false, error: error.message };
211
+ }
212
+ }
213
+
214
+ async listModels() {
215
+ return [];
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Ollama provider
221
+ */
222
+ class OllamaProvider extends BaseProvider {
223
+ constructor(config) {
224
+ super(config);
225
+ this.name = 'ollama';
226
+ }
227
+
228
+ async complete(prompt, options = {}) {
229
+ const endpoint = this.config.endpoint || DEFAULT_CONFIGS.ollama.endpoint;
230
+ const url = new URL('/api/generate', endpoint);
231
+
232
+ const body = {
233
+ model: options.model || this.config.model || DEFAULT_CONFIGS.ollama.model,
234
+ prompt,
235
+ stream: false,
236
+ options: {
237
+ temperature: options.temperature || this.config.temperature || 0.7,
238
+ num_predict: options.maxTokens || this.config.maxTokens || 4096
239
+ }
240
+ };
241
+
242
+ const response = await this._request(url, body);
243
+ return {
244
+ content: response.response,
245
+ model: response.model,
246
+ usage: {
247
+ promptTokens: response.prompt_eval_count,
248
+ completionTokens: response.eval_count,
249
+ totalTokens: (response.prompt_eval_count || 0) + (response.eval_count || 0)
250
+ }
251
+ };
252
+ }
253
+
254
+ async listModels() {
255
+ const endpoint = this.config.endpoint || DEFAULT_CONFIGS.ollama.endpoint;
256
+ const url = new URL('/api/tags', endpoint);
257
+
258
+ try {
259
+ const response = await this._request(url, null, 'GET');
260
+ return (response.models || []).map(m => ({
261
+ id: m.name,
262
+ name: m.name,
263
+ size: m.size,
264
+ capabilities: detectModelCapabilities(m.name)
265
+ }));
266
+ } catch {
267
+ return [];
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Get detailed model info from Ollama API
273
+ */
274
+ async getModelInfo(modelName) {
275
+ const endpoint = this.config.endpoint || DEFAULT_CONFIGS.ollama.endpoint;
276
+ const url = new URL('/api/show', endpoint);
277
+
278
+ try {
279
+ const response = await this._request(url, { name: modelName });
280
+
281
+ // Extract capabilities from model metadata if available
282
+ const modelfile = response.modelfile || '';
283
+ const parameters = response.parameters || '';
284
+
285
+ // Parse context length from modelfile or parameters
286
+ let contextWindow = 4096;
287
+ const ctxMatch = modelfile.match(/num_ctx\s+(\d+)/i) ||
288
+ parameters.match(/num_ctx\s+(\d+)/i);
289
+ if (ctxMatch) {
290
+ contextWindow = parseInt(ctxMatch[1], 10);
291
+ }
292
+
293
+ // Get heuristic capabilities and override with API data
294
+ const heuristicCaps = detectModelCapabilities(modelName);
295
+
296
+ return {
297
+ name: modelName,
298
+ modelfile: modelfile.slice(0, 500), // Truncate for display
299
+ parameters,
300
+ capabilities: {
301
+ ...heuristicCaps,
302
+ contextWindow: Math.max(contextWindow, heuristicCaps.contextWindow),
303
+ source: 'api+heuristic'
304
+ }
305
+ };
306
+ } catch {
307
+ // Fall back to heuristic only
308
+ return {
309
+ name: modelName,
310
+ capabilities: detectModelCapabilities(modelName)
311
+ };
312
+ }
313
+ }
314
+
315
+ _request(url, body, method = 'POST') {
316
+ return new Promise((resolve, reject) => {
317
+ const options = {
318
+ method,
319
+ hostname: url.hostname,
320
+ port: url.port || 11434,
321
+ path: url.pathname,
322
+ headers: {
323
+ 'Content-Type': 'application/json'
324
+ },
325
+ timeout: this.config.timeout || 120000
326
+ };
327
+
328
+ const req = http.request(options, (res) => {
329
+ let data = '';
330
+ res.on('data', chunk => data += chunk);
331
+ res.on('end', () => {
332
+ try {
333
+ resolve(JSON.parse(data));
334
+ } catch {
335
+ reject(new Error(`Invalid response: ${data.slice(0, 100)}`));
336
+ }
337
+ });
338
+ });
339
+
340
+ req.on('error', reject);
341
+ req.on('timeout', () => reject(new Error('Request timeout')));
342
+
343
+ if (body) {
344
+ req.write(JSON.stringify(body));
345
+ }
346
+ req.end();
347
+ });
348
+ }
349
+ }
350
+
351
+ /**
352
+ * LM Studio provider (OpenAI-compatible)
353
+ */
354
+ class LMStudioProvider extends BaseProvider {
355
+ constructor(config) {
356
+ super(config);
357
+ this.name = 'lm-studio';
358
+ }
359
+
360
+ async complete(prompt, options = {}) {
361
+ const endpoint = this.config.endpoint || DEFAULT_CONFIGS['lm-studio'].endpoint;
362
+ const url = new URL('/chat/completions', endpoint);
363
+
364
+ const body = {
365
+ model: options.model || this.config.model || 'local-model',
366
+ messages: [{ role: 'user', content: prompt }],
367
+ temperature: options.temperature || this.config.temperature || 0.7,
368
+ max_tokens: options.maxTokens || this.config.maxTokens || 4096
369
+ };
370
+
371
+ const response = await this._request(url, body);
372
+ return {
373
+ content: response.choices?.[0]?.message?.content || '',
374
+ model: response.model,
375
+ usage: {
376
+ promptTokens: response.usage?.prompt_tokens,
377
+ completionTokens: response.usage?.completion_tokens,
378
+ totalTokens: response.usage?.total_tokens
379
+ }
380
+ };
381
+ }
382
+
383
+ _request(url, body) {
384
+ return new Promise((resolve, reject) => {
385
+ const options = {
386
+ method: 'POST',
387
+ hostname: url.hostname,
388
+ port: url.port || 1234,
389
+ path: url.pathname,
390
+ headers: {
391
+ 'Content-Type': 'application/json'
392
+ },
393
+ timeout: this.config.timeout || 120000
394
+ };
395
+
396
+ const req = http.request(options, (res) => {
397
+ let data = '';
398
+ res.on('data', chunk => data += chunk);
399
+ res.on('end', () => {
400
+ try {
401
+ resolve(JSON.parse(data));
402
+ } catch {
403
+ reject(new Error(`Invalid response: ${data.slice(0, 100)}`));
404
+ }
405
+ });
406
+ });
407
+
408
+ req.on('error', reject);
409
+ req.on('timeout', () => reject(new Error('Request timeout')));
410
+ req.write(JSON.stringify(body));
411
+ req.end();
412
+ });
413
+ }
414
+
415
+ async listModels() {
416
+ const endpoint = this.config.endpoint || DEFAULT_CONFIGS['lm-studio'].endpoint;
417
+ return new Promise((resolve) => {
418
+ const url = new URL('/v1/models', endpoint);
419
+ const req = http.request({
420
+ method: 'GET',
421
+ hostname: url.hostname,
422
+ port: url.port || 1234,
423
+ path: url.pathname,
424
+ timeout: 5000
425
+ }, (res) => {
426
+ let data = '';
427
+ res.on('data', chunk => data += chunk);
428
+ res.on('end', () => {
429
+ try {
430
+ const parsed = JSON.parse(data);
431
+ resolve((parsed.data || []).map(m => ({ id: m.id, name: m.id })));
432
+ } catch {
433
+ resolve([]);
434
+ }
435
+ });
436
+ });
437
+ req.on('error', () => resolve([]));
438
+ req.on('timeout', () => resolve([]));
439
+ req.end();
440
+ });
441
+ }
442
+ }
443
+
444
+ /**
445
+ * Anthropic provider
446
+ */
447
+ class AnthropicProvider extends BaseProvider {
448
+ constructor(config) {
449
+ super(config);
450
+ this.name = 'anthropic';
451
+ this.apiKey = config.apiKey || process.env.ANTHROPIC_API_KEY;
452
+ }
453
+
454
+ async complete(prompt, options = {}) {
455
+ if (!this.apiKey) {
456
+ throw new Error('Anthropic API key not configured. Set ANTHROPIC_API_KEY or provide apiKey in config.');
457
+ }
458
+
459
+ const endpoint = this.config.endpoint || DEFAULT_CONFIGS.anthropic.endpoint;
460
+ const url = new URL('/messages', endpoint);
461
+
462
+ const body = {
463
+ model: options.model || this.config.model || DEFAULT_CONFIGS.anthropic.model,
464
+ max_tokens: options.maxTokens || this.config.maxTokens || 4096,
465
+ messages: [{ role: 'user', content: prompt }]
466
+ };
467
+
468
+ if (options.system || this.config.system) {
469
+ body.system = options.system || this.config.system;
470
+ }
471
+
472
+ const response = await this._request(url, body);
473
+
474
+ if (response.error) {
475
+ throw new Error(response.error.message);
476
+ }
477
+
478
+ return {
479
+ content: response.content?.[0]?.text || '',
480
+ model: response.model,
481
+ stopReason: response.stop_reason,
482
+ usage: {
483
+ promptTokens: response.usage?.input_tokens,
484
+ completionTokens: response.usage?.output_tokens,
485
+ totalTokens: (response.usage?.input_tokens || 0) + (response.usage?.output_tokens || 0)
486
+ }
487
+ };
488
+ }
489
+
490
+ _request(url, body) {
491
+ return new Promise((resolve, reject) => {
492
+ const options = {
493
+ method: 'POST',
494
+ hostname: url.hostname,
495
+ path: url.pathname,
496
+ headers: {
497
+ 'Content-Type': 'application/json',
498
+ 'x-api-key': this.apiKey,
499
+ 'anthropic-version': '2023-06-01'
500
+ },
501
+ timeout: this.config.timeout || 60000
502
+ };
503
+
504
+ const req = https.request(options, (res) => {
505
+ let data = '';
506
+ res.on('data', chunk => data += chunk);
507
+ res.on('end', () => {
508
+ try {
509
+ resolve(JSON.parse(data));
510
+ } catch {
511
+ reject(new Error(`Invalid response: ${data.slice(0, 100)}`));
512
+ }
513
+ });
514
+ });
515
+
516
+ req.on('error', reject);
517
+ req.on('timeout', () => reject(new Error('Request timeout')));
518
+ req.write(JSON.stringify(body));
519
+ req.end();
520
+ });
521
+ }
522
+ }
523
+
524
+ /**
525
+ * OpenAI provider
526
+ */
527
+ class OpenAIProvider extends BaseProvider {
528
+ constructor(config) {
529
+ super(config);
530
+ this.name = 'openai';
531
+ this.apiKey = config.apiKey || process.env.OPENAI_API_KEY;
532
+ }
533
+
534
+ async complete(prompt, options = {}) {
535
+ if (!this.apiKey) {
536
+ throw new Error('OpenAI API key not configured. Set OPENAI_API_KEY or provide apiKey in config.');
537
+ }
538
+
539
+ const endpoint = this.config.endpoint || DEFAULT_CONFIGS.openai.endpoint;
540
+ const url = new URL('/chat/completions', endpoint);
541
+
542
+ const messages = [{ role: 'user', content: prompt }];
543
+
544
+ if (options.system || this.config.system) {
545
+ messages.unshift({ role: 'system', content: options.system || this.config.system });
546
+ }
547
+
548
+ const body = {
549
+ model: options.model || this.config.model || DEFAULT_CONFIGS.openai.model,
550
+ messages,
551
+ temperature: options.temperature || this.config.temperature || 0.7,
552
+ max_tokens: options.maxTokens || this.config.maxTokens || 4096
553
+ };
554
+
555
+ const response = await this._request(url, body);
556
+
557
+ if (response.error) {
558
+ throw new Error(response.error.message);
559
+ }
560
+
561
+ return {
562
+ content: response.choices?.[0]?.message?.content || '',
563
+ model: response.model,
564
+ stopReason: response.choices?.[0]?.finish_reason,
565
+ usage: {
566
+ promptTokens: response.usage?.prompt_tokens,
567
+ completionTokens: response.usage?.completion_tokens,
568
+ totalTokens: response.usage?.total_tokens
569
+ }
570
+ };
571
+ }
572
+
573
+ async listModels() {
574
+ if (!this.apiKey) return [];
575
+
576
+ const endpoint = this.config.endpoint || DEFAULT_CONFIGS.openai.endpoint;
577
+ const url = new URL('/models', endpoint);
578
+
579
+ try {
580
+ const response = await this._request(url, null, 'GET');
581
+ return (response.data || [])
582
+ .filter(m => m.id.includes('gpt'))
583
+ .map(m => ({
584
+ id: m.id,
585
+ name: m.id,
586
+ owned_by: m.owned_by
587
+ }));
588
+ } catch {
589
+ return [];
590
+ }
591
+ }
592
+
593
+ _request(url, body, method = 'POST') {
594
+ return new Promise((resolve, reject) => {
595
+ const options = {
596
+ method,
597
+ hostname: url.hostname,
598
+ path: url.pathname,
599
+ headers: {
600
+ 'Content-Type': 'application/json',
601
+ 'Authorization': `Bearer ${this.apiKey}`
602
+ },
603
+ timeout: this.config.timeout || 60000
604
+ };
605
+
606
+ const req = https.request(options, (res) => {
607
+ let data = '';
608
+ res.on('data', chunk => data += chunk);
609
+ res.on('end', () => {
610
+ try {
611
+ resolve(JSON.parse(data));
612
+ } catch {
613
+ reject(new Error(`Invalid response: ${data.slice(0, 100)}`));
614
+ }
615
+ });
616
+ });
617
+
618
+ req.on('error', reject);
619
+ req.on('timeout', () => reject(new Error('Request timeout')));
620
+
621
+ if (body) {
622
+ req.write(JSON.stringify(body));
623
+ }
624
+ req.end();
625
+ });
626
+ }
627
+ }
628
+
629
+ /**
630
+ * Google (Gemini) provider
631
+ */
632
+ class GoogleProvider extends BaseProvider {
633
+ constructor(config) {
634
+ super(config);
635
+ this.name = 'google';
636
+ this.apiKey = config.apiKey || process.env.GOOGLE_API_KEY;
637
+ }
638
+
639
+ async complete(prompt, options = {}) {
640
+ if (!this.apiKey) {
641
+ throw new Error('Google API key not configured. Set GOOGLE_API_KEY or provide apiKey in config.');
642
+ }
643
+
644
+ const model = options.model || this.config.model || DEFAULT_CONFIGS.google.model;
645
+ const endpoint = this.config.endpoint || DEFAULT_CONFIGS.google.endpoint;
646
+ const url = new URL(`/models/${model}:generateContent`, endpoint);
647
+ // API key passed via header for security (not URL to avoid logging exposure)
648
+
649
+ const body = {
650
+ contents: [{
651
+ parts: [{ text: prompt }]
652
+ }],
653
+ generationConfig: {
654
+ temperature: options.temperature || this.config.temperature || 0.7,
655
+ maxOutputTokens: options.maxTokens || this.config.maxTokens || 4096
656
+ }
657
+ };
658
+
659
+ // Add system instruction if provided
660
+ if (options.system || this.config.system) {
661
+ body.systemInstruction = {
662
+ parts: [{ text: options.system || this.config.system }]
663
+ };
664
+ }
665
+
666
+ const response = await this._request(url, body, 'POST', { 'x-goog-api-key': this.apiKey });
667
+
668
+ if (response.error) {
669
+ throw new Error(response.error.message);
670
+ }
671
+
672
+ const content = response.candidates?.[0]?.content?.parts?.[0]?.text || '';
673
+
674
+ return {
675
+ content,
676
+ model,
677
+ stopReason: response.candidates?.[0]?.finishReason,
678
+ usage: {
679
+ promptTokens: response.usageMetadata?.promptTokenCount,
680
+ completionTokens: response.usageMetadata?.candidatesTokenCount,
681
+ totalTokens: response.usageMetadata?.totalTokenCount
682
+ }
683
+ };
684
+ }
685
+
686
+ async listModels() {
687
+ if (!this.apiKey) return [];
688
+
689
+ const endpoint = this.config.endpoint || DEFAULT_CONFIGS.google.endpoint;
690
+ const url = new URL('/models', endpoint);
691
+
692
+ try {
693
+ const response = await this._request(url, null, 'GET', { 'x-goog-api-key': this.apiKey });
694
+ return (response.models || [])
695
+ .filter(m => m.name.includes('gemini'))
696
+ .map(m => ({
697
+ id: m.name.replace('models/', ''),
698
+ name: m.displayName || m.name,
699
+ description: m.description
700
+ }));
701
+ } catch {
702
+ return [];
703
+ }
704
+ }
705
+
706
+ _request(url, body, method = 'POST', additionalHeaders = {}) {
707
+ return new Promise((resolve, reject) => {
708
+ const options = {
709
+ method,
710
+ hostname: url.hostname,
711
+ path: url.pathname + url.search,
712
+ headers: {
713
+ 'Content-Type': 'application/json',
714
+ ...additionalHeaders
715
+ },
716
+ timeout: this.config.timeout || 60000
717
+ };
718
+
719
+ const req = https.request(options, (res) => {
720
+ let data = '';
721
+ res.on('data', chunk => data += chunk);
722
+ res.on('end', () => {
723
+ try {
724
+ resolve(JSON.parse(data));
725
+ } catch {
726
+ reject(new Error(`Invalid response: ${data.slice(0, 100)}`));
727
+ }
728
+ });
729
+ });
730
+
731
+ req.on('error', reject);
732
+ req.on('timeout', () => reject(new Error('Request timeout')));
733
+
734
+ if (body) {
735
+ req.write(JSON.stringify(body));
736
+ }
737
+ req.end();
738
+ });
739
+ }
740
+ }
741
+
742
+ /**
743
+ * Create a provider instance
744
+ */
745
+ function createProvider(config) {
746
+ const type = config.type || config.provider;
747
+
748
+ switch (type) {
749
+ case PROVIDER_TYPES.OLLAMA:
750
+ return new OllamaProvider(config);
751
+ case PROVIDER_TYPES.LM_STUDIO:
752
+ return new LMStudioProvider(config);
753
+ case PROVIDER_TYPES.ANTHROPIC:
754
+ return new AnthropicProvider(config);
755
+ case PROVIDER_TYPES.OPENAI:
756
+ return new OpenAIProvider(config);
757
+ case PROVIDER_TYPES.GOOGLE:
758
+ return new GoogleProvider(config);
759
+ default:
760
+ throw new Error(`Unknown provider type: ${type}`);
761
+ }
762
+ }
763
+
764
+ /**
765
+ * Create executor provider from hybrid config
766
+ * Supports both legacy config (provider/model) and new executor config
767
+ */
768
+ function createExecutorFromConfig(hybridConfig) {
769
+ // New executor config structure
770
+ if (hybridConfig.executor && hybridConfig.executor.provider) {
771
+ const executor = hybridConfig.executor;
772
+ return createProvider({
773
+ type: executor.provider,
774
+ endpoint: executor.providerEndpoint,
775
+ model: executor.model,
776
+ apiKey: executor.apiKey,
777
+ ...hybridConfig.settings
778
+ });
779
+ }
780
+
781
+ // Legacy config structure (backward compatible)
782
+ if (hybridConfig.provider) {
783
+ return createProvider({
784
+ type: hybridConfig.provider,
785
+ endpoint: hybridConfig.providerEndpoint,
786
+ model: hybridConfig.model,
787
+ apiKey: hybridConfig.apiKey,
788
+ ...hybridConfig.settings
789
+ });
790
+ }
791
+
792
+ return null;
793
+ }
794
+
795
+ /**
796
+ * Get executor configuration from hybrid config
797
+ * Normalizes legacy and new config formats
798
+ */
799
+ function getExecutorConfig(hybridConfig) {
800
+ // New executor config
801
+ if (hybridConfig.executor && hybridConfig.executor.provider) {
802
+ return {
803
+ type: hybridConfig.executor.type || 'local',
804
+ provider: hybridConfig.executor.provider,
805
+ providerEndpoint: hybridConfig.executor.providerEndpoint,
806
+ model: hybridConfig.executor.model,
807
+ apiKey: hybridConfig.executor.apiKey
808
+ };
809
+ }
810
+
811
+ // Legacy config
812
+ if (hybridConfig.provider) {
813
+ const isLocal = ['ollama', 'lm-studio'].includes(hybridConfig.provider);
814
+ return {
815
+ type: isLocal ? 'local' : 'cloud',
816
+ provider: hybridConfig.provider,
817
+ providerEndpoint: hybridConfig.providerEndpoint,
818
+ model: hybridConfig.model,
819
+ apiKey: hybridConfig.apiKey
820
+ };
821
+ }
822
+
823
+ return null;
824
+ }
825
+
826
+ /**
827
+ * List all available providers
828
+ */
829
+ function listProviders() {
830
+ return [
831
+ {
832
+ type: PROVIDER_TYPES.OLLAMA,
833
+ name: 'Ollama',
834
+ local: true,
835
+ requiresKey: false,
836
+ defaultEndpoint: DEFAULT_CONFIGS.ollama.endpoint,
837
+ executorModels: ['qwen3-coder', 'deepseek-coder', 'codellama', 'nemotron']
838
+ },
839
+ {
840
+ type: PROVIDER_TYPES.LM_STUDIO,
841
+ name: 'LM Studio',
842
+ local: true,
843
+ requiresKey: false,
844
+ defaultEndpoint: DEFAULT_CONFIGS['lm-studio'].endpoint,
845
+ executorModels: ['loaded model']
846
+ },
847
+ {
848
+ type: PROVIDER_TYPES.ANTHROPIC,
849
+ name: 'Anthropic',
850
+ local: false,
851
+ requiresKey: true,
852
+ envVar: 'ANTHROPIC_API_KEY',
853
+ defaultEndpoint: DEFAULT_CONFIGS.anthropic.endpoint,
854
+ executorModels: ['claude-3-5-haiku-latest', 'claude-3-haiku-20240307']
855
+ },
856
+ {
857
+ type: PROVIDER_TYPES.OPENAI,
858
+ name: 'OpenAI',
859
+ local: false,
860
+ requiresKey: true,
861
+ envVar: 'OPENAI_API_KEY',
862
+ defaultEndpoint: DEFAULT_CONFIGS.openai.endpoint,
863
+ executorModels: ['gpt-4o-mini', 'gpt-4o']
864
+ },
865
+ {
866
+ type: PROVIDER_TYPES.GOOGLE,
867
+ name: 'Google (Gemini)',
868
+ local: false,
869
+ requiresKey: true,
870
+ envVar: 'GOOGLE_API_KEY',
871
+ defaultEndpoint: DEFAULT_CONFIGS.google.endpoint,
872
+ executorModels: ['gemini-2.0-flash-exp', 'gemini-1.5-flash']
873
+ }
874
+ ];
875
+ }
876
+
877
+ /**
878
+ * Detect available providers
879
+ */
880
+ async function detectProviders() {
881
+ const available = [];
882
+
883
+ // Check Ollama (local)
884
+ try {
885
+ const ollama = new OllamaProvider({});
886
+ const models = await ollama.listModels();
887
+ if (models.length > 0) {
888
+ available.push({
889
+ type: PROVIDER_TYPES.OLLAMA,
890
+ name: 'Ollama',
891
+ local: true,
892
+ cost: 'free',
893
+ models: models.slice(0, 5)
894
+ });
895
+ }
896
+ } catch {
897
+ // Not available
898
+ }
899
+
900
+ // Check LM Studio (local)
901
+ try {
902
+ const lmStudio = new LMStudioProvider({});
903
+ const models = await lmStudio.listModels();
904
+ if (models.length > 0) {
905
+ available.push({
906
+ type: PROVIDER_TYPES.LM_STUDIO,
907
+ name: 'LM Studio',
908
+ local: true,
909
+ cost: 'free',
910
+ models: models
911
+ });
912
+ }
913
+ } catch {
914
+ // Not available
915
+ }
916
+
917
+ // Check Anthropic (cloud - if key present)
918
+ if (process.env.ANTHROPIC_API_KEY) {
919
+ available.push({
920
+ type: PROVIDER_TYPES.ANTHROPIC,
921
+ name: 'Anthropic',
922
+ local: false,
923
+ cost: 'paid',
924
+ models: [
925
+ { id: 'claude-3-5-haiku-20241022', name: 'Claude 3.5 Haiku (Best for executor)', recommended: true },
926
+ { id: 'claude-sonnet-4-20250514', name: 'Claude Sonnet 4' },
927
+ { id: 'claude-3-5-sonnet-20241022', name: 'Claude 3.5 Sonnet' }
928
+ ]
929
+ });
930
+ }
931
+
932
+ // Check OpenAI (cloud - if key present)
933
+ if (process.env.OPENAI_API_KEY) {
934
+ available.push({
935
+ type: PROVIDER_TYPES.OPENAI,
936
+ name: 'OpenAI',
937
+ local: false,
938
+ cost: 'paid',
939
+ models: [
940
+ { id: 'gpt-4o-mini', name: 'GPT-4o Mini (Best for executor)', recommended: true },
941
+ { id: 'gpt-4o', name: 'GPT-4o' }
942
+ ]
943
+ });
944
+ }
945
+
946
+ // Check Google (cloud - if key present)
947
+ if (process.env.GOOGLE_API_KEY) {
948
+ available.push({
949
+ type: PROVIDER_TYPES.GOOGLE,
950
+ name: 'Google (Gemini)',
951
+ local: false,
952
+ cost: 'paid',
953
+ models: [
954
+ { id: 'gemini-2.0-flash-exp', name: 'Gemini 2.0 Flash (Best for executor)', recommended: true },
955
+ { id: 'gemini-1.5-flash', name: 'Gemini 1.5 Flash' }
956
+ ]
957
+ });
958
+ }
959
+
960
+ return available;
961
+ }
962
+
963
+ /**
964
+ * Load provider from config
965
+ */
966
+ function loadProviderFromConfig() {
967
+ if (!fs.existsSync(CONFIG_PATH)) {
968
+ return null;
969
+ }
970
+
971
+ try {
972
+ const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
973
+ const hybridConfig = config.hybrid || {};
974
+
975
+ if (!hybridConfig.enabled || !hybridConfig.provider) {
976
+ return null;
977
+ }
978
+
979
+ return createProvider({
980
+ type: hybridConfig.provider,
981
+ endpoint: hybridConfig.providerEndpoint,
982
+ model: hybridConfig.model,
983
+ apiKey: hybridConfig.apiKey,
984
+ ...hybridConfig.settings
985
+ });
986
+ } catch {
987
+ return null;
988
+ }
989
+ }
990
+
991
+ // ============================================================
992
+ // Token Budgeting with Auto-Detection
993
+ // ============================================================
994
+
995
+ /**
996
+ * Auto-detect model context limit from provider API
997
+ * No manual config needed - queries Ollama/LM Studio directly
998
+ *
999
+ * @param {string} providerType - 'ollama' or 'lm-studio'
1000
+ * @param {string} endpoint - Provider endpoint URL
1001
+ * @param {string} modelName - Model name to check
1002
+ * @returns {Promise<number>} Context window size in tokens
1003
+ */
1004
+ async function getModelContextLimit(providerType, endpoint, modelName) {
1005
+ try {
1006
+ if (providerType === PROVIDER_TYPES.OLLAMA || providerType === 'ollama') {
1007
+ // Ollama /api/show returns model metadata including num_ctx
1008
+ const provider = new OllamaProvider({ endpoint });
1009
+ const info = await provider.getModelInfo(modelName);
1010
+
1011
+ if (info?.capabilities?.contextWindow) {
1012
+ return info.capabilities.contextWindow;
1013
+ }
1014
+ return 4096; // Ollama default
1015
+ }
1016
+
1017
+ if (providerType === PROVIDER_TYPES.LM_STUDIO || providerType === 'lm-studio' || providerType === 'lmstudio') {
1018
+ // LM Studio /v1/models returns context_length in model metadata
1019
+ const url = new URL('/v1/models', endpoint);
1020
+
1021
+ return new Promise((resolve) => {
1022
+ const req = http.get(url.toString(), { timeout: 5000 }, (res) => {
1023
+ let data = '';
1024
+ res.on('data', chunk => data += chunk);
1025
+ res.on('end', () => {
1026
+ try {
1027
+ const parsed = JSON.parse(data);
1028
+ // LM Studio returns context_length in model data
1029
+ const model = parsed.data?.find(m => m.id === modelName) || parsed.data?.[0];
1030
+ if (model?.context_length) {
1031
+ resolve(model.context_length);
1032
+ } else {
1033
+ // Default for LM Studio
1034
+ resolve(8192);
1035
+ }
1036
+ } catch {
1037
+ resolve(8192);
1038
+ }
1039
+ });
1040
+ });
1041
+
1042
+ req.on('error', () => resolve(8192));
1043
+ req.on('timeout', () => {
1044
+ req.destroy();
1045
+ resolve(8192);
1046
+ });
1047
+ });
1048
+ }
1049
+
1050
+ // Cloud providers - use known limits from capabilities
1051
+ const caps = detectModelCapabilities(modelName);
1052
+ return caps?.contextWindow || 8192;
1053
+
1054
+ } catch (err) {
1055
+ if (process.env.DEBUG) {
1056
+ console.warn(`Could not detect context limit: ${err.message}`);
1057
+ }
1058
+ return 8192; // Safe fallback
1059
+ }
1060
+ }
1061
+
1062
+ /**
1063
+ * Estimate token count from text
1064
+ * Uses conservative 4 chars per token estimate for English/code
1065
+ *
1066
+ * @param {string} text - Text to estimate
1067
+ * @returns {number} Estimated token count
1068
+ */
1069
+ function estimateTokens(text) {
1070
+ if (!text) return 0;
1071
+ // 4 chars per token is conservative estimate
1072
+ // Actual tokenizers vary but this is a reasonable approximation
1073
+ return Math.ceil(text.length / 4);
1074
+ }
1075
+
1076
+ /**
1077
+ * Create a token budgeting helper for a model
1078
+ *
1079
+ * @param {number} contextLimit - Total context window
1080
+ * @param {number} reserveForResponse - Tokens to reserve for response (default: 20%)
1081
+ * @returns {Object} Budget helper functions
1082
+ */
1083
+ function createTokenBudget(contextLimit, reserveForResponse = null) {
1084
+ // Reserve 20% for response by default, minimum 1000 tokens
1085
+ const responseReserve = reserveForResponse || Math.max(1000, Math.floor(contextLimit * 0.2));
1086
+ const promptBudget = contextLimit - responseReserve;
1087
+
1088
+ return {
1089
+ contextLimit,
1090
+ responseReserve,
1091
+ promptBudget,
1092
+
1093
+ /**
1094
+ * Check if prompt fits within budget
1095
+ */
1096
+ fitsWithin(text) {
1097
+ return estimateTokens(text) <= promptBudget;
1098
+ },
1099
+
1100
+ /**
1101
+ * Get remaining budget after some text
1102
+ */
1103
+ remaining(text) {
1104
+ return promptBudget - estimateTokens(text);
1105
+ },
1106
+
1107
+ /**
1108
+ * Truncate text to fit budget with optional ellipsis
1109
+ */
1110
+ truncateToFit(text, targetTokens = promptBudget) {
1111
+ const currentTokens = estimateTokens(text);
1112
+ if (currentTokens <= targetTokens) return text;
1113
+
1114
+ // Truncate to approximate target (4 chars per token)
1115
+ const targetChars = targetTokens * 4;
1116
+ return text.substring(0, targetChars - 50) + '\n\n... (truncated to fit context window)';
1117
+ },
1118
+
1119
+ /**
1120
+ * Get usage summary
1121
+ */
1122
+ summarize(text) {
1123
+ const used = estimateTokens(text);
1124
+ const percent = Math.round((used / promptBudget) * 100);
1125
+ return {
1126
+ used,
1127
+ budget: promptBudget,
1128
+ remaining: promptBudget - used,
1129
+ percent,
1130
+ overBudget: used > promptBudget,
1131
+ contextLimit
1132
+ };
1133
+ }
1134
+ };
1135
+ }
1136
+
1137
+ /**
1138
+ * Initialize token budgeting for a hybrid session
1139
+ * Auto-detects context limit from provider
1140
+ *
1141
+ * @param {Object} config - Hybrid config from config.json
1142
+ * @returns {Promise<Object>} Token budget helper
1143
+ */
1144
+ async function initializeTokenBudget(config) {
1145
+ const {
1146
+ provider,
1147
+ providerEndpoint,
1148
+ model,
1149
+ maxContextTokens // Optional manual override
1150
+ } = config;
1151
+
1152
+ // Use manual override if provided
1153
+ if (maxContextTokens && maxContextTokens > 0) {
1154
+ console.log(`📊 Using configured context window: ${maxContextTokens.toLocaleString()} tokens`);
1155
+ return createTokenBudget(maxContextTokens);
1156
+ }
1157
+
1158
+ // Auto-detect from provider
1159
+ const contextLimit = await getModelContextLimit(provider, providerEndpoint, model);
1160
+ console.log(`📊 Detected context window: ${contextLimit.toLocaleString()} tokens`);
1161
+
1162
+ return createTokenBudget(contextLimit);
1163
+ }
1164
+
1165
+ // Module exports
1166
+ module.exports = {
1167
+ PROVIDER_TYPES,
1168
+ DEFAULT_CONFIGS,
1169
+ MODEL_CAPABILITIES,
1170
+ BaseProvider,
1171
+ OllamaProvider,
1172
+ LMStudioProvider,
1173
+ AnthropicProvider,
1174
+ OpenAIProvider,
1175
+ GoogleProvider,
1176
+ createProvider,
1177
+ createExecutorFromConfig,
1178
+ getExecutorConfig,
1179
+ listProviders,
1180
+ detectProviders,
1181
+ loadProviderFromConfig,
1182
+ detectModelCapabilities,
1183
+ getRecommendedModel,
1184
+
1185
+ // Token budgeting
1186
+ getModelContextLimit,
1187
+ estimateTokens,
1188
+ createTokenBudget,
1189
+ initializeTokenBudget
1190
+ };
1191
+
1192
+ /**
1193
+ * Format capability level with color
1194
+ */
1195
+ function formatCapability(level) {
1196
+ switch (level) {
1197
+ case 'excellent': return `${c.green}excellent${c.reset}`;
1198
+ case 'high': return `${c.green}high${c.reset}`;
1199
+ case 'medium': return `${c.yellow}medium${c.reset}`;
1200
+ case 'unknown': return `${c.dim}unknown${c.reset}`;
1201
+ default: return level;
1202
+ }
1203
+ }
1204
+
1205
+ // CLI Handler
1206
+ if (require.main === module) {
1207
+ const args = process.argv.slice(2);
1208
+ const command = args[0];
1209
+
1210
+ async function main() {
1211
+ switch (command) {
1212
+ case 'list': {
1213
+ const providers = listProviders();
1214
+ console.log(`\n${c.cyan}${c.bold}Available Providers${c.reset}\n`);
1215
+
1216
+ for (const p of providers) {
1217
+ const icon = p.local ? '🏠' : '☁️';
1218
+ const keyStatus = p.requiresKey
1219
+ ? (process.env[p.envVar] ? `${c.green}✓ API key set${c.reset}` : `${c.yellow}⚠ Requires ${p.envVar}${c.reset}`)
1220
+ : `${c.green}✓ No key required${c.reset}`;
1221
+
1222
+ console.log(`${icon} ${c.bold}${p.name}${c.reset} (${p.type})`);
1223
+ console.log(` ${keyStatus}`);
1224
+ console.log(` Endpoint: ${p.defaultEndpoint}`);
1225
+ console.log('');
1226
+ }
1227
+ break;
1228
+ }
1229
+
1230
+ case 'detect': {
1231
+ console.log(`\n${c.cyan}Detecting available providers...${c.reset}\n`);
1232
+ const available = await detectProviders();
1233
+
1234
+ if (available.length === 0) {
1235
+ console.log(`${c.yellow}No providers detected.${c.reset}`);
1236
+ console.log(`${c.dim}Make sure Ollama/LM Studio is running, or set API keys.${c.reset}`);
1237
+ } else {
1238
+ for (const p of available) {
1239
+ console.log(`${c.green}✅ ${p.name}${c.reset}`);
1240
+ if (p.models && p.models.length > 0) {
1241
+ console.log(` Models: ${p.models.map(m => m.id).join(', ')}`);
1242
+ }
1243
+ }
1244
+ }
1245
+ break;
1246
+ }
1247
+
1248
+ case 'test': {
1249
+ const providerType = args[1];
1250
+ if (!providerType) {
1251
+ console.error(`${c.red}Error: Provider type required${c.reset}`);
1252
+ console.log(`${c.dim}Usage: flow providers test <ollama|lm-studio|anthropic|openai>${c.reset}`);
1253
+ process.exit(1);
1254
+ }
1255
+
1256
+ console.log(`${c.cyan}Testing ${providerType}...${c.reset}`);
1257
+
1258
+ try {
1259
+ const provider = createProvider({ type: providerType });
1260
+ const result = await provider.test();
1261
+
1262
+ if (result.success) {
1263
+ console.log(`${c.green}✅ ${providerType} is working${c.reset}`);
1264
+ console.log(` Response: ${result.response.content.slice(0, 50)}...`);
1265
+ } else {
1266
+ console.log(`${c.red}❌ ${providerType} test failed${c.reset}`);
1267
+ console.log(` Error: ${result.error}`);
1268
+ }
1269
+ } catch (err) {
1270
+ console.log(`${c.red}❌ ${providerType} test failed${c.reset}`);
1271
+ console.log(` Error: ${err.message}`);
1272
+ }
1273
+ break;
1274
+ }
1275
+
1276
+ case 'capabilities': {
1277
+ const modelName = args[1];
1278
+
1279
+ if (modelName) {
1280
+ // Show capabilities for specific model
1281
+ const caps = detectModelCapabilities(modelName);
1282
+ console.log(`\n${c.cyan}${c.bold}Model Capabilities: ${modelName}${c.reset}\n`);
1283
+ console.log(` Code Quality: ${formatCapability(caps.codeQuality)}`);
1284
+ console.log(` Instruction Following: ${formatCapability(caps.instructionFollowing)}`);
1285
+ console.log(` Context Window: ${caps.contextWindow.toLocaleString()} tokens`);
1286
+ console.log(` Detection Source: ${caps.source}`);
1287
+ if (caps.matchedPattern) {
1288
+ console.log(` Matched Pattern: ${caps.matchedPattern}`);
1289
+ }
1290
+ } else {
1291
+ // List all known capabilities
1292
+ console.log(`\n${c.cyan}${c.bold}Known Model Capabilities${c.reset}\n`);
1293
+ console.log(`${c.dim}Pattern Code Quality Instructions Context${c.reset}`);
1294
+ console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
1295
+
1296
+ for (const [pattern, caps] of Object.entries(MODEL_CAPABILITIES)) {
1297
+ const cq = caps.codeQuality.padEnd(12);
1298
+ const inf = caps.instructionFollowing.padEnd(12);
1299
+ const ctx = caps.contextWindow.toLocaleString().padStart(10);
1300
+ console.log(` ${pattern.padEnd(16)} ${cq} ${inf} ${ctx}`);
1301
+ }
1302
+
1303
+ console.log('');
1304
+ console.log(`${c.dim}Use "flow providers capabilities <model-name>" to check a specific model${c.reset}`);
1305
+ }
1306
+ break;
1307
+ }
1308
+
1309
+ case 'recommend': {
1310
+ console.log(`\n${c.cyan}Finding best model for code tasks...${c.reset}\n`);
1311
+
1312
+ const available = await detectProviders();
1313
+
1314
+ if (available.length === 0) {
1315
+ console.log(`${c.yellow}No providers detected.${c.reset}`);
1316
+ process.exit(1);
1317
+ }
1318
+
1319
+ for (const provider of available) {
1320
+ if (provider.models && provider.models.length > 0) {
1321
+ const recommended = getRecommendedModel(provider.models, 'code');
1322
+ if (recommended) {
1323
+ console.log(`${c.green}${provider.name}:${c.reset} ${recommended.id || recommended.name}`);
1324
+ const caps = recommended.capabilities;
1325
+ console.log(` Code: ${caps.codeQuality} | Instructions: ${caps.instructionFollowing}`);
1326
+ console.log(` Context: ${caps.contextWindow.toLocaleString()} tokens | Score: ${recommended.score}`);
1327
+ console.log('');
1328
+ }
1329
+ }
1330
+ }
1331
+ break;
1332
+ }
1333
+
1334
+ default: {
1335
+ console.log(`
1336
+ ${c.cyan}Wogi Flow - Model Providers${c.reset}
1337
+
1338
+ ${c.bold}Usage:${c.reset}
1339
+ flow providers list List all available providers
1340
+ flow providers detect Detect running local providers
1341
+ flow providers test <type> Test a provider connection
1342
+ flow providers capabilities List known model capabilities
1343
+ flow providers capabilities <model> Show capabilities for a model
1344
+ flow providers recommend Find best model for code tasks
1345
+
1346
+ ${c.bold}Supported Providers:${c.reset}
1347
+ ollama Local Ollama instance
1348
+ lm-studio Local LM Studio instance
1349
+ anthropic Anthropic API (requires ANTHROPIC_API_KEY)
1350
+ openai OpenAI API (requires OPENAI_API_KEY)
1351
+
1352
+ ${c.bold}Model Capabilities:${c.reset}
1353
+ The system detects model capabilities using:
1354
+ 1. API queries (when available, e.g., Ollama /api/show)
1355
+ 2. Heuristics based on model name patterns
1356
+
1357
+ Capabilities tracked:
1358
+ - Code Quality: How well the model generates code
1359
+ - Instruction Following: How well it follows prompts
1360
+ - Context Window: Maximum tokens supported
1361
+
1362
+ ${c.bold}Configuration:${c.reset}
1363
+ Set in .workflow/config.json under "hybrid":
1364
+ {
1365
+ "hybrid": {
1366
+ "enabled": true,
1367
+ "provider": "anthropic",
1368
+ "model": "claude-sonnet-4-20250514",
1369
+ "apiKey": "sk-..." // or use environment variable
1370
+ }
1371
+ }
1372
+ `);
1373
+ }
1374
+ }
1375
+ }
1376
+
1377
+ main().catch(err => {
1378
+ console.error(`${c.red}Error: ${err.message}${c.reset}`);
1379
+ process.exit(1);
1380
+ });
1381
+ }