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,1231 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow - Model Registry Commands
5
+ *
6
+ * Manages the multi-model registry and provides model selection,
7
+ * routing recommendations, and statistics viewing.
8
+ *
9
+ * Part of Phase 1: Model Infrastructure
10
+ *
11
+ * Usage:
12
+ * flow models Show current model and routing
13
+ * flow models list List all registered models
14
+ * flow models info <model> Show detailed model info
15
+ * flow models route <task-type> Show recommended model for task
16
+ * flow models stats Show model performance statistics
17
+ * flow models cost [--period] Show cost analysis
18
+ * flow models providers List available providers
19
+ */
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+ const {
24
+ PROJECT_ROOT,
25
+ parseFlags,
26
+ outputJson,
27
+ color,
28
+ error,
29
+ info,
30
+ warn,
31
+ getConfig,
32
+ fileExists,
33
+ dirExists,
34
+ safeJsonParse,
35
+ printHeader,
36
+ printSection
37
+ } = require('./flow-utils');
38
+
39
+ // Phase 2: Import task analyzer and model router
40
+ const { analyzeTask } = require('./flow-task-analyzer');
41
+ const { routeTask, ROUTING_STRATEGIES } = require('./flow-model-router');
42
+ const { composePrompt } = require('./flow-prompt-composer');
43
+
44
+ // Paths
45
+ const MODELS_DIR = path.join(PROJECT_ROOT, '.workflow', 'models');
46
+ const REGISTRY_PATH = path.join(MODELS_DIR, 'registry.json');
47
+ const STATS_PATH = path.join(MODELS_DIR, 'stats.json');
48
+
49
+ // ============================================================
50
+ // Constants (extracted magic numbers)
51
+ // ============================================================
52
+
53
+ const CONFIG = {
54
+ // Cost tier ordering for sorting
55
+ TIER_ORDER: { economy: 1, standard: 2, premium: 3 },
56
+ // Maximum recent tasks to keep in stats
57
+ MAX_RECENT_TASKS: 50,
58
+ // Minimum tasks before generating recommendations
59
+ MIN_TASKS_FOR_RECOMMENDATION: 5,
60
+ // Cost threshold for optimization recommendations
61
+ COST_OPTIMIZATION_THRESHOLD: 0.10,
62
+ // Estimated savings ratio when downgrading from premium to standard
63
+ DOWNGRADE_SAVINGS_RATIO: 0.6,
64
+ // Valid providers for input validation
65
+ VALID_PROVIDERS: ['anthropic', 'openai', 'google', 'ollama'],
66
+ // Valid capabilities for input validation
67
+ VALID_CAPABILITIES: ['code-gen', 'reasoning', 'analysis', 'structured-output', 'vision', 'extended-thinking'],
68
+ // Decimal places for cost display (consistent formatting)
69
+ COST_DECIMAL_PLACES: 4,
70
+ // Success rate thresholds for coloring
71
+ SUCCESS_RATE_HIGH: 90,
72
+ SUCCESS_RATE_MEDIUM: 70
73
+ };
74
+
75
+ // ============================================================
76
+ // Input Validation
77
+ // ============================================================
78
+
79
+ /**
80
+ * Validate provider filter value
81
+ * @param {string} provider - Provider name to validate
82
+ * @returns {string|null} Valid provider or null
83
+ */
84
+ function validateProvider(provider) {
85
+ if (!provider) return null;
86
+ const lower = provider.toLowerCase();
87
+ return CONFIG.VALID_PROVIDERS.includes(lower) ? lower : null;
88
+ }
89
+
90
+ /**
91
+ * Validate capability filter value
92
+ * @param {string} capability - Capability name to validate
93
+ * @returns {string|null} Valid capability or null
94
+ */
95
+ function validateCapability(capability) {
96
+ if (!capability) return null;
97
+ const lower = capability.toLowerCase();
98
+ return CONFIG.VALID_CAPABILITIES.includes(lower) ? lower : null;
99
+ }
100
+
101
+ // ============================================================
102
+ // Helper Functions (DRY extraction)
103
+ // ============================================================
104
+
105
+ /**
106
+ * Filter and sort models based on options
107
+ * @param {Array} models - Array of model objects
108
+ * @param {object} options - Filter/sort options
109
+ * @returns {Array} Filtered and sorted models
110
+ */
111
+ function filterAndSortModels(models, options = {}) {
112
+ let result = [...models];
113
+
114
+ // Filter by provider
115
+ if (options.provider) {
116
+ const validProvider = validateProvider(options.provider);
117
+ if (validProvider) {
118
+ result = result.filter(m => m.provider === validProvider);
119
+ }
120
+ }
121
+
122
+ // Filter by capability (with defensive null check)
123
+ if (options.capability) {
124
+ const validCapability = validateCapability(options.capability);
125
+ if (validCapability) {
126
+ result = result.filter(m => m.capabilities?.includes(validCapability) ?? false);
127
+ }
128
+ }
129
+
130
+ // Sort by cost tier
131
+ if (options.sortBy === 'cost') {
132
+ result.sort((a, b) =>
133
+ (CONFIG.TIER_ORDER[a.costTier] || 2) - (CONFIG.TIER_ORDER[b.costTier] || 2)
134
+ );
135
+ }
136
+
137
+ return result;
138
+ }
139
+
140
+ /**
141
+ * Calculate task cost based on model pricing
142
+ * @param {object} model - Model with pricing info
143
+ * @param {object} taskData - Task data with token counts
144
+ * @returns {number} Calculated cost
145
+ */
146
+ function calculateTaskCost(model, taskData) {
147
+ if (!model?.pricing || !taskData.tokensUsed) {
148
+ return 0;
149
+ }
150
+
151
+ const inputCost = (taskData.inputTokens || 0) / 1000 * model.pricing.inputPer1kTokens;
152
+ const outputCost = (taskData.outputTokens || 0) / 1000 * model.pricing.outputPer1kTokens;
153
+ return inputCost + outputCost;
154
+ }
155
+
156
+ // ============================================================
157
+ // Registry Loading
158
+ // ============================================================
159
+
160
+ /**
161
+ * Load the model registry with safety checks and validation
162
+ * @returns {Object|null} Validated registry data or null if invalid
163
+ */
164
+ function loadRegistry() {
165
+ if (!fileExists(REGISTRY_PATH)) {
166
+ return null;
167
+ }
168
+
169
+ const registry = safeJsonParse(REGISTRY_PATH);
170
+
171
+ // Validate registry structure
172
+ if (!registry || typeof registry !== 'object') {
173
+ return null;
174
+ }
175
+
176
+ // Ensure required top-level fields exist
177
+ if (!registry.version || !registry.models || typeof registry.models !== 'object') {
178
+ warn('Invalid registry structure: missing version or models');
179
+ return null;
180
+ }
181
+
182
+ return registry;
183
+ }
184
+
185
+ /**
186
+ * Load model statistics with safety checks
187
+ */
188
+ function loadStats() {
189
+ const defaultStats = {
190
+ version: '1.0.0',
191
+ lastUpdated: new Date().toISOString(),
192
+ trackingSince: new Date().toISOString(),
193
+ summary: {
194
+ totalTasks: 0,
195
+ totalTokensUsed: 0,
196
+ totalCost: 0
197
+ },
198
+ byModel: {},
199
+ byTaskType: {},
200
+ failureStats: {
201
+ totalFailures: 0,
202
+ byCategory: {}
203
+ },
204
+ routingStats: {
205
+ escalations: 0,
206
+ fallbacks: 0
207
+ },
208
+ recentTasks: []
209
+ };
210
+
211
+ if (!fileExists(STATS_PATH)) {
212
+ return defaultStats;
213
+ }
214
+
215
+ const parsed = safeJsonParse(STATS_PATH);
216
+ return parsed || defaultStats;
217
+ }
218
+
219
+ /**
220
+ * Save model statistics
221
+ */
222
+ function saveStats(stats) {
223
+ stats.lastUpdated = new Date().toISOString();
224
+
225
+ if (!dirExists(MODELS_DIR)) {
226
+ fs.mkdirSync(MODELS_DIR, { recursive: true });
227
+ }
228
+
229
+ fs.writeFileSync(STATS_PATH, JSON.stringify(stats, null, 2));
230
+ }
231
+
232
+ // ============================================================
233
+ // Model Information
234
+ // ============================================================
235
+
236
+ /**
237
+ * Get current active model from config
238
+ */
239
+ function getCurrentModel() {
240
+ const config = getConfig();
241
+ const registry = loadRegistry();
242
+
243
+ if (!registry) {
244
+ return { name: 'unknown', info: null };
245
+ }
246
+
247
+ // Check hybrid mode config
248
+ if (config.hybrid?.enabled && config.hybrid.executor?.model) {
249
+ const modelId = config.hybrid.executor.model;
250
+ return {
251
+ name: modelId,
252
+ info: registry.models[modelId] || null,
253
+ source: 'hybrid-config'
254
+ };
255
+ }
256
+
257
+ // Check environment (validate against registry)
258
+ if (process.env.CLAUDE_MODEL) {
259
+ const envModel = process.env.CLAUDE_MODEL;
260
+ // Security: Validate model ID format (alphanumeric, dash, dot, underscore only)
261
+ const SAFE_MODEL_ID_PATTERN = /^[a-zA-Z0-9._-]+$/;
262
+ if (!SAFE_MODEL_ID_PATTERN.test(envModel)) {
263
+ console.error(`[flow-models] CLAUDE_MODEL contains invalid characters, using default`);
264
+ } else if (registry.models && registry.models[envModel]) {
265
+ // Validate the environment variable against known models
266
+ return {
267
+ name: envModel,
268
+ info: registry.models[envModel],
269
+ source: 'environment'
270
+ };
271
+ } else {
272
+ // Warn about invalid environment variable but continue to default
273
+ console.error(`[flow-models] CLAUDE_MODEL="${envModel}" not found in registry, using default`);
274
+ }
275
+ }
276
+
277
+ // Use default from routing
278
+ const defaultModel = registry.routing?.default?.primary || 'claude-sonnet-4';
279
+ return {
280
+ name: defaultModel,
281
+ info: registry.models[defaultModel] || null,
282
+ source: 'default'
283
+ };
284
+ }
285
+
286
+ /**
287
+ * Get model by ID
288
+ */
289
+ function getModel(modelId) {
290
+ const registry = loadRegistry();
291
+ if (!registry) return null;
292
+
293
+ return registry.models[modelId] || null;
294
+ }
295
+
296
+ /**
297
+ * List all registered models
298
+ */
299
+ function listModels(options = {}) {
300
+ const registry = loadRegistry();
301
+ if (!registry) {
302
+ console.error(error('No model registry found. Run flow init to create one.'));
303
+ return [];
304
+ }
305
+
306
+ // Build model list
307
+ const models = Object.entries(registry.models).map(([id, model]) => ({
308
+ id,
309
+ displayName: model.displayName,
310
+ provider: model.provider,
311
+ contextWindow: model.contextWindow,
312
+ costTier: model.costTier,
313
+ capabilities: model.capabilities,
314
+ bestFor: model.bestFor
315
+ }));
316
+
317
+ // Use helper for filtering and sorting
318
+ return filterAndSortModels(models, options);
319
+ }
320
+
321
+ /**
322
+ * Get alternative models for fallback (DRY helper)
323
+ * @param {string|null} fallback - Primary fallback model
324
+ * @param {string|null} escalation - Escalation model
325
+ * @returns {string[]} List of valid alternative model IDs
326
+ */
327
+ function getAlternatives(fallback, escalation) {
328
+ return [fallback, escalation].filter(Boolean);
329
+ }
330
+
331
+ /**
332
+ * Get routing recommendation for a task type
333
+ */
334
+ function getRouteRecommendation(taskType, options = {}) {
335
+ const registry = loadRegistry();
336
+ if (!registry) return null;
337
+
338
+ const routing = registry.routing;
339
+ const stats = loadStats();
340
+ const defaultEscalation = routing.default?.escalation;
341
+
342
+ // Check task-type specific routing
343
+ const taskRouting = routing.byTaskType?.[taskType];
344
+ if (taskRouting) {
345
+ const modelId = taskRouting.primary;
346
+ const model = registry.models[modelId];
347
+
348
+ return {
349
+ recommended: modelId,
350
+ model: model,
351
+ reason: `Task type '${taskType}' routes to ${model?.displayName || modelId}`,
352
+ alternatives: getAlternatives(routing.default?.fallback, defaultEscalation),
353
+ stats: stats.byModel?.[modelId] || null
354
+ };
355
+ }
356
+
357
+ // Check language-specific routing
358
+ if (options.language && routing.byLanguage?.[options.language]) {
359
+ const langRouting = routing.byLanguage[options.language];
360
+ const modelId = langRouting.primary;
361
+ const model = registry.models[modelId];
362
+
363
+ return {
364
+ recommended: modelId,
365
+ model: model,
366
+ reason: `Language '${options.language}' routes to ${model?.displayName || modelId}`,
367
+ alternatives: getAlternatives(langRouting.fallback, defaultEscalation),
368
+ stats: stats.byModel?.[modelId] || null
369
+ };
370
+ }
371
+
372
+ // Use default routing
373
+ const defaultModel = routing.default.primary;
374
+ const model = registry.models[defaultModel];
375
+
376
+ return {
377
+ recommended: defaultModel,
378
+ model: model,
379
+ reason: 'Using default routing',
380
+ alternatives: getAlternatives(routing.default?.fallback, defaultEscalation),
381
+ stats: stats.byModel?.[defaultModel] || null
382
+ };
383
+ }
384
+
385
+ /**
386
+ * List available providers
387
+ */
388
+ function listProviders() {
389
+ const registry = loadRegistry();
390
+ if (!registry) return [];
391
+
392
+ return Object.entries(registry.providers).map(([id, provider]) => ({
393
+ id,
394
+ name: provider.name,
395
+ hasCliSupport: !!provider.cli,
396
+ cliId: provider.cli?.cliId || null,
397
+ supportedFeatures: provider.supportedFeatures
398
+ }));
399
+ }
400
+
401
+ // ============================================================
402
+ // Statistics & Analytics
403
+ // ============================================================
404
+
405
+ /**
406
+ * Record a task execution for statistics
407
+ */
408
+ function recordTaskExecution(modelId, taskData) {
409
+ const stats = loadStats();
410
+ const registry = loadRegistry();
411
+ const model = registry?.models[modelId];
412
+
413
+ // Warn if model not in registry (but still record)
414
+ if (!model) {
415
+ console.warn(`Warning: Model '${modelId}' not found in registry. Stats recorded without cost.`);
416
+ }
417
+
418
+ // Calculate cost FIRST using helper function (fixes cost tracking bug)
419
+ const taskCost = calculateTaskCost(model, taskData);
420
+ taskData.cost = taskCost;
421
+
422
+ // Update summary
423
+ stats.summary.totalTasks++;
424
+ stats.summary.totalTokensUsed += taskData.tokensUsed || 0;
425
+ stats.summary.totalCost += taskCost;
426
+
427
+ // Initialize model stats if needed
428
+ if (!stats.byModel[modelId]) {
429
+ stats.byModel[modelId] = {
430
+ totalTasks: 0,
431
+ successes: 0,
432
+ failures: 0,
433
+ totalTokens: 0,
434
+ totalCost: 0,
435
+ avgLatencyMs: 0,
436
+ byTaskType: {}
437
+ };
438
+ }
439
+
440
+ const modelStats = stats.byModel[modelId];
441
+ modelStats.totalTasks++;
442
+ modelStats.totalTokens += taskData.tokensUsed || 0;
443
+ modelStats.totalCost += taskCost;
444
+
445
+ if (taskData.success) {
446
+ modelStats.successes++;
447
+
448
+ // Phase 3: Record success in cascade tracker (resets failure count)
449
+ try {
450
+ const cascadeModule = require('./flow-cascade');
451
+ cascadeModule.recordSuccess({
452
+ modelId,
453
+ taskType: taskData.taskType || 'unknown'
454
+ });
455
+ } catch (err) {
456
+ // Cascade module not available - log only if not a "cannot find module" error
457
+ if (!err.code || err.code !== 'MODULE_NOT_FOUND') {
458
+ console.error('[flow-models] Cascade integration error:', err.message);
459
+ }
460
+ }
461
+
462
+ // Phase 3: Record success in tiered learning
463
+ try {
464
+ const tieredLearning = require('./flow-tiered-learning');
465
+ const patternId = `${modelId}:${taskData.taskType || 'unknown'}`;
466
+ tieredLearning.recordPatternResult({
467
+ patternId,
468
+ success: true,
469
+ context: taskData.description || taskData.title || ''
470
+ });
471
+ } catch (err) {
472
+ // Tiered learning module not available - log only if not a "cannot find module" error
473
+ if (!err.code || err.code !== 'MODULE_NOT_FOUND') {
474
+ console.error('[flow-models] Tiered learning integration error:', err.message);
475
+ }
476
+ }
477
+ } else {
478
+ modelStats.failures++;
479
+ stats.failureStats.totalFailures++;
480
+
481
+ if (taskData.errorCategory) {
482
+ stats.failureStats.byCategory[taskData.errorCategory] =
483
+ (stats.failureStats.byCategory[taskData.errorCategory] || 0) + 1;
484
+ }
485
+
486
+ // Phase 3: Record failure in cascade tracker
487
+ try {
488
+ const cascadeModule = require('./flow-cascade');
489
+ const cascadeResult = cascadeModule.recordFailure({
490
+ modelId,
491
+ taskType: taskData.taskType || 'unknown',
492
+ error: taskData.errorMessage || taskData.error || 'Unknown error',
493
+ category: taskData.errorCategory
494
+ });
495
+
496
+ // Add cascade info to task data for tracking
497
+ taskData.cascadeInfo = cascadeResult;
498
+ } catch (err) {
499
+ // Cascade module not available - log only if not a "cannot find module" error
500
+ if (!err.code || err.code !== 'MODULE_NOT_FOUND') {
501
+ console.error('[flow-models] Cascade integration error:', err.message);
502
+ }
503
+ }
504
+
505
+ // Phase 3: Record failure in tiered learning
506
+ try {
507
+ const tieredLearning = require('./flow-tiered-learning');
508
+ const patternId = `${modelId}:${taskData.taskType || 'unknown'}`;
509
+ tieredLearning.recordPatternResult({
510
+ patternId,
511
+ success: false,
512
+ context: taskData.errorMessage || taskData.error || ''
513
+ });
514
+ } catch (err) {
515
+ // Tiered learning module not available - log only if not a "cannot find module" error
516
+ if (!err.code || err.code !== 'MODULE_NOT_FOUND') {
517
+ console.error('[flow-models] Tiered learning integration error:', err.message);
518
+ }
519
+ }
520
+ }
521
+
522
+ // Track by task type
523
+ if (taskData.taskType) {
524
+ if (!stats.byTaskType[taskData.taskType]) {
525
+ stats.byTaskType[taskData.taskType] = {
526
+ total: 0,
527
+ success: 0,
528
+ avgTokens: 0,
529
+ totalCost: 0
530
+ };
531
+ }
532
+
533
+ const typeStats = stats.byTaskType[taskData.taskType];
534
+ typeStats.total++;
535
+ if (taskData.success) typeStats.success++;
536
+ typeStats.totalCost += taskData.cost || 0;
537
+
538
+ // Also track in model-specific task types
539
+ if (!modelStats.byTaskType[taskData.taskType]) {
540
+ modelStats.byTaskType[taskData.taskType] = { total: 0, success: 0 };
541
+ }
542
+ modelStats.byTaskType[taskData.taskType].total++;
543
+ if (taskData.success) modelStats.byTaskType[taskData.taskType].success++;
544
+ }
545
+
546
+ // Add to recent tasks (keep last 50)
547
+ stats.recentTasks.unshift({
548
+ timestamp: new Date().toISOString(),
549
+ model: modelId,
550
+ taskType: taskData.taskType,
551
+ success: taskData.success,
552
+ tokensUsed: taskData.tokensUsed,
553
+ cost: taskData.cost,
554
+ latencyMs: taskData.latencyMs
555
+ });
556
+ stats.recentTasks = stats.recentTasks.slice(0, CONFIG.MAX_RECENT_TASKS);
557
+
558
+ // Track routing events
559
+ if (taskData.wasEscalation) {
560
+ stats.routingStats.escalations++;
561
+ }
562
+ if (taskData.wasFallback) {
563
+ stats.routingStats.fallbacks++;
564
+ }
565
+
566
+ saveStats(stats);
567
+
568
+ return {
569
+ recorded: true,
570
+ cost: taskData.cost
571
+ };
572
+ }
573
+
574
+ /**
575
+ * Get cost analysis
576
+ */
577
+ function getCostAnalysis(options = {}) {
578
+ const stats = loadStats();
579
+ const registry = loadRegistry();
580
+
581
+ const analysis = {
582
+ totalCost: stats.summary.totalCost,
583
+ totalTasks: stats.summary.totalTasks,
584
+ avgCostPerTask: stats.summary.totalTasks > 0
585
+ ? stats.summary.totalCost / stats.summary.totalTasks
586
+ : 0,
587
+ byModel: {},
588
+ byTaskType: {},
589
+ recommendations: []
590
+ };
591
+
592
+ // Cost by model
593
+ for (const [modelId, modelStats] of Object.entries(stats.byModel)) {
594
+ const model = registry?.models[modelId];
595
+ analysis.byModel[modelId] = {
596
+ displayName: model?.displayName || modelId,
597
+ costTier: model?.costTier || 'unknown',
598
+ totalCost: modelStats.totalCost,
599
+ totalTasks: modelStats.totalTasks,
600
+ avgCostPerTask: modelStats.totalTasks > 0
601
+ ? modelStats.totalCost / modelStats.totalTasks
602
+ : 0
603
+ };
604
+ }
605
+
606
+ // Cost by task type
607
+ for (const [taskType, typeStats] of Object.entries(stats.byTaskType)) {
608
+ analysis.byTaskType[taskType] = {
609
+ totalCost: typeStats.totalCost,
610
+ totalTasks: typeStats.total,
611
+ avgCost: typeStats.total > 0 ? typeStats.totalCost / typeStats.total : 0
612
+ };
613
+ }
614
+
615
+ // Generate recommendations using CONFIG constants
616
+ for (const [modelId, modelData] of Object.entries(analysis.byModel)) {
617
+ if (modelData.costTier === 'premium' && modelData.totalTasks > CONFIG.MIN_TASKS_FOR_RECOMMENDATION) {
618
+ const avgCost = modelData.avgCostPerTask;
619
+ if (avgCost > CONFIG.COST_OPTIMIZATION_THRESHOLD) {
620
+ analysis.recommendations.push({
621
+ type: 'cost-optimization',
622
+ message: `Consider using Claude Sonnet for simpler tasks currently using ${modelData.displayName}`,
623
+ potentialSavings: avgCost * CONFIG.DOWNGRADE_SAVINGS_RATIO * modelData.totalTasks
624
+ });
625
+ }
626
+ }
627
+ }
628
+
629
+ return analysis;
630
+ }
631
+
632
+ /**
633
+ * Get model performance comparison
634
+ */
635
+ function getModelComparison() {
636
+ const stats = loadStats();
637
+ const registry = loadRegistry();
638
+
639
+ const comparison = [];
640
+
641
+ for (const [modelId, modelStats] of Object.entries(stats.byModel)) {
642
+ const model = registry?.models[modelId];
643
+ const successRate = modelStats.totalTasks > 0
644
+ ? (modelStats.successes / modelStats.totalTasks) * 100
645
+ : 0;
646
+
647
+ comparison.push({
648
+ id: modelId,
649
+ displayName: model?.displayName || modelId,
650
+ costTier: model?.costTier || 'unknown',
651
+ totalTasks: modelStats.totalTasks,
652
+ successRate: successRate.toFixed(1) + '%',
653
+ avgCost: modelStats.totalTasks > 0
654
+ ? (modelStats.totalCost / modelStats.totalTasks).toFixed(4)
655
+ : '0',
656
+ avgLatencyMs: modelStats.avgLatencyMs || 0,
657
+ topTaskTypes: Object.entries(modelStats.byTaskType)
658
+ .sort((a, b) => b[1].total - a[1].total)
659
+ .slice(0, 3)
660
+ .map(([type, data]) => `${type}(${data.total})`)
661
+ });
662
+ }
663
+
664
+ return comparison.sort((a, b) => b.totalTasks - a.totalTasks);
665
+ }
666
+
667
+ // ============================================================
668
+ // CLI Output Formatting
669
+ // ============================================================
670
+
671
+ /**
672
+ * Format section header as string (unlike printSection which logs directly)
673
+ */
674
+ function sectionHeader(title) {
675
+ return color('cyan', title);
676
+ }
677
+
678
+ /**
679
+ * Format main header as string (unlike printHeader which logs directly)
680
+ */
681
+ function headerString(title) {
682
+ const line = color('cyan', '='.repeat(50));
683
+ return `${line}\n${color('cyan', ` ${title}`)}\n${line}\n`;
684
+ }
685
+
686
+ function formatCurrentModel() {
687
+ const current = getCurrentModel();
688
+ const registry = loadRegistry();
689
+
690
+ let output = '';
691
+ output += headerString('Current Model');
692
+ output += '\n';
693
+
694
+ if (current.info) {
695
+ output += ` ${color('cyan', current.info.displayName)} (${current.name})\n`;
696
+ output += ` ${color('dim', 'Provider:')} ${current.info.provider}\n`;
697
+ output += ` ${color('dim', 'Source:')} ${current.source}\n`;
698
+ output += ` ${color('dim', 'Context:')} ${(current.info.contextWindow / 1000).toFixed(0)}K tokens\n`;
699
+ output += ` ${color('dim', 'Cost tier:')} ${current.info.costTier}\n`;
700
+
701
+ if (current.info.capabilities) {
702
+ output += ` ${color('dim', 'Capabilities:')} ${current.info.capabilities.join(', ')}\n`;
703
+ }
704
+ } else {
705
+ output += ` ${color('yellow', current.name)} (not in registry)\n`;
706
+ }
707
+
708
+ // Show routing info
709
+ output += '\n';
710
+ output += sectionHeader('Routing') + '\n';
711
+
712
+ const routing = registry?.routing?.default;
713
+ if (routing) {
714
+ output += ` ${color('dim', 'Primary:')} ${routing.primary}\n`;
715
+ output += ` ${color('dim', 'Fallback:')} ${routing.fallback}\n`;
716
+ output += ` ${color('dim', 'Escalation:')} ${routing.escalation}\n`;
717
+ }
718
+
719
+ return output;
720
+ }
721
+
722
+ function formatModelList(models) {
723
+ let output = '';
724
+ output += headerString('Registered Models');
725
+ output += '\n';
726
+
727
+ // Group by cost tier
728
+ const tiers = { premium: [], standard: [], economy: [] };
729
+
730
+ for (const model of models) {
731
+ const tier = model.costTier || 'standard';
732
+ if (tiers[tier]) {
733
+ tiers[tier].push(model);
734
+ }
735
+ }
736
+
737
+ for (const [tier, tierModels] of Object.entries(tiers)) {
738
+ if (tierModels.length === 0) continue;
739
+
740
+ const tierIcon = tier === 'premium' ? '*' : tier === 'economy' ? 'o' : '-';
741
+ output += `\n${color('cyan', `${tierIcon} ${tier.toUpperCase()}`)}\n`;
742
+
743
+ for (const model of tierModels) {
744
+ output += ` ${color('bold', model.displayName)} (${model.id})\n`;
745
+ output += ` ${color('dim', 'Provider:')} ${model.provider}\n`;
746
+ output += ` ${color('dim', 'Context:')} ${(model.contextWindow / 1000).toFixed(0)}K\n`;
747
+ output += ` ${color('dim', 'Best for:')} ${model.bestFor.join(', ')}\n`;
748
+ }
749
+ }
750
+
751
+ return output;
752
+ }
753
+
754
+ function formatModelInfo(modelId) {
755
+ const model = getModel(modelId);
756
+ const stats = loadStats();
757
+ const modelStats = stats.byModel?.[modelId];
758
+
759
+ let output = '';
760
+
761
+ if (!model) {
762
+ return error(`Model '${modelId}' not found in registry`);
763
+ }
764
+
765
+ output += headerString(model.displayName);
766
+ output += '\n';
767
+
768
+ output += sectionHeader('Configuration') + '\n';
769
+ output += ` ${color('dim', 'ID:')} ${modelId}\n`;
770
+ output += ` ${color('dim', 'Model ID:')} ${model.modelId}\n`;
771
+ output += ` ${color('dim', 'Provider:')} ${model.provider}\n`;
772
+ output += ` ${color('dim', 'Context window:')} ${model.contextWindow.toLocaleString()} tokens\n`;
773
+ output += ` ${color('dim', 'Max output:')} ${model.maxOutputTokens.toLocaleString()} tokens\n`;
774
+ output += ` ${color('dim', 'Cost tier:')} ${model.costTier}\n`;
775
+
776
+ output += '\n';
777
+ output += sectionHeader('Capabilities') + '\n';
778
+ for (const cap of model.capabilities) {
779
+ output += ` - ${cap}\n`;
780
+ }
781
+
782
+ output += '\n';
783
+ output += sectionHeader('Best For') + '\n';
784
+ for (const use of model.bestFor) {
785
+ output += ` - ${use}\n`;
786
+ }
787
+
788
+ output += '\n';
789
+ output += sectionHeader('Language Proficiency') + '\n';
790
+ const sortedLangs = Object.entries(model.languages)
791
+ .sort((a, b) => b[1] - a[1])
792
+ .slice(0, 5);
793
+ for (const [lang, score] of sortedLangs) {
794
+ const bar = '#'.repeat(score) + '.'.repeat(10 - score);
795
+ output += ` ${lang.padEnd(12)} ${bar} ${score}/10\n`;
796
+ }
797
+
798
+ output += '\n';
799
+ output += sectionHeader('Pricing') + '\n';
800
+ output += ` ${color('dim', 'Input:')} $${model.pricing.inputPer1kTokens}/1K tokens\n`;
801
+ output += ` ${color('dim', 'Output:')} $${model.pricing.outputPer1kTokens}/1K tokens\n`;
802
+
803
+ if (modelStats) {
804
+ output += '\n';
805
+ output += sectionHeader('Usage Statistics') + '\n';
806
+ const successRate = modelStats.totalTasks > 0
807
+ ? ((modelStats.successes / modelStats.totalTasks) * 100).toFixed(1)
808
+ : 0;
809
+ output += ` ${color('dim', 'Total tasks:')} ${modelStats.totalTasks}\n`;
810
+ output += ` ${color('dim', 'Success rate:')} ${successRate}%\n`;
811
+ output += ` ${color('dim', 'Total cost:')} $${modelStats.totalCost.toFixed(4)}\n`;
812
+ }
813
+
814
+ return output;
815
+ }
816
+
817
+ function formatRouteRecommendation(taskType, recommendation) {
818
+ let output = '';
819
+ output += headerString(`Routing: ${taskType}`);
820
+ output += '\n';
821
+
822
+ output += ` ${color('green', '->')} ${color('bold', recommendation.model?.displayName || recommendation.recommended)}\n`;
823
+ output += ` ${color('dim', recommendation.reason)}\n`;
824
+
825
+ if (recommendation.alternatives?.length > 0) {
826
+ output += `\n ${color('dim', 'Alternatives:')} ${recommendation.alternatives.join(', ')}\n`;
827
+ }
828
+
829
+ if (recommendation.stats) {
830
+ const successRate = recommendation.stats.totalTasks > 0
831
+ ? ((recommendation.stats.successes / recommendation.stats.totalTasks) * 100).toFixed(1)
832
+ : 'N/A';
833
+ output += `\n ${color('dim', 'Past performance:')} ${successRate}% success (${recommendation.stats.totalTasks} tasks)\n`;
834
+ }
835
+
836
+ return output;
837
+ }
838
+
839
+ function formatStats() {
840
+ const stats = loadStats();
841
+ const comparison = getModelComparison();
842
+
843
+ let output = '';
844
+ output += headerString('Model Statistics');
845
+ output += '\n';
846
+
847
+ output += sectionHeader('Summary') + '\n';
848
+ output += ` ${color('dim', 'Total tasks:')} ${stats.summary.totalTasks}\n`;
849
+ output += ` ${color('dim', 'Total tokens:')} ${stats.summary.totalTokensUsed.toLocaleString()}\n`;
850
+ output += ` ${color('dim', 'Total cost:')} $${stats.summary.totalCost.toFixed(4)}\n`;
851
+ output += ` ${color('dim', 'Escalations:')} ${stats.routingStats.escalations}\n`;
852
+ output += ` ${color('dim', 'Fallbacks:')} ${stats.routingStats.fallbacks}\n`;
853
+
854
+ if (comparison.length > 0) {
855
+ output += '\n';
856
+ output += sectionHeader('By Model') + '\n';
857
+
858
+ for (const model of comparison) {
859
+ const icon = parseFloat(model.successRate) >= CONFIG.SUCCESS_RATE_HIGH ? '+'
860
+ : parseFloat(model.successRate) >= CONFIG.SUCCESS_RATE_MEDIUM ? '~' : '-';
861
+ output += `\n ${color('cyan', icon)} ${color('bold', model.displayName)}\n`;
862
+ output += ` Tasks: ${model.totalTasks} | Success: ${model.successRate} | Avg cost: $${model.avgCost}\n`;
863
+ if (model.topTaskTypes.length > 0) {
864
+ output += ` Top types: ${model.topTaskTypes.join(', ')}\n`;
865
+ }
866
+ }
867
+ }
868
+
869
+ if (Object.keys(stats.failureStats.byCategory).length > 0) {
870
+ output += '\n';
871
+ output += sectionHeader('Failures by Category') + '\n';
872
+ for (const [category, count] of Object.entries(stats.failureStats.byCategory)) {
873
+ output += ` ${category}: ${count}\n`;
874
+ }
875
+ }
876
+
877
+ return output;
878
+ }
879
+
880
+ function formatCostAnalysis(analysis) {
881
+ let output = '';
882
+ output += headerString('Cost Analysis');
883
+ output += '\n';
884
+
885
+ output += sectionHeader('Overview') + '\n';
886
+ output += ` ${color('dim', 'Total spend:')} $${analysis.totalCost.toFixed(4)}\n`;
887
+ output += ` ${color('dim', 'Total tasks:')} ${analysis.totalTasks}\n`;
888
+ output += ` ${color('dim', 'Avg per task:')} $${analysis.avgCostPerTask.toFixed(4)}\n`;
889
+
890
+ if (Object.keys(analysis.byModel).length > 0) {
891
+ output += '\n';
892
+ output += sectionHeader('Cost by Model') + '\n';
893
+
894
+ const sortedModels = Object.entries(analysis.byModel)
895
+ .sort((a, b) => b[1].totalCost - a[1].totalCost);
896
+
897
+ for (const [modelId, data] of sortedModels) {
898
+ output += ` ${color('cyan', data.displayName)} (${data.costTier})\n`;
899
+ output += ` Total: $${data.totalCost.toFixed(4)} | Tasks: ${data.totalTasks} | Avg: $${data.avgCostPerTask.toFixed(4)}\n`;
900
+ }
901
+ }
902
+
903
+ if (analysis.recommendations.length > 0) {
904
+ output += '\n';
905
+ output += sectionHeader('Recommendations') + '\n';
906
+ for (const rec of analysis.recommendations) {
907
+ output += ` ${color('yellow', '->')} ${rec.message}\n`;
908
+ if (rec.potentialSavings) {
909
+ output += ` ${color('green', `Potential savings: $${rec.potentialSavings.toFixed(2)}`)}\n`;
910
+ }
911
+ }
912
+ }
913
+
914
+ return output;
915
+ }
916
+
917
+ /**
918
+ * Format recommendation output (Phase 2)
919
+ * @param {string} taskDesc - Task description
920
+ * @param {Object} analysis - Task analysis result
921
+ * @param {Object} routing - Routing decision
922
+ * @param {Object|null} promptPreview - Optional prompt preview
923
+ */
924
+ function formatRecommendation(taskDesc, analysis, routing, promptPreview) {
925
+ printHeader('MODEL RECOMMENDATION');
926
+
927
+ // Task
928
+ printSection('Task');
929
+ console.log(` "${taskDesc}"`);
930
+
931
+ // Analysis
932
+ printSection('Analysis');
933
+ const complexityColor = {
934
+ low: 'green',
935
+ medium: 'yellow',
936
+ high: 'red'
937
+ }[analysis.complexity.level];
938
+ console.log(` Complexity: ${color(complexityColor, analysis.complexity.level.toUpperCase())}`);
939
+ console.log(` Domain: ${color('cyan', analysis.domains.primary)}`);
940
+ console.log(` Language: ${analysis.languages.primary}`);
941
+ console.log(` Capabilities: ${analysis.capabilities.join(', ')}`);
942
+ console.log(` Est. tokens: ~${analysis.tokens.estimated.total.toLocaleString()}`);
943
+
944
+ // Routing
945
+ printSection('Routing');
946
+ console.log(` Strategy: ${color('cyan', routing.strategy)}`);
947
+ if (ROUTING_STRATEGIES[routing.strategy]) {
948
+ console.log(` (${ROUTING_STRATEGIES[routing.strategy]})`);
949
+ }
950
+
951
+ // Primary Model
952
+ if (routing.primary) {
953
+ printSection('Recommended Model');
954
+ console.log(` ${color('green', routing.primary.displayName)}`);
955
+ console.log(` Provider: ${routing.primary.provider}`);
956
+ console.log(` Tier: ${routing.primary.costTier}`);
957
+ console.log(` Score: ${routing.primary.scores.total.toFixed(1)}/100`);
958
+ for (const reason of routing.primary.reasons.slice(0, 2)) {
959
+ console.log(` - ${reason}`);
960
+ }
961
+ }
962
+
963
+ // Fallback
964
+ if (routing.fallback) {
965
+ printSection('Fallback');
966
+ console.log(` ${color('yellow', routing.fallback.displayName)} (${routing.fallback.provider})`);
967
+ }
968
+
969
+ // Escalation
970
+ if (routing.escalation) {
971
+ printSection('Escalation');
972
+ console.log(` ${color('cyan', routing.escalation.displayName)} (${routing.escalation.costTier} tier)`);
973
+ }
974
+
975
+ // Prompt preview
976
+ if (promptPreview) {
977
+ printSection('Prompt');
978
+ console.log(` Fragments: ${promptPreview.fragments}`);
979
+ console.log(` Est. tokens: ~${promptPreview.tokenEstimate.toLocaleString()}`);
980
+ }
981
+
982
+ // Warning
983
+ if (routing.warning) {
984
+ console.log('');
985
+ console.log(warn(routing.warning));
986
+ }
987
+
988
+ console.log('');
989
+ }
990
+
991
+ function formatProviders(providers) {
992
+ let output = '';
993
+ output += headerString('Available Providers');
994
+ output += '\n';
995
+
996
+ for (const provider of providers) {
997
+ const cliIcon = provider.hasCliSupport ? color('green', '+') : color('dim', 'o');
998
+ output += ` ${cliIcon} ${color('bold', provider.name)} (${provider.id})\n`;
999
+
1000
+ if (provider.hasCliSupport) {
1001
+ output += ` ${color('dim', 'CLI:')} ${provider.cliId}\n`;
1002
+ }
1003
+
1004
+ if (provider.supportedFeatures) {
1005
+ output += ` ${color('dim', 'Features:')} ${provider.supportedFeatures.join(', ')}\n`;
1006
+ }
1007
+ }
1008
+
1009
+ return output;
1010
+ }
1011
+
1012
+ // ============================================================
1013
+ // CLI Entry Point
1014
+ // ============================================================
1015
+
1016
+ function showHelp() {
1017
+ console.log(`
1018
+ Wogi Flow - Model Registry
1019
+
1020
+ Manage multi-model configuration, routing, and statistics.
1021
+
1022
+ Usage:
1023
+ flow models Show current model and routing
1024
+ flow models list [options] List all registered models
1025
+ flow models info <model> Show detailed model information
1026
+ flow models route <task-type> Get routing recommendation
1027
+ flow models stats Show model performance statistics
1028
+ flow models cost Show cost analysis
1029
+ flow models providers List available providers
1030
+
1031
+ Options:
1032
+ --provider <name> Filter by provider (anthropic, openai, google, ollama)
1033
+ --capability <name> Filter by capability (code-gen, reasoning, vision, etc.)
1034
+ --json Output as JSON
1035
+ --help, -h Show this help
1036
+
1037
+ Examples:
1038
+ flow models # Show current model
1039
+ flow models list --provider anthropic # List Anthropic models
1040
+ flow models info claude-sonnet-4 # Show Sonnet details
1041
+ flow models route feature # Get model for feature work
1042
+ flow models stats # View statistics
1043
+ flow models cost # Analyze costs
1044
+ `);
1045
+ }
1046
+
1047
+ function main() {
1048
+ const args = process.argv.slice(2);
1049
+ const { flags } = parseFlags(args);
1050
+ const command = args.find(a => !a.startsWith('--')) || '';
1051
+
1052
+ if (flags.help || flags.h) {
1053
+ showHelp();
1054
+ process.exit(0);
1055
+ }
1056
+
1057
+ // Check registry exists
1058
+ if (!fileExists(REGISTRY_PATH)) {
1059
+ console.log(error('No model registry found at .workflow/models/registry.json'));
1060
+ console.log(info('Run `flow init` to create one or check your workflow setup.'));
1061
+ process.exit(1);
1062
+ }
1063
+
1064
+ switch (command) {
1065
+ case '':
1066
+ case 'current':
1067
+ if (flags.json) {
1068
+ outputJson(getCurrentModel());
1069
+ } else {
1070
+ console.log(formatCurrentModel());
1071
+ }
1072
+ break;
1073
+
1074
+ case 'list':
1075
+ case 'ls':
1076
+ const models = listModels({
1077
+ provider: flags.provider,
1078
+ capability: flags.capability,
1079
+ sortBy: flags.sort
1080
+ });
1081
+ if (flags.json) {
1082
+ outputJson(models);
1083
+ } else {
1084
+ console.log(formatModelList(models));
1085
+ }
1086
+ break;
1087
+
1088
+ case 'info':
1089
+ const modelId = args.find(a => !a.startsWith('--') && a !== 'info');
1090
+ if (!modelId) {
1091
+ console.log(error('Please specify a model ID. Use `flow models list` to see available models.'));
1092
+ process.exit(1);
1093
+ }
1094
+ if (flags.json) {
1095
+ outputJson(getModel(modelId));
1096
+ } else {
1097
+ console.log(formatModelInfo(modelId));
1098
+ }
1099
+ break;
1100
+
1101
+ case 'route':
1102
+ const taskType = args.find(a => !a.startsWith('--') && a !== 'route');
1103
+ if (!taskType) {
1104
+ console.log(error('Please specify a task type (feature, bugfix, refactor, etc.)'));
1105
+ process.exit(1);
1106
+ }
1107
+ const recommendation = getRouteRecommendation(taskType, { language: flags.language });
1108
+ if (flags.json) {
1109
+ outputJson(recommendation);
1110
+ } else {
1111
+ console.log(formatRouteRecommendation(taskType, recommendation));
1112
+ }
1113
+ break;
1114
+
1115
+ case 'stats':
1116
+ case 'statistics':
1117
+ if (flags.json) {
1118
+ outputJson({
1119
+ stats: loadStats(),
1120
+ comparison: getModelComparison()
1121
+ });
1122
+ } else {
1123
+ console.log(formatStats());
1124
+ }
1125
+ break;
1126
+
1127
+ case 'cost':
1128
+ case 'costs':
1129
+ const analysis = getCostAnalysis({ period: flags.period });
1130
+ if (flags.json) {
1131
+ outputJson(analysis);
1132
+ } else {
1133
+ console.log(formatCostAnalysis(analysis));
1134
+ }
1135
+ break;
1136
+
1137
+ case 'recommend':
1138
+ case 'analyze':
1139
+ // Phase 2: Full task analysis and model recommendation
1140
+ const taskDesc = args.filter(a => !a.startsWith('--') && a !== 'recommend' && a !== 'analyze').join(' ');
1141
+ if (!taskDesc) {
1142
+ console.log(error('Please provide a task description.'));
1143
+ console.log(info('Usage: flow models recommend "Add user authentication"'));
1144
+ process.exit(1);
1145
+ }
1146
+
1147
+ // Analyze task
1148
+ const taskAnalysis = analyzeTask({
1149
+ title: taskDesc,
1150
+ type: flags.type || 'feature'
1151
+ });
1152
+
1153
+ // Route to model
1154
+ const routeDecision = routeTask({
1155
+ analysis: taskAnalysis,
1156
+ strategy: flags.strategy || 'quality-first'
1157
+ });
1158
+
1159
+ // Compose prompt preview (optional)
1160
+ let promptPreview = null;
1161
+ if (flags['with-prompt']) {
1162
+ const composed = composePrompt({
1163
+ model: routeDecision.primary?.modelId,
1164
+ domain: taskAnalysis.domains.primary,
1165
+ taskType: taskAnalysis.taskType
1166
+ });
1167
+ promptPreview = {
1168
+ fragments: composed.fragmentCount,
1169
+ tokenEstimate: composed.tokenEstimate
1170
+ };
1171
+ }
1172
+
1173
+ if (flags.json) {
1174
+ outputJson({
1175
+ success: true,
1176
+ task: taskDesc,
1177
+ analysis: taskAnalysis,
1178
+ routing: routeDecision,
1179
+ promptPreview
1180
+ });
1181
+ } else {
1182
+ formatRecommendation(taskDesc, taskAnalysis, routeDecision, promptPreview);
1183
+ }
1184
+ break;
1185
+
1186
+ case 'providers':
1187
+ const providers = listProviders();
1188
+ if (flags.json) {
1189
+ outputJson(providers);
1190
+ } else {
1191
+ console.log(formatProviders(providers));
1192
+ }
1193
+ break;
1194
+
1195
+ default:
1196
+ // Check if it's a model ID
1197
+ if (getModel(command)) {
1198
+ if (flags.json) {
1199
+ outputJson(getModel(command));
1200
+ } else {
1201
+ console.log(formatModelInfo(command));
1202
+ }
1203
+ } else {
1204
+ console.log(error(`Unknown command: ${command}`));
1205
+ showHelp();
1206
+ process.exit(1);
1207
+ }
1208
+ }
1209
+ }
1210
+
1211
+ // ============================================================
1212
+ // Exports
1213
+ // ============================================================
1214
+
1215
+ module.exports = {
1216
+ loadRegistry,
1217
+ loadStats,
1218
+ saveStats,
1219
+ getCurrentModel,
1220
+ getModel,
1221
+ listModels,
1222
+ getRouteRecommendation,
1223
+ listProviders,
1224
+ recordTaskExecution,
1225
+ getCostAnalysis,
1226
+ getModelComparison
1227
+ };
1228
+
1229
+ if (require.main === module) {
1230
+ main();
1231
+ }