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.
- package/.workflow/agents/reviewer.md +81 -0
- package/.workflow/agents/security.md +94 -0
- package/.workflow/agents/story-writer.md +58 -0
- package/.workflow/bridges/base-bridge.js +395 -0
- package/.workflow/bridges/claude-bridge.js +434 -0
- package/.workflow/bridges/index.js +130 -0
- package/.workflow/lib/assumption-detector.js +481 -0
- package/.workflow/lib/config-substitution.js +371 -0
- package/.workflow/lib/failure-categories.js +478 -0
- package/.workflow/state/app-map.md.template +15 -0
- package/.workflow/state/architecture.md.template +24 -0
- package/.workflow/state/component-index.json.template +5 -0
- package/.workflow/state/decisions.md.template +15 -0
- package/.workflow/state/feedback-patterns.md.template +9 -0
- package/.workflow/state/knowledge-sync.json.template +6 -0
- package/.workflow/state/progress.md.template +14 -0
- package/.workflow/state/ready.json.template +7 -0
- package/.workflow/state/request-log.md.template +14 -0
- package/.workflow/state/session-state.json.template +11 -0
- package/.workflow/state/stack.md.template +33 -0
- package/.workflow/state/testing.md.template +36 -0
- package/.workflow/templates/claude-md.hbs +257 -0
- package/.workflow/templates/correction-report.md +67 -0
- package/.workflow/templates/gemini-md.hbs +52 -0
- package/README.md +1802 -0
- package/bin/flow +205 -0
- package/lib/index.js +33 -0
- package/lib/installer.js +467 -0
- package/lib/release-channel.js +269 -0
- package/lib/skill-registry.js +526 -0
- package/lib/upgrader.js +401 -0
- package/lib/utils.js +305 -0
- package/package.json +64 -0
- package/scripts/flow +985 -0
- package/scripts/flow-adaptive-learning.js +1259 -0
- package/scripts/flow-aggregate.js +488 -0
- package/scripts/flow-archive +133 -0
- package/scripts/flow-auto-context.js +1015 -0
- package/scripts/flow-auto-learn.js +615 -0
- package/scripts/flow-bridge.js +223 -0
- package/scripts/flow-browser-suggest.js +316 -0
- package/scripts/flow-bug.js +247 -0
- package/scripts/flow-cascade.js +711 -0
- package/scripts/flow-changelog +85 -0
- package/scripts/flow-checkpoint.js +483 -0
- package/scripts/flow-cli.js +403 -0
- package/scripts/flow-code-intelligence.js +760 -0
- package/scripts/flow-complexity.js +502 -0
- package/scripts/flow-config-set.js +152 -0
- package/scripts/flow-constants.js +157 -0
- package/scripts/flow-context +152 -0
- package/scripts/flow-context-init.js +482 -0
- package/scripts/flow-context-monitor.js +384 -0
- package/scripts/flow-context-scoring.js +886 -0
- package/scripts/flow-correct.js +458 -0
- package/scripts/flow-damage-control.js +985 -0
- package/scripts/flow-deps +101 -0
- package/scripts/flow-diff.js +700 -0
- package/scripts/flow-done +151 -0
- package/scripts/flow-done.js +489 -0
- package/scripts/flow-durable-session.js +1541 -0
- package/scripts/flow-entropy-monitor.js +345 -0
- package/scripts/flow-export-profile +349 -0
- package/scripts/flow-export-scanner.js +1046 -0
- package/scripts/flow-figma-confirm.js +400 -0
- package/scripts/flow-figma-extract.js +496 -0
- package/scripts/flow-figma-generate.js +683 -0
- package/scripts/flow-figma-index.js +909 -0
- package/scripts/flow-figma-match.js +617 -0
- package/scripts/flow-figma-mcp-server.js +518 -0
- package/scripts/flow-figma-pipeline.js +414 -0
- package/scripts/flow-file-ops.js +301 -0
- package/scripts/flow-gate-confidence.js +825 -0
- package/scripts/flow-guided-edit.js +659 -0
- package/scripts/flow-health +185 -0
- package/scripts/flow-health.js +413 -0
- package/scripts/flow-hooks.js +556 -0
- package/scripts/flow-http-client.js +249 -0
- package/scripts/flow-hybrid-detect.js +167 -0
- package/scripts/flow-hybrid-interactive.js +591 -0
- package/scripts/flow-hybrid-test.js +152 -0
- package/scripts/flow-import-profile +439 -0
- package/scripts/flow-init +253 -0
- package/scripts/flow-instruction-richness.js +827 -0
- package/scripts/flow-jira-integration.js +579 -0
- package/scripts/flow-knowledge-router.js +522 -0
- package/scripts/flow-knowledge-sync.js +589 -0
- package/scripts/flow-linear-integration.js +631 -0
- package/scripts/flow-links.js +774 -0
- package/scripts/flow-log-manager.js +559 -0
- package/scripts/flow-loop-enforcer.js +1246 -0
- package/scripts/flow-loop-retry-learning.js +630 -0
- package/scripts/flow-lsp.js +923 -0
- package/scripts/flow-map-index +348 -0
- package/scripts/flow-map-sync +201 -0
- package/scripts/flow-memory-blocks.js +668 -0
- package/scripts/flow-memory-compactor.js +350 -0
- package/scripts/flow-memory-db.js +1110 -0
- package/scripts/flow-memory-sync.js +484 -0
- package/scripts/flow-metrics.js +353 -0
- package/scripts/flow-migrate-ids.js +370 -0
- package/scripts/flow-model-adapter.js +802 -0
- package/scripts/flow-model-router.js +884 -0
- package/scripts/flow-models.js +1231 -0
- package/scripts/flow-morning.js +517 -0
- package/scripts/flow-multi-approach.js +660 -0
- package/scripts/flow-new-feature +86 -0
- package/scripts/flow-onboard +1042 -0
- package/scripts/flow-orchestrate-llm.js +459 -0
- package/scripts/flow-orchestrate.js +3592 -0
- package/scripts/flow-output.js +123 -0
- package/scripts/flow-parallel-detector.js +399 -0
- package/scripts/flow-parallel-dispatch.js +987 -0
- package/scripts/flow-parallel.js +428 -0
- package/scripts/flow-pattern-enforcer.js +600 -0
- package/scripts/flow-prd-manager.js +282 -0
- package/scripts/flow-progress.js +323 -0
- package/scripts/flow-project-analyzer.js +975 -0
- package/scripts/flow-prompt-composer.js +487 -0
- package/scripts/flow-providers.js +1381 -0
- package/scripts/flow-queue.js +308 -0
- package/scripts/flow-ready +82 -0
- package/scripts/flow-ready.js +189 -0
- package/scripts/flow-regression.js +396 -0
- package/scripts/flow-response-parser.js +450 -0
- package/scripts/flow-resume.js +284 -0
- package/scripts/flow-rules-sync.js +439 -0
- package/scripts/flow-run-trace.js +718 -0
- package/scripts/flow-safety.js +587 -0
- package/scripts/flow-search +104 -0
- package/scripts/flow-security.js +481 -0
- package/scripts/flow-session-end +106 -0
- package/scripts/flow-session-end.js +437 -0
- package/scripts/flow-session-state.js +671 -0
- package/scripts/flow-setup-hooks +216 -0
- package/scripts/flow-setup-hooks.js +377 -0
- package/scripts/flow-skill-create.js +329 -0
- package/scripts/flow-skill-creator.js +572 -0
- package/scripts/flow-skill-generator.js +1046 -0
- package/scripts/flow-skill-learn.js +880 -0
- package/scripts/flow-skill-matcher.js +578 -0
- package/scripts/flow-spec-generator.js +820 -0
- package/scripts/flow-stack-wizard.js +895 -0
- package/scripts/flow-standup +162 -0
- package/scripts/flow-start +74 -0
- package/scripts/flow-start.js +235 -0
- package/scripts/flow-status +110 -0
- package/scripts/flow-status.js +301 -0
- package/scripts/flow-step-browser.js +83 -0
- package/scripts/flow-step-changelog.js +217 -0
- package/scripts/flow-step-comments.js +306 -0
- package/scripts/flow-step-complexity.js +234 -0
- package/scripts/flow-step-coverage.js +218 -0
- package/scripts/flow-step-knowledge.js +193 -0
- package/scripts/flow-step-pr-tests.js +364 -0
- package/scripts/flow-step-regression.js +89 -0
- package/scripts/flow-step-review.js +516 -0
- package/scripts/flow-step-security.js +162 -0
- package/scripts/flow-step-silent-failures.js +290 -0
- package/scripts/flow-step-simplifier.js +346 -0
- package/scripts/flow-story +105 -0
- package/scripts/flow-story.js +500 -0
- package/scripts/flow-suspend.js +252 -0
- package/scripts/flow-sync-daemon.js +654 -0
- package/scripts/flow-task-analyzer.js +606 -0
- package/scripts/flow-team-dashboard.js +748 -0
- package/scripts/flow-team-sync.js +752 -0
- package/scripts/flow-team.js +977 -0
- package/scripts/flow-tech-options.js +528 -0
- package/scripts/flow-templates.js +812 -0
- package/scripts/flow-tiered-learning.js +728 -0
- package/scripts/flow-trace +204 -0
- package/scripts/flow-transcript-chunking.js +1106 -0
- package/scripts/flow-transcript-digest.js +7918 -0
- package/scripts/flow-transcript-language.js +465 -0
- package/scripts/flow-transcript-parsing.js +1085 -0
- package/scripts/flow-transcript-stories.js +2194 -0
- package/scripts/flow-update-map +224 -0
- package/scripts/flow-utils.js +2242 -0
- package/scripts/flow-verification.js +644 -0
- package/scripts/flow-verify.js +1177 -0
- package/scripts/flow-voice-input.js +638 -0
- package/scripts/flow-watch +168 -0
- package/scripts/flow-workflow-steps.js +521 -0
- package/scripts/flow-workflow.js +1029 -0
- package/scripts/flow-worktree.js +489 -0
- package/scripts/hooks/adapters/base-adapter.js +102 -0
- package/scripts/hooks/adapters/claude-code.js +359 -0
- package/scripts/hooks/adapters/index.js +79 -0
- package/scripts/hooks/core/component-check.js +341 -0
- package/scripts/hooks/core/index.js +35 -0
- package/scripts/hooks/core/loop-check.js +241 -0
- package/scripts/hooks/core/session-context.js +294 -0
- package/scripts/hooks/core/task-gate.js +177 -0
- package/scripts/hooks/core/validation.js +230 -0
- package/scripts/hooks/entry/claude-code/post-tool-use.js +65 -0
- package/scripts/hooks/entry/claude-code/pre-tool-use.js +89 -0
- package/scripts/hooks/entry/claude-code/session-end.js +87 -0
- package/scripts/hooks/entry/claude-code/session-start.js +46 -0
- package/scripts/hooks/entry/claude-code/stop.js +43 -0
- package/scripts/postinstall.js +139 -0
- package/templates/browser-test-flow.json +56 -0
- package/templates/bug-report.md +43 -0
- package/templates/component-detail.md +42 -0
- package/templates/component.stories.tsx +49 -0
- package/templates/context/constraints.md +83 -0
- package/templates/context/conventions.md +177 -0
- package/templates/context/stack.md +60 -0
- package/templates/correction-report.md +90 -0
- package/templates/feature-proposal.md +35 -0
- package/templates/hybrid/_base.md +254 -0
- package/templates/hybrid/_patterns.md +45 -0
- package/templates/hybrid/create-component.md +127 -0
- package/templates/hybrid/create-file.md +56 -0
- package/templates/hybrid/create-hook.md +145 -0
- package/templates/hybrid/create-service.md +70 -0
- package/templates/hybrid/fix-bug.md +33 -0
- package/templates/hybrid/modify-file.md +55 -0
- package/templates/story.md +68 -0
- package/templates/task.json +56 -0
- 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
|
+
}
|