wogiflow 1.0.31 → 1.0.32

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.
@@ -14,14 +14,40 @@ Let me check what's available on your system:
14
14
  node scripts/flow-hybrid-detect.js providers
15
15
  ```
16
16
 
17
- ## Step 2: Interactive Setup
17
+ ## Step 2: Choose Setup Method
18
18
 
19
- Running the interactive setup wizard:
19
+ ### Option A: Use Unified Model Setup (Recommended)
20
+
21
+ If you want to configure multiple providers at once (for both hybrid and peer review):
22
+
23
+ ```
24
+ /wogi-models-setup
25
+ ```
26
+
27
+ This configures all your models in one place.
28
+
29
+ ### Option B: Hybrid-Specific Setup
30
+
31
+ For hybrid-specific configuration only:
20
32
 
21
33
  ```bash
22
34
  node scripts/flow-hybrid-interactive.js
23
35
  ```
24
36
 
37
+ ### Model Selection
38
+
39
+ If models are already configured (via `/wogi-models-setup`), you can select which one to use:
40
+
41
+ ```javascript
42
+ const modelConfig = require('./scripts/flow-model-config');
43
+ const models = modelConfig.getEnabledModels();
44
+
45
+ // Show selection if multiple models available
46
+ if (models.length > 1) {
47
+ // Use AskUserQuestion to let user select
48
+ }
49
+ ```
50
+
25
51
  ## How Hybrid Mode Works
26
52
 
27
53
  1. **You give me a task** - "Add user authentication"
@@ -0,0 +1,254 @@
1
+ Configure external models for WogiFlow features (peer review, hybrid mode).
2
+
3
+ ## Overview
4
+
5
+ This wizard helps you set up external LLM providers:
6
+ - **OpenAI** (GPT-4o, o1)
7
+ - **Google** (Gemini)
8
+ - **Anthropic** (Claude - for peer review)
9
+ - **Local LLM** (Ollama, LM Studio)
10
+
11
+ Configured models are shared between:
12
+ - `/wogi-peer-review` - Multi-model code review
13
+ - `/wogi-hybrid` - Local/cloud execution mode
14
+
15
+ ## Setup Flow
16
+
17
+ ### Step 1: Provider Selection
18
+
19
+ Use AskUserQuestion to let user select which providers to configure:
20
+
21
+ ```javascript
22
+ {
23
+ question: "Which AI providers do you want to configure?",
24
+ header: "Providers",
25
+ multiSelect: true,
26
+ options: [
27
+ { label: "OpenAI", description: "GPT-4o, GPT-4o-mini, o1 models" },
28
+ { label: "Google (Gemini)", description: "Gemini 2.0 Flash, Gemini Pro" },
29
+ { label: "Anthropic", description: "Claude models (for peer review comparison)" },
30
+ { label: "Local LLM", description: "Ollama or LM Studio (free, runs on your machine)" }
31
+ ]
32
+ }
33
+ ```
34
+
35
+ ### Step 2: Configure Each Selected Provider
36
+
37
+ For each selected provider, run the appropriate setup:
38
+
39
+ #### OpenAI Setup
40
+
41
+ 1. Ask for API key:
42
+ ```
43
+ Please paste your OpenAI API key (starts with sk-):
44
+ ```
45
+
46
+ 2. Test connection:
47
+ ```bash
48
+ node -e "require('./scripts/flow-model-config').testProviderConnection('openai').then(r => console.log(JSON.stringify(r)))"
49
+ ```
50
+
51
+ 3. If successful, ask which models to enable:
52
+ ```javascript
53
+ {
54
+ question: "Which OpenAI models do you want to enable?",
55
+ header: "OpenAI Models",
56
+ multiSelect: true,
57
+ options: [
58
+ { label: "gpt-4o (Recommended)", description: "Best quality, good speed" },
59
+ { label: "gpt-4o-mini", description: "Faster, cheaper, still capable" },
60
+ { label: "o1-mini", description: "Advanced reasoning, slower" }
61
+ ]
62
+ }
63
+ ```
64
+
65
+ 4. Save configuration:
66
+ ```javascript
67
+ const modelConfig = require('./scripts/flow-model-config');
68
+ modelConfig.addProvider('openai', {
69
+ apiKey: userProvidedKey,
70
+ models: selectedModels
71
+ });
72
+ ```
73
+
74
+ #### Google (Gemini) Setup
75
+
76
+ 1. Ask for API key:
77
+ ```
78
+ Please paste your Google AI API key (get one at https://aistudio.google.com/apikey):
79
+ ```
80
+
81
+ 2. Test and select models (similar to OpenAI):
82
+ - gemini-2.0-flash-exp (Recommended)
83
+ - gemini-1.5-flash
84
+ - gemini-1.5-pro
85
+
86
+ #### Anthropic Setup
87
+
88
+ 1. Ask for API key:
89
+ ```
90
+ Please paste your Anthropic API key (starts with sk-ant-):
91
+ ```
92
+
93
+ 2. Select models:
94
+ - claude-sonnet-4 (Recommended)
95
+ - claude-3-5-haiku
96
+ - claude-opus-4
97
+
98
+ Note: Anthropic models are mainly useful for peer review to get different perspectives.
99
+
100
+ #### Local LLM Setup
101
+
102
+ 1. Auto-detect local providers:
103
+ ```bash
104
+ # Test Ollama
105
+ curl -s http://localhost:11434/api/tags 2>/dev/null | head -1
106
+
107
+ # Test LM Studio
108
+ curl -s http://localhost:1234/v1/models 2>/dev/null | head -1
109
+ ```
110
+
111
+ 2. If detected, list available models and let user select.
112
+
113
+ 3. If not detected, show instructions:
114
+ ```
115
+ No local LLM detected. To use local models:
116
+
117
+ Option 1: Install Ollama
118
+ 1. Visit https://ollama.ai
119
+ 2. Install and run: ollama pull qwen2.5-coder:7b
120
+ 3. Re-run /wogi-models-setup
121
+
122
+ Option 2: Install LM Studio
123
+ 1. Visit https://lmstudio.ai
124
+ 2. Download and load a model
125
+ 3. Start the server
126
+ 4. Re-run /wogi-models-setup
127
+ ```
128
+
129
+ ### Step 3: Set Defaults (Optional)
130
+
131
+ Ask user about default preferences:
132
+
133
+ ```javascript
134
+ {
135
+ question: "Which models should be used by default for peer review?",
136
+ header: "Peer Review Default",
137
+ multiSelect: true,
138
+ options: [
139
+ // Show only configured models
140
+ { label: "openai:gpt-4o", description: "..." },
141
+ { label: "google:gemini-2.0-flash", description: "..." }
142
+ ]
143
+ }
144
+ ```
145
+
146
+ ### Step 4: Summary
147
+
148
+ Display configuration summary:
149
+
150
+ ```
151
+ ╔══════════════════════════════════════════════════════════╗
152
+ ║ Model Configuration Complete ║
153
+ ╚══════════════════════════════════════════════════════════╝
154
+
155
+ Configured Providers:
156
+ ✓ OpenAI: gpt-4o, gpt-4o-mini
157
+ ✓ Google: gemini-2.0-flash
158
+ ✓ Local: qwen2.5-coder (via Ollama)
159
+
160
+ API Keys stored in: .env
161
+ Config saved to: .workflow/config.json
162
+
163
+ Default for peer review: gpt-4o, gemini-2.0-flash
164
+ Default for hybrid mode: local:qwen2.5-coder
165
+
166
+ You can now use:
167
+ /wogi-review - Multi-model code review
168
+ /wogi-hybrid - Hybrid execution mode
169
+ /wogi-peer-review - Same as /wogi-review
170
+ ```
171
+
172
+ ## Implementation Details
173
+
174
+ ### Config Storage
175
+
176
+ API keys are stored as environment variable names (not values) in config:
177
+ ```json
178
+ {
179
+ "models": {
180
+ "providers": {
181
+ "openai": {
182
+ "apiKeyEnv": "OPENAI_API_KEY",
183
+ "enabled": true,
184
+ "models": ["gpt-4o", "gpt-4o-mini"]
185
+ }
186
+ }
187
+ }
188
+ }
189
+ ```
190
+
191
+ Actual keys go in `.env`:
192
+ ```
193
+ OPENAI_API_KEY=sk-proj-...
194
+ GOOGLE_API_KEY=AIza...
195
+ ```
196
+
197
+ ### Migration
198
+
199
+ If old config exists (hybrid.executor or peerReview.apiKeys), migrate automatically:
200
+ ```javascript
201
+ const modelConfig = require('./scripts/flow-model-config');
202
+ modelConfig.migrateOldConfig();
203
+ ```
204
+
205
+ ### Testing
206
+
207
+ Test any provider connection:
208
+ ```bash
209
+ node scripts/flow-model-config.js test openai
210
+ node scripts/flow-model-config.js test local
211
+ ```
212
+
213
+ ## Error Handling
214
+
215
+ ### Invalid API Key
216
+
217
+ ```
218
+ ✗ OpenAI connection failed: Invalid API key
219
+
220
+ Please check your API key and try again.
221
+ You can get a new key at: https://platform.openai.com/api-keys
222
+ ```
223
+
224
+ ### No Local LLM
225
+
226
+ ```
227
+ ✗ No local LLM detected
228
+
229
+ Install Ollama (recommended): https://ollama.ai
230
+ Then run: ollama pull qwen2.5-coder:7b
231
+ ```
232
+
233
+ ### Network Error
234
+
235
+ ```
236
+ ✗ Connection failed: Network error
237
+
238
+ Please check your internet connection and try again.
239
+ ```
240
+
241
+ ## Quick Setup (Non-Interactive)
242
+
243
+ For users who already have API keys set in environment:
244
+
245
+ ```bash
246
+ # If OPENAI_API_KEY is already in environment
247
+ node -e "
248
+ const mc = require('./scripts/flow-model-config');
249
+ if (process.env.OPENAI_API_KEY) {
250
+ mc.addProvider('openai', { models: ['gpt-4o'] });
251
+ console.log('OpenAI configured');
252
+ }
253
+ "
254
+ ```
@@ -1,5 +1,74 @@
1
1
  Run a multi-model peer review where different AI models review the same code.
2
2
 
3
+ ## Step 0: Model Selection (Every Run)
4
+
5
+ **Before starting the review, check for configured models and let user select:**
6
+
7
+ ### Check Configuration
8
+
9
+ ```javascript
10
+ const modelConfig = require('./scripts/flow-model-config');
11
+
12
+ // Run migration if needed (handles old config formats)
13
+ modelConfig.migrateOldConfig();
14
+
15
+ // Get enabled models
16
+ const models = modelConfig.getEnabledModels();
17
+ ```
18
+
19
+ ### If No Models Configured
20
+
21
+ If `models.length === 0`:
22
+
23
+ ```
24
+ No external models configured for peer review.
25
+
26
+ Run /wogi-models-setup to configure:
27
+ - OpenAI (GPT-4o)
28
+ - Google (Gemini)
29
+ - Local LLM (Ollama)
30
+
31
+ Or use --manual flag for manual mode.
32
+ ```
33
+
34
+ Then either:
35
+ - Auto-launch `/wogi-models-setup` wizard
36
+ - Or use `--manual` mode if user prefers
37
+
38
+ ### Model Selection Dialog
39
+
40
+ If models are configured, show selection dialog using AskUserQuestion:
41
+
42
+ ```javascript
43
+ {
44
+ question: "Select models for peer review (multiple allowed):",
45
+ header: "Models",
46
+ multiSelect: true,
47
+ options: [
48
+ // Dynamically populated from configured models
49
+ { label: "openai:gpt-4o", description: "Best quality reasoning" },
50
+ { label: "openai:gpt-4o-mini", description: "Faster, cheaper" },
51
+ { label: "google:gemini-2.0-flash", description: "Fast, good at code" },
52
+ { label: "local:qwen2.5-coder", description: "Free, runs locally" }
53
+ // ... other configured models
54
+ ]
55
+ }
56
+ ```
57
+
58
+ **Show only models that:**
59
+ 1. Are configured in `models.providers`
60
+ 2. Have `enabled: true`
61
+ 3. Have API key set (check `process.env[apiKeyEnv]`) or are local
62
+
63
+ ### After Selection
64
+
65
+ Save the selection for future runs (optional):
66
+ ```javascript
67
+ modelConfig.setDefaultModels('peerReview', selectedModels);
68
+ ```
69
+
70
+ Then proceed with the review using selected models.
71
+
3
72
  ## How It Works
4
73
 
5
74
  1. **Primary model (Claude)** reviews the changes for improvements
@@ -37,41 +106,73 @@ Run a multi-model peer review where different AI models review the same code.
37
106
 
38
107
  ## Provider Configuration
39
108
 
40
- Configure in `.workflow/config.json` under `peerReview`:
109
+ ### Recommended: Use `/wogi-models-setup`
41
110
 
42
- ### Option A: API Keys (Default)
43
- ```json
44
- "peerReview": {
45
- "enabled": true,
46
- "provider": "api",
47
- "models": ["openai:gpt-4o", "google:gemini-pro"],
48
- "apiKeys": {
49
- "openai": "${OPENAI_API_KEY}",
50
- "google": "${GOOGLE_API_KEY}"
51
- }
52
- }
111
+ The easiest way to configure models is the setup wizard:
112
+ ```
113
+ /wogi-models-setup
53
114
  ```
54
115
 
55
- ### Option B: MCP Integration
116
+ This creates a unified configuration used by both peer review and hybrid mode.
117
+
118
+ ### Config Location
119
+
120
+ Models are configured in `.workflow/config.json` under `models`:
121
+
56
122
  ```json
57
- "peerReview": {
58
- "enabled": true,
59
- "provider": "mcp",
60
- "mcpServers": {
61
- "openai": "mcp-openai",
62
- "google": "mcp-gemini"
123
+ {
124
+ "models": {
125
+ "providers": {
126
+ "openai": {
127
+ "apiKeyEnv": "OPENAI_API_KEY",
128
+ "enabled": true,
129
+ "models": ["gpt-4o", "gpt-4o-mini"]
130
+ },
131
+ "google": {
132
+ "apiKeyEnv": "GOOGLE_API_KEY",
133
+ "enabled": true,
134
+ "models": ["gemini-2.0-flash"]
135
+ },
136
+ "local": {
137
+ "endpoint": "http://localhost:11434",
138
+ "provider": "ollama",
139
+ "enabled": true,
140
+ "models": ["qwen2.5-coder"]
141
+ }
142
+ },
143
+ "defaults": {
144
+ "peerReview": ["openai:gpt-4o", "google:gemini-2.0-flash"]
145
+ }
63
146
  }
64
147
  }
65
148
  ```
66
149
 
67
- ### Option C: Manual Mode
150
+ API keys are stored in `.env` (not in config):
151
+ ```
152
+ OPENAI_API_KEY=sk-proj-...
153
+ GOOGLE_API_KEY=AIza...
154
+ ```
155
+
156
+ ### Legacy Config (Auto-Migrated)
157
+
158
+ Old format configs are automatically migrated on first use:
68
159
  ```json
160
+ // Old format (still supported, auto-migrates)
69
161
  "peerReview": {
70
- "enabled": true,
71
- "provider": "manual"
162
+ "apiKeys": {
163
+ "openai": "${OPENAI_API_KEY}"
164
+ },
165
+ "models": ["openai:gpt-4o"]
72
166
  }
73
167
  ```
74
168
 
169
+ ### Manual Mode
170
+
171
+ For manual review (no API keys needed):
172
+ ```
173
+ /wogi-peer-review --manual
174
+ ```
175
+
75
176
  When manual:
76
177
  1. Outputs the review prompt
77
178
  2. User runs in Cursor/other tool
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wogiflow",
3
- "version": "1.0.31",
3
+ "version": "1.0.32",
4
4
  "description": "AI-powered development workflow management system with multi-model support",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -260,9 +260,34 @@ async function callModel(modelString, prompt, options = {}) {
260
260
 
261
261
  /**
262
262
  * Get list of configured models for peer review
263
+ * Checks unified config first, falls back to legacy peerReview config
263
264
  */
264
265
  function getConfiguredModels() {
265
266
  const config = getConfig();
267
+
268
+ // Check unified models config first (new format)
269
+ if (config.models?.providers) {
270
+ const models = [];
271
+ for (const [provider, providerConfig] of Object.entries(config.models.providers)) {
272
+ if (providerConfig.enabled !== false && providerConfig.models?.length > 0) {
273
+ // Check if API key is available (or local provider)
274
+ const apiKeyEnv = providerConfig.apiKeyEnv;
275
+ const hasApiKey = !apiKeyEnv || process.env[apiKeyEnv];
276
+
277
+ if (hasApiKey || provider === 'local') {
278
+ for (const model of providerConfig.models) {
279
+ models.push(`${provider}:${model}`);
280
+ }
281
+ }
282
+ }
283
+ }
284
+
285
+ if (models.length > 0) {
286
+ return models;
287
+ }
288
+ }
289
+
290
+ // Fall back to legacy peerReview config
266
291
  const peerReviewConfig = config.peerReview || {};
267
292
 
268
293
  if (peerReviewConfig.models && Array.isArray(peerReviewConfig.models)) {
@@ -274,6 +299,7 @@ function getConfiguredModels() {
274
299
 
275
300
  /**
276
301
  * Check if model calling is available
302
+ * Checks unified config first, falls back to legacy peerReview config
277
303
  */
278
304
  function isModelCallingAvailable() {
279
305
  const config = getConfig();
@@ -284,7 +310,25 @@ function isModelCallingAvailable() {
284
310
  return { available: false, reason: 'Manual mode configured' };
285
311
  }
286
312
 
287
- // Check for at least one API key
313
+ // Check unified config first
314
+ if (config.models?.providers) {
315
+ for (const [provider, providerConfig] of Object.entries(config.models.providers)) {
316
+ if (providerConfig.enabled !== false) {
317
+ // Local providers don't need API keys
318
+ if (provider === 'local') {
319
+ return { available: true };
320
+ }
321
+
322
+ // Cloud providers need API keys
323
+ const apiKeyEnv = providerConfig.apiKeyEnv;
324
+ if (apiKeyEnv && process.env[apiKeyEnv]) {
325
+ return { available: true };
326
+ }
327
+ }
328
+ }
329
+ }
330
+
331
+ // Fall back to legacy peerReview config
288
332
  const models = getConfiguredModels();
289
333
 
290
334
  for (const modelStr of models) {
@@ -301,7 +345,7 @@ function isModelCallingAvailable() {
301
345
 
302
346
  return {
303
347
  available: false,
304
- reason: 'No API keys configured. Set environment variables or configure in peerReview.apiKeys'
348
+ reason: 'No models configured. Run /wogi-models-setup to configure external models.'
305
349
  };
306
350
  }
307
351
 
@@ -0,0 +1,649 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow - Unified Model Configuration
5
+ *
6
+ * Centralized model and API key management for all WogiFlow features:
7
+ * - Hybrid mode (local LLM execution)
8
+ * - Peer review (multi-model code review)
9
+ * - Model routing (task-based model selection)
10
+ *
11
+ * Config structure in .workflow/config.json:
12
+ * {
13
+ * "models": {
14
+ * "providers": {
15
+ * "openai": { "apiKeyEnv": "OPENAI_API_KEY", "enabled": true, "models": ["gpt-4o"] },
16
+ * "google": { "apiKeyEnv": "GOOGLE_API_KEY", "enabled": true, "models": ["gemini-2.0-flash"] },
17
+ * ...
18
+ * },
19
+ * "defaults": {
20
+ * "hybrid": "local:qwen2.5-coder",
21
+ * "peerReview": ["openai:gpt-4o", "google:gemini-2.0-flash"]
22
+ * }
23
+ * }
24
+ * }
25
+ *
26
+ * Usage:
27
+ * const modelConfig = require('./flow-model-config');
28
+ * const models = modelConfig.getEnabledModels();
29
+ * await modelConfig.addProvider('openai', { apiKey: 'sk-...' });
30
+ */
31
+
32
+ const fs = require('fs');
33
+ const path = require('path');
34
+ const { getProjectRoot, safeJsonParse, colors: c } = require('./flow-utils');
35
+
36
+ const PROJECT_ROOT = getProjectRoot();
37
+ const WORKFLOW_DIR = path.join(PROJECT_ROOT, '.workflow');
38
+ const CONFIG_PATH = path.join(WORKFLOW_DIR, 'config.json');
39
+ const ENV_PATH = path.join(PROJECT_ROOT, '.env');
40
+
41
+ /**
42
+ * Known provider configurations
43
+ */
44
+ const KNOWN_PROVIDERS = {
45
+ openai: {
46
+ displayName: 'OpenAI',
47
+ envKey: 'OPENAI_API_KEY',
48
+ endpoint: 'https://api.openai.com/v1',
49
+ testEndpoint: '/models',
50
+ models: ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'o1-mini', 'o1-preview'],
51
+ defaultModel: 'gpt-4o'
52
+ },
53
+ google: {
54
+ displayName: 'Google (Gemini)',
55
+ envKey: 'GOOGLE_API_KEY',
56
+ endpoint: 'https://generativelanguage.googleapis.com/v1beta',
57
+ testEndpoint: '/models',
58
+ models: ['gemini-2.0-flash-exp', 'gemini-1.5-flash', 'gemini-1.5-pro', 'gemini-pro'],
59
+ defaultModel: 'gemini-2.0-flash-exp'
60
+ },
61
+ anthropic: {
62
+ displayName: 'Anthropic',
63
+ envKey: 'ANTHROPIC_API_KEY',
64
+ endpoint: 'https://api.anthropic.com/v1',
65
+ testEndpoint: '/messages',
66
+ models: ['claude-sonnet-4-20250514', 'claude-3-5-haiku-20241022', 'claude-opus-4-20250514'],
67
+ defaultModel: 'claude-sonnet-4-20250514'
68
+ },
69
+ local: {
70
+ displayName: 'Local LLM',
71
+ envKey: null, // No API key needed
72
+ endpoint: 'http://localhost:11434',
73
+ testEndpoint: '/api/tags',
74
+ models: [], // Detected at runtime
75
+ defaultModel: null,
76
+ subProviders: ['ollama', 'lmstudio']
77
+ }
78
+ };
79
+
80
+ /**
81
+ * Read config file
82
+ * @returns {Object} Config object
83
+ */
84
+ function readConfig() {
85
+ return safeJsonParse(CONFIG_PATH, {});
86
+ }
87
+
88
+ /**
89
+ * Write config file
90
+ * @param {Object} config - Config to write
91
+ */
92
+ function writeConfig(config) {
93
+ fs.mkdirSync(WORKFLOW_DIR, { recursive: true });
94
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
95
+ }
96
+
97
+ /**
98
+ * Get the models configuration section
99
+ * @returns {Object} Models config or empty structure
100
+ */
101
+ function getModelsConfig() {
102
+ const config = readConfig();
103
+ return config.models || { providers: {}, defaults: {} };
104
+ }
105
+
106
+ /**
107
+ * Update models configuration
108
+ * @param {Object} modelsConfig - New models config
109
+ */
110
+ function updateModelsConfig(modelsConfig) {
111
+ const config = readConfig();
112
+ config.models = modelsConfig;
113
+ writeConfig(config);
114
+ }
115
+
116
+ /**
117
+ * Get all configured providers
118
+ * @returns {Array<{name: string, displayName: string, enabled: boolean, models: string[], apiKeySet: boolean}>}
119
+ */
120
+ function getConfiguredProviders() {
121
+ const modelsConfig = getModelsConfig();
122
+ const providers = [];
123
+
124
+ for (const [name, providerConfig] of Object.entries(modelsConfig.providers || {})) {
125
+ const knownProvider = KNOWN_PROVIDERS[name] || {};
126
+ const apiKeyEnv = providerConfig.apiKeyEnv || knownProvider.envKey;
127
+ const apiKeySet = apiKeyEnv ? !!process.env[apiKeyEnv] : true;
128
+
129
+ providers.push({
130
+ name,
131
+ displayName: knownProvider.displayName || name,
132
+ enabled: providerConfig.enabled !== false,
133
+ models: providerConfig.models || [],
134
+ apiKeyEnv,
135
+ apiKeySet,
136
+ endpoint: providerConfig.endpoint || knownProvider.endpoint
137
+ });
138
+ }
139
+
140
+ return providers;
141
+ }
142
+
143
+ /**
144
+ * Get all enabled models in provider:model format
145
+ * @returns {string[]} Array of "provider:model" strings
146
+ */
147
+ function getEnabledModels() {
148
+ const providers = getConfiguredProviders();
149
+ const models = [];
150
+
151
+ for (const provider of providers) {
152
+ if (provider.enabled && provider.apiKeySet) {
153
+ for (const model of provider.models) {
154
+ models.push(`${provider.name}:${model}`);
155
+ }
156
+ }
157
+ }
158
+
159
+ return models;
160
+ }
161
+
162
+ /**
163
+ * Add or update a provider configuration
164
+ * @param {string} providerName - Provider name (openai, google, anthropic, local)
165
+ * @param {Object} options - Provider options
166
+ * @param {string} [options.apiKey] - API key (will be stored in .env)
167
+ * @param {string[]} [options.models] - Models to enable
168
+ * @param {string} [options.endpoint] - Custom endpoint
169
+ * @param {boolean} [options.enabled] - Enable/disable provider
170
+ */
171
+ function addProvider(providerName, options = {}) {
172
+ const modelsConfig = getModelsConfig();
173
+ const knownProvider = KNOWN_PROVIDERS[providerName];
174
+
175
+ if (!modelsConfig.providers) {
176
+ modelsConfig.providers = {};
177
+ }
178
+
179
+ // Merge with existing config
180
+ const existingConfig = modelsConfig.providers[providerName] || {};
181
+ const newConfig = {
182
+ ...existingConfig,
183
+ enabled: options.enabled !== undefined ? options.enabled : true,
184
+ models: options.models || existingConfig.models || (knownProvider?.models?.slice(0, 2) || []),
185
+ apiKeyEnv: knownProvider?.envKey || existingConfig.apiKeyEnv
186
+ };
187
+
188
+ if (options.endpoint) {
189
+ newConfig.endpoint = options.endpoint;
190
+ }
191
+
192
+ // Store API key in .env if provided
193
+ if (options.apiKey && knownProvider?.envKey) {
194
+ updateEnvFile(knownProvider.envKey, options.apiKey);
195
+ }
196
+
197
+ modelsConfig.providers[providerName] = newConfig;
198
+ updateModelsConfig(modelsConfig);
199
+
200
+ return newConfig;
201
+ }
202
+
203
+ /**
204
+ * Remove/disable a provider
205
+ * @param {string} providerName - Provider name
206
+ */
207
+ function removeProvider(providerName) {
208
+ const modelsConfig = getModelsConfig();
209
+
210
+ if (modelsConfig.providers && modelsConfig.providers[providerName]) {
211
+ modelsConfig.providers[providerName].enabled = false;
212
+ updateModelsConfig(modelsConfig);
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Update .env file with API key
218
+ * @param {string} keyName - Environment variable name
219
+ * @param {string} keyValue - API key value
220
+ */
221
+ function updateEnvFile(keyName, keyValue) {
222
+ let envContent = '';
223
+
224
+ try {
225
+ envContent = fs.readFileSync(ENV_PATH, 'utf-8');
226
+ } catch (err) {
227
+ // .env doesn't exist, will create
228
+ }
229
+
230
+ // Parse existing env vars
231
+ const lines = envContent.split('\n');
232
+ const envVars = {};
233
+ const comments = [];
234
+
235
+ for (const line of lines) {
236
+ if (line.startsWith('#') || line.trim() === '') {
237
+ comments.push(line);
238
+ } else {
239
+ const [key, ...valueParts] = line.split('=');
240
+ if (key) {
241
+ envVars[key.trim()] = valueParts.join('=').trim();
242
+ }
243
+ }
244
+ }
245
+
246
+ // Update or add the key
247
+ envVars[keyName] = keyValue;
248
+
249
+ // Rebuild .env content
250
+ let newContent = '';
251
+
252
+ // Add header comment if new file
253
+ if (!envContent) {
254
+ newContent = '# WogiFlow API Keys\n# Generated by /wogi-models-setup\n\n';
255
+ } else if (comments.length > 0) {
256
+ newContent = comments.join('\n') + '\n';
257
+ }
258
+
259
+ // Add env vars
260
+ for (const [key, value] of Object.entries(envVars)) {
261
+ newContent += `${key}=${value}\n`;
262
+ }
263
+
264
+ fs.writeFileSync(ENV_PATH, newContent);
265
+
266
+ // Also set in current process
267
+ process.env[keyName] = keyValue;
268
+ }
269
+
270
+ /**
271
+ * Test connection to a provider
272
+ * @param {string} providerName - Provider name
273
+ * @returns {Promise<{success: boolean, message: string, models?: string[]}>}
274
+ */
275
+ async function testProviderConnection(providerName) {
276
+ const modelsConfig = getModelsConfig();
277
+ const providerConfig = modelsConfig.providers?.[providerName] || {};
278
+ const knownProvider = KNOWN_PROVIDERS[providerName];
279
+
280
+ if (!knownProvider) {
281
+ return { success: false, message: `Unknown provider: ${providerName}` };
282
+ }
283
+
284
+ const endpoint = providerConfig.endpoint || knownProvider.endpoint;
285
+ const apiKeyEnv = providerConfig.apiKeyEnv || knownProvider.envKey;
286
+ const apiKey = apiKeyEnv ? process.env[apiKeyEnv] : null;
287
+
288
+ // Local provider detection
289
+ if (providerName === 'local') {
290
+ return testLocalProvider(endpoint);
291
+ }
292
+
293
+ // Cloud provider test
294
+ if (!apiKey) {
295
+ return { success: false, message: `API key not set (${apiKeyEnv})` };
296
+ }
297
+
298
+ try {
299
+ const result = await testCloudProvider(providerName, endpoint, apiKey);
300
+ return result;
301
+ } catch (err) {
302
+ return { success: false, message: err.message };
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Test local LLM provider (Ollama/LM Studio)
308
+ */
309
+ async function testLocalProvider(endpoint) {
310
+ const https = require('https');
311
+ const http = require('http');
312
+
313
+ return new Promise((resolve) => {
314
+ const url = new URL('/api/tags', endpoint);
315
+ const client = url.protocol === 'https:' ? https : http;
316
+
317
+ const req = client.request(url, { method: 'GET', timeout: 5000 }, (res) => {
318
+ let data = '';
319
+ res.on('data', chunk => data += chunk);
320
+ res.on('end', () => {
321
+ try {
322
+ const parsed = JSON.parse(data);
323
+ const models = parsed.models?.map(m => m.name) || [];
324
+ resolve({
325
+ success: true,
326
+ message: `Connected to Ollama. Found ${models.length} models.`,
327
+ models
328
+ });
329
+ } catch (err) {
330
+ resolve({ success: false, message: 'Invalid response from local LLM' });
331
+ }
332
+ });
333
+ });
334
+
335
+ req.on('error', () => {
336
+ // Try LM Studio endpoint
337
+ testLMStudio(endpoint).then(resolve);
338
+ });
339
+
340
+ req.on('timeout', () => {
341
+ req.destroy();
342
+ resolve({ success: false, message: 'Connection timeout' });
343
+ });
344
+
345
+ req.end();
346
+ });
347
+ }
348
+
349
+ /**
350
+ * Test LM Studio provider
351
+ */
352
+ async function testLMStudio(baseEndpoint) {
353
+ const http = require('http');
354
+
355
+ return new Promise((resolve) => {
356
+ // LM Studio uses OpenAI-compatible endpoint
357
+ const endpoint = baseEndpoint.replace(':11434', ':1234');
358
+ const url = new URL('/v1/models', endpoint);
359
+
360
+ const req = http.request(url, { method: 'GET', timeout: 5000 }, (res) => {
361
+ let data = '';
362
+ res.on('data', chunk => data += chunk);
363
+ res.on('end', () => {
364
+ try {
365
+ const parsed = JSON.parse(data);
366
+ const models = parsed.data?.map(m => m.id) || [];
367
+ resolve({
368
+ success: true,
369
+ message: `Connected to LM Studio. Found ${models.length} models.`,
370
+ models,
371
+ provider: 'lmstudio'
372
+ });
373
+ } catch (err) {
374
+ resolve({ success: false, message: 'No local LLM detected' });
375
+ }
376
+ });
377
+ });
378
+
379
+ req.on('error', () => {
380
+ resolve({ success: false, message: 'No local LLM detected at localhost:11434 or :1234' });
381
+ });
382
+
383
+ req.end();
384
+ });
385
+ }
386
+
387
+ /**
388
+ * Test cloud provider connection
389
+ */
390
+ async function testCloudProvider(providerName, endpoint, apiKey) {
391
+ const https = require('https');
392
+
393
+ return new Promise((resolve, reject) => {
394
+ let url, headers;
395
+
396
+ switch (providerName) {
397
+ case 'openai':
398
+ url = new URL('/v1/models', endpoint);
399
+ headers = { 'Authorization': `Bearer ${apiKey}` };
400
+ break;
401
+ case 'google':
402
+ url = new URL(`/v1beta/models?key=${apiKey}`, endpoint);
403
+ headers = {};
404
+ break;
405
+ case 'anthropic':
406
+ // Anthropic doesn't have a models list endpoint, just test with a simple check
407
+ resolve({ success: true, message: 'API key format valid (Anthropic)' });
408
+ return;
409
+ default:
410
+ reject(new Error(`Unknown cloud provider: ${providerName}`));
411
+ return;
412
+ }
413
+
414
+ const req = https.request(url, { method: 'GET', headers, timeout: 10000 }, (res) => {
415
+ let data = '';
416
+ res.on('data', chunk => data += chunk);
417
+ res.on('end', () => {
418
+ if (res.statusCode === 200) {
419
+ try {
420
+ const parsed = JSON.parse(data);
421
+ let models = [];
422
+
423
+ if (providerName === 'openai') {
424
+ models = parsed.data?.map(m => m.id).filter(id =>
425
+ id.startsWith('gpt-4') || id.startsWith('o1')
426
+ ).slice(0, 10) || [];
427
+ } else if (providerName === 'google') {
428
+ models = parsed.models?.map(m => m.name.replace('models/', '')) || [];
429
+ }
430
+
431
+ resolve({
432
+ success: true,
433
+ message: `Connected to ${providerName}. Found ${models.length} models.`,
434
+ models
435
+ });
436
+ } catch (err) {
437
+ resolve({ success: true, message: `Connected to ${providerName}` });
438
+ }
439
+ } else if (res.statusCode === 401) {
440
+ resolve({ success: false, message: 'Invalid API key' });
441
+ } else {
442
+ resolve({ success: false, message: `API error: ${res.statusCode}` });
443
+ }
444
+ });
445
+ });
446
+
447
+ req.on('error', (err) => reject(err));
448
+ req.on('timeout', () => {
449
+ req.destroy();
450
+ reject(new Error('Connection timeout'));
451
+ });
452
+
453
+ req.end();
454
+ });
455
+ }
456
+
457
+ /**
458
+ * Migrate old config formats to new unified structure
459
+ * - hybrid.executor → models.providers
460
+ * - peerReview.apiKeys → models.providers
461
+ */
462
+ function migrateOldConfig() {
463
+ const config = readConfig();
464
+ let migrated = false;
465
+
466
+ // Initialize models section if needed
467
+ if (!config.models) {
468
+ config.models = { providers: {}, defaults: {} };
469
+ }
470
+ if (!config.models.providers) {
471
+ config.models.providers = {};
472
+ }
473
+
474
+ // Migrate hybrid.executor config
475
+ if (config.hybrid?.executor) {
476
+ const executor = config.hybrid.executor;
477
+ const provider = executor.provider;
478
+
479
+ if (provider && !config.models.providers[provider]) {
480
+ config.models.providers[provider] = {
481
+ enabled: true,
482
+ apiKeyEnv: executor.apiKeyEnv || KNOWN_PROVIDERS[provider]?.envKey,
483
+ models: executor.model ? [executor.model] : [],
484
+ endpoint: executor.providerEndpoint
485
+ };
486
+
487
+ // Set as hybrid default
488
+ if (executor.model) {
489
+ config.models.defaults = config.models.defaults || {};
490
+ config.models.defaults.hybrid = `${provider}:${executor.model}`;
491
+ }
492
+
493
+ migrated = true;
494
+ console.log(`${c.cyan}[model-config]${c.reset} Migrated hybrid.executor to models.providers.${provider}`);
495
+ }
496
+ }
497
+
498
+ // Migrate peerReview.apiKeys config
499
+ if (config.peerReview?.apiKeys) {
500
+ for (const [provider, keyRef] of Object.entries(config.peerReview.apiKeys)) {
501
+ if (!config.models.providers[provider]) {
502
+ // Extract env var name from ${VAR_NAME} format
503
+ const envKey = keyRef.replace(/^\$\{|\}$/g, '');
504
+
505
+ config.models.providers[provider] = {
506
+ enabled: true,
507
+ apiKeyEnv: envKey,
508
+ models: KNOWN_PROVIDERS[provider]?.models?.slice(0, 2) || []
509
+ };
510
+
511
+ migrated = true;
512
+ console.log(`${c.cyan}[model-config]${c.reset} Migrated peerReview.apiKeys.${provider} to models.providers`);
513
+ }
514
+ }
515
+
516
+ // Migrate peer review model defaults
517
+ if (config.peerReview.models && !config.models.defaults?.peerReview) {
518
+ config.models.defaults = config.models.defaults || {};
519
+ config.models.defaults.peerReview = config.peerReview.models;
520
+ migrated = true;
521
+ }
522
+ }
523
+
524
+ if (migrated) {
525
+ writeConfig(config);
526
+ console.log(`${c.green}[model-config]${c.reset} Migration complete. Config saved.`);
527
+ }
528
+
529
+ return migrated;
530
+ }
531
+
532
+ /**
533
+ * Get default models for a feature
534
+ * @param {string} feature - 'hybrid' or 'peerReview'
535
+ * @returns {string|string[]} Default model(s)
536
+ */
537
+ function getDefaultModels(feature) {
538
+ const modelsConfig = getModelsConfig();
539
+ return modelsConfig.defaults?.[feature] || null;
540
+ }
541
+
542
+ /**
543
+ * Set default models for a feature
544
+ * @param {string} feature - 'hybrid' or 'peerReview'
545
+ * @param {string|string[]} models - Model(s) to set as default
546
+ */
547
+ function setDefaultModels(feature, models) {
548
+ const modelsConfig = getModelsConfig();
549
+ if (!modelsConfig.defaults) {
550
+ modelsConfig.defaults = {};
551
+ }
552
+ modelsConfig.defaults[feature] = models;
553
+ updateModelsConfig(modelsConfig);
554
+ }
555
+
556
+ /**
557
+ * Check if any models are configured
558
+ * @returns {boolean}
559
+ */
560
+ function hasConfiguredModels() {
561
+ const providers = getConfiguredProviders();
562
+ return providers.some(p => p.enabled && p.apiKeySet && p.models.length > 0);
563
+ }
564
+
565
+ // CLI interface
566
+ if (require.main === module) {
567
+ const args = process.argv.slice(2);
568
+ const command = args[0];
569
+
570
+ switch (command) {
571
+ case 'list':
572
+ const providers = getConfiguredProviders();
573
+ console.log('\nConfigured Providers:');
574
+ for (const p of providers) {
575
+ const status = p.enabled && p.apiKeySet ? c.green + '✓' + c.reset : c.red + '✗' + c.reset;
576
+ console.log(` ${status} ${p.displayName}: ${p.models.join(', ') || '(no models)'}`);
577
+ }
578
+ console.log('\nEnabled Models:');
579
+ const models = getEnabledModels();
580
+ for (const m of models) {
581
+ console.log(` - ${m}`);
582
+ }
583
+ break;
584
+
585
+ case 'migrate':
586
+ migrateOldConfig();
587
+ break;
588
+
589
+ case 'test':
590
+ const providerName = args[1];
591
+ if (!providerName) {
592
+ console.error('Usage: flow-model-config test <provider>');
593
+ process.exit(1);
594
+ }
595
+ testProviderConnection(providerName).then(result => {
596
+ if (result.success) {
597
+ console.log(`${c.green}✓${c.reset} ${result.message}`);
598
+ if (result.models) {
599
+ console.log(' Models:', result.models.slice(0, 5).join(', '));
600
+ }
601
+ } else {
602
+ console.log(`${c.red}✗${c.reset} ${result.message}`);
603
+ }
604
+ });
605
+ break;
606
+
607
+ default:
608
+ console.log(`
609
+ Wogi Flow - Model Configuration
610
+
611
+ Commands:
612
+ list List configured providers and models
613
+ migrate Migrate old config format to new unified format
614
+ test <provider> Test connection to a provider
615
+
616
+ Examples:
617
+ node flow-model-config.js list
618
+ node flow-model-config.js test openai
619
+ node flow-model-config.js migrate
620
+ `);
621
+ }
622
+ }
623
+
624
+ module.exports = {
625
+ // Core functions
626
+ getModelsConfig,
627
+ updateModelsConfig,
628
+ getConfiguredProviders,
629
+ getEnabledModels,
630
+ hasConfiguredModels,
631
+
632
+ // Provider management
633
+ addProvider,
634
+ removeProvider,
635
+ testProviderConnection,
636
+
637
+ // Defaults
638
+ getDefaultModels,
639
+ setDefaultModels,
640
+
641
+ // Migration
642
+ migrateOldConfig,
643
+
644
+ // Constants
645
+ KNOWN_PROVIDERS,
646
+
647
+ // Utility
648
+ updateEnvFile
649
+ };
@@ -79,6 +79,20 @@ const WOGI_COMMAND_PATTERNS = [
79
79
  /\brun\s+(\/)?wogi-/i
80
80
  ];
81
81
 
82
+ // Maximum length for prompt display (DRY helper)
83
+ const MAX_DISPLAY_LENGTH = 80;
84
+
85
+ /**
86
+ * Truncate prompt for display in messages
87
+ * @param {string} prompt - The prompt to truncate
88
+ * @param {number} maxLength - Maximum length (default: 80)
89
+ * @returns {string} Truncated prompt with ellipsis if needed
90
+ */
91
+ function truncatePrompt(prompt, maxLength = MAX_DISPLAY_LENGTH) {
92
+ if (!prompt || typeof prompt !== 'string') return '';
93
+ return prompt.length > maxLength ? prompt.slice(0, maxLength) + '...' : prompt;
94
+ }
95
+
82
96
  /**
83
97
  * Check if implementation gate should be enforced
84
98
  * @returns {boolean}
@@ -159,17 +173,9 @@ function detectImplementationIntent(prompt) {
159
173
  return { isImplementation: false, confidence: 'low', matches: [] };
160
174
  }
161
175
 
162
- // Determine confidence based on number and quality of matches
163
- let confidence = 'low';
164
- if (matches.length >= 3) {
165
- confidence = 'high';
166
- } else if (matches.length >= 1) {
167
- // Check for strong signals
168
- const hasStrongSignal = matches.some(m =>
169
- /\b(add|create|implement|fix|build)\b/i.test(m)
170
- );
171
- confidence = hasStrongSignal ? 'high' : 'medium';
172
- }
176
+ // Determine confidence based on number of matches
177
+ // Simplified: any match from IMPLEMENTATION_PATTERNS is a strong signal
178
+ const confidence = matches.length >= 2 ? 'high' : 'medium';
173
179
 
174
180
  return { isImplementation: true, confidence, matches };
175
181
  }
@@ -183,7 +189,7 @@ function detectImplementationIntent(prompt) {
183
189
  * @returns {Object} Result: { allowed, blocked, message, reason, confidence, suggestedAction }
184
190
  */
185
191
  function checkImplementationGate(options = {}) {
186
- const { prompt, source: _source } = options;
192
+ const { prompt } = options;
187
193
 
188
194
  // Empty or invalid prompt - allow
189
195
  if (!prompt || typeof prompt !== 'string' || prompt.trim().length === 0) {
@@ -264,7 +270,7 @@ function checkImplementationGate(options = {}) {
264
270
  return {
265
271
  allowed: true,
266
272
  blocked: false,
267
- message: generateWarningMessage(prompt, confidence, matches),
273
+ message: generateWarningMessage(prompt),
268
274
  reason: 'warn_only',
269
275
  confidence,
270
276
  suggestedAction: 'create-story',
@@ -276,7 +282,7 @@ function checkImplementationGate(options = {}) {
276
282
  return {
277
283
  allowed: false,
278
284
  blocked: true,
279
- message: generateBlockMessage(prompt, confidence, matches),
285
+ message: generateBlockMessage(prompt),
280
286
  reason: 'no_active_task',
281
287
  confidence,
282
288
  suggestedAction: 'create-story',
@@ -287,11 +293,10 @@ function checkImplementationGate(options = {}) {
287
293
  /**
288
294
  * Generate warning message (soft mode)
289
295
  */
290
- function generateWarningMessage(prompt, confidence, matches) {
291
- const truncatedPrompt = prompt.length > 80 ? prompt.slice(0, 80) + '...' : prompt;
296
+ function generateWarningMessage(prompt) {
292
297
  return `Warning: No active WogiFlow task.
293
298
 
294
- Consider: /wogi-start "${truncatedPrompt}"
299
+ Consider: /wogi-start "${truncatePrompt(prompt)}"
295
300
 
296
301
  This will execute directly (git/npm/deploy) or create a story first (features/fixes).`;
297
302
  }
@@ -299,13 +304,11 @@ This will execute directly (git/npm/deploy) or create a story first (features/fi
299
304
  /**
300
305
  * Generate block message (hard mode)
301
306
  */
302
- function generateBlockMessage(prompt, confidence, matches) {
303
- const truncatedPrompt = prompt.length > 80 ? prompt.slice(0, 80) + '...' : prompt;
304
-
307
+ function generateBlockMessage(prompt) {
305
308
  return `BLOCKED: No active WogiFlow task.
306
309
 
307
310
  To proceed, run:
308
- /wogi-start "${truncatedPrompt}"
311
+ /wogi-start "${truncatePrompt(prompt)}"
309
312
 
310
313
  This will either:
311
314
  - Execute directly (if operational: git, npm, deploy, build)
@@ -323,6 +326,7 @@ module.exports = {
323
326
  checkImplementationGate,
324
327
  generateWarningMessage,
325
328
  generateBlockMessage,
329
+ truncatePrompt,
326
330
  IMPLEMENTATION_PATTERNS,
327
331
  EXPLORATION_PATTERNS
328
332
  };