wogiflow 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/.workflow/agents/reviewer.md +81 -0
  2. package/.workflow/agents/security.md +94 -0
  3. package/.workflow/agents/story-writer.md +58 -0
  4. package/.workflow/bridges/base-bridge.js +395 -0
  5. package/.workflow/bridges/claude-bridge.js +434 -0
  6. package/.workflow/bridges/index.js +130 -0
  7. package/.workflow/lib/assumption-detector.js +481 -0
  8. package/.workflow/lib/config-substitution.js +371 -0
  9. package/.workflow/lib/failure-categories.js +478 -0
  10. package/.workflow/state/app-map.md.template +15 -0
  11. package/.workflow/state/architecture.md.template +24 -0
  12. package/.workflow/state/component-index.json.template +5 -0
  13. package/.workflow/state/decisions.md.template +15 -0
  14. package/.workflow/state/feedback-patterns.md.template +9 -0
  15. package/.workflow/state/knowledge-sync.json.template +6 -0
  16. package/.workflow/state/progress.md.template +14 -0
  17. package/.workflow/state/ready.json.template +7 -0
  18. package/.workflow/state/request-log.md.template +14 -0
  19. package/.workflow/state/session-state.json.template +11 -0
  20. package/.workflow/state/stack.md.template +33 -0
  21. package/.workflow/state/testing.md.template +36 -0
  22. package/.workflow/templates/claude-md.hbs +257 -0
  23. package/.workflow/templates/correction-report.md +67 -0
  24. package/.workflow/templates/gemini-md.hbs +52 -0
  25. package/README.md +1802 -0
  26. package/bin/flow +205 -0
  27. package/lib/index.js +33 -0
  28. package/lib/installer.js +467 -0
  29. package/lib/release-channel.js +269 -0
  30. package/lib/skill-registry.js +526 -0
  31. package/lib/upgrader.js +401 -0
  32. package/lib/utils.js +305 -0
  33. package/package.json +64 -0
  34. package/scripts/flow +985 -0
  35. package/scripts/flow-adaptive-learning.js +1259 -0
  36. package/scripts/flow-aggregate.js +488 -0
  37. package/scripts/flow-archive +133 -0
  38. package/scripts/flow-auto-context.js +1015 -0
  39. package/scripts/flow-auto-learn.js +615 -0
  40. package/scripts/flow-bridge.js +223 -0
  41. package/scripts/flow-browser-suggest.js +316 -0
  42. package/scripts/flow-bug.js +247 -0
  43. package/scripts/flow-cascade.js +711 -0
  44. package/scripts/flow-changelog +85 -0
  45. package/scripts/flow-checkpoint.js +483 -0
  46. package/scripts/flow-cli.js +403 -0
  47. package/scripts/flow-code-intelligence.js +760 -0
  48. package/scripts/flow-complexity.js +502 -0
  49. package/scripts/flow-config-set.js +152 -0
  50. package/scripts/flow-constants.js +157 -0
  51. package/scripts/flow-context +152 -0
  52. package/scripts/flow-context-init.js +482 -0
  53. package/scripts/flow-context-monitor.js +384 -0
  54. package/scripts/flow-context-scoring.js +886 -0
  55. package/scripts/flow-correct.js +458 -0
  56. package/scripts/flow-damage-control.js +985 -0
  57. package/scripts/flow-deps +101 -0
  58. package/scripts/flow-diff.js +700 -0
  59. package/scripts/flow-done +151 -0
  60. package/scripts/flow-done.js +489 -0
  61. package/scripts/flow-durable-session.js +1541 -0
  62. package/scripts/flow-entropy-monitor.js +345 -0
  63. package/scripts/flow-export-profile +349 -0
  64. package/scripts/flow-export-scanner.js +1046 -0
  65. package/scripts/flow-figma-confirm.js +400 -0
  66. package/scripts/flow-figma-extract.js +496 -0
  67. package/scripts/flow-figma-generate.js +683 -0
  68. package/scripts/flow-figma-index.js +909 -0
  69. package/scripts/flow-figma-match.js +617 -0
  70. package/scripts/flow-figma-mcp-server.js +518 -0
  71. package/scripts/flow-figma-pipeline.js +414 -0
  72. package/scripts/flow-file-ops.js +301 -0
  73. package/scripts/flow-gate-confidence.js +825 -0
  74. package/scripts/flow-guided-edit.js +659 -0
  75. package/scripts/flow-health +185 -0
  76. package/scripts/flow-health.js +413 -0
  77. package/scripts/flow-hooks.js +556 -0
  78. package/scripts/flow-http-client.js +249 -0
  79. package/scripts/flow-hybrid-detect.js +167 -0
  80. package/scripts/flow-hybrid-interactive.js +591 -0
  81. package/scripts/flow-hybrid-test.js +152 -0
  82. package/scripts/flow-import-profile +439 -0
  83. package/scripts/flow-init +253 -0
  84. package/scripts/flow-instruction-richness.js +827 -0
  85. package/scripts/flow-jira-integration.js +579 -0
  86. package/scripts/flow-knowledge-router.js +522 -0
  87. package/scripts/flow-knowledge-sync.js +589 -0
  88. package/scripts/flow-linear-integration.js +631 -0
  89. package/scripts/flow-links.js +774 -0
  90. package/scripts/flow-log-manager.js +559 -0
  91. package/scripts/flow-loop-enforcer.js +1246 -0
  92. package/scripts/flow-loop-retry-learning.js +630 -0
  93. package/scripts/flow-lsp.js +923 -0
  94. package/scripts/flow-map-index +348 -0
  95. package/scripts/flow-map-sync +201 -0
  96. package/scripts/flow-memory-blocks.js +668 -0
  97. package/scripts/flow-memory-compactor.js +350 -0
  98. package/scripts/flow-memory-db.js +1110 -0
  99. package/scripts/flow-memory-sync.js +484 -0
  100. package/scripts/flow-metrics.js +353 -0
  101. package/scripts/flow-migrate-ids.js +370 -0
  102. package/scripts/flow-model-adapter.js +802 -0
  103. package/scripts/flow-model-router.js +884 -0
  104. package/scripts/flow-models.js +1231 -0
  105. package/scripts/flow-morning.js +517 -0
  106. package/scripts/flow-multi-approach.js +660 -0
  107. package/scripts/flow-new-feature +86 -0
  108. package/scripts/flow-onboard +1042 -0
  109. package/scripts/flow-orchestrate-llm.js +459 -0
  110. package/scripts/flow-orchestrate.js +3592 -0
  111. package/scripts/flow-output.js +123 -0
  112. package/scripts/flow-parallel-detector.js +399 -0
  113. package/scripts/flow-parallel-dispatch.js +987 -0
  114. package/scripts/flow-parallel.js +428 -0
  115. package/scripts/flow-pattern-enforcer.js +600 -0
  116. package/scripts/flow-prd-manager.js +282 -0
  117. package/scripts/flow-progress.js +323 -0
  118. package/scripts/flow-project-analyzer.js +975 -0
  119. package/scripts/flow-prompt-composer.js +487 -0
  120. package/scripts/flow-providers.js +1381 -0
  121. package/scripts/flow-queue.js +308 -0
  122. package/scripts/flow-ready +82 -0
  123. package/scripts/flow-ready.js +189 -0
  124. package/scripts/flow-regression.js +396 -0
  125. package/scripts/flow-response-parser.js +450 -0
  126. package/scripts/flow-resume.js +284 -0
  127. package/scripts/flow-rules-sync.js +439 -0
  128. package/scripts/flow-run-trace.js +718 -0
  129. package/scripts/flow-safety.js +587 -0
  130. package/scripts/flow-search +104 -0
  131. package/scripts/flow-security.js +481 -0
  132. package/scripts/flow-session-end +106 -0
  133. package/scripts/flow-session-end.js +437 -0
  134. package/scripts/flow-session-state.js +671 -0
  135. package/scripts/flow-setup-hooks +216 -0
  136. package/scripts/flow-setup-hooks.js +377 -0
  137. package/scripts/flow-skill-create.js +329 -0
  138. package/scripts/flow-skill-creator.js +572 -0
  139. package/scripts/flow-skill-generator.js +1046 -0
  140. package/scripts/flow-skill-learn.js +880 -0
  141. package/scripts/flow-skill-matcher.js +578 -0
  142. package/scripts/flow-spec-generator.js +820 -0
  143. package/scripts/flow-stack-wizard.js +895 -0
  144. package/scripts/flow-standup +162 -0
  145. package/scripts/flow-start +74 -0
  146. package/scripts/flow-start.js +235 -0
  147. package/scripts/flow-status +110 -0
  148. package/scripts/flow-status.js +301 -0
  149. package/scripts/flow-step-browser.js +83 -0
  150. package/scripts/flow-step-changelog.js +217 -0
  151. package/scripts/flow-step-comments.js +306 -0
  152. package/scripts/flow-step-complexity.js +234 -0
  153. package/scripts/flow-step-coverage.js +218 -0
  154. package/scripts/flow-step-knowledge.js +193 -0
  155. package/scripts/flow-step-pr-tests.js +364 -0
  156. package/scripts/flow-step-regression.js +89 -0
  157. package/scripts/flow-step-review.js +516 -0
  158. package/scripts/flow-step-security.js +162 -0
  159. package/scripts/flow-step-silent-failures.js +290 -0
  160. package/scripts/flow-step-simplifier.js +346 -0
  161. package/scripts/flow-story +105 -0
  162. package/scripts/flow-story.js +500 -0
  163. package/scripts/flow-suspend.js +252 -0
  164. package/scripts/flow-sync-daemon.js +654 -0
  165. package/scripts/flow-task-analyzer.js +606 -0
  166. package/scripts/flow-team-dashboard.js +748 -0
  167. package/scripts/flow-team-sync.js +752 -0
  168. package/scripts/flow-team.js +977 -0
  169. package/scripts/flow-tech-options.js +528 -0
  170. package/scripts/flow-templates.js +812 -0
  171. package/scripts/flow-tiered-learning.js +728 -0
  172. package/scripts/flow-trace +204 -0
  173. package/scripts/flow-transcript-chunking.js +1106 -0
  174. package/scripts/flow-transcript-digest.js +7918 -0
  175. package/scripts/flow-transcript-language.js +465 -0
  176. package/scripts/flow-transcript-parsing.js +1085 -0
  177. package/scripts/flow-transcript-stories.js +2194 -0
  178. package/scripts/flow-update-map +224 -0
  179. package/scripts/flow-utils.js +2242 -0
  180. package/scripts/flow-verification.js +644 -0
  181. package/scripts/flow-verify.js +1177 -0
  182. package/scripts/flow-voice-input.js +638 -0
  183. package/scripts/flow-watch +168 -0
  184. package/scripts/flow-workflow-steps.js +521 -0
  185. package/scripts/flow-workflow.js +1029 -0
  186. package/scripts/flow-worktree.js +489 -0
  187. package/scripts/hooks/adapters/base-adapter.js +102 -0
  188. package/scripts/hooks/adapters/claude-code.js +359 -0
  189. package/scripts/hooks/adapters/index.js +79 -0
  190. package/scripts/hooks/core/component-check.js +341 -0
  191. package/scripts/hooks/core/index.js +35 -0
  192. package/scripts/hooks/core/loop-check.js +241 -0
  193. package/scripts/hooks/core/session-context.js +294 -0
  194. package/scripts/hooks/core/task-gate.js +177 -0
  195. package/scripts/hooks/core/validation.js +230 -0
  196. package/scripts/hooks/entry/claude-code/post-tool-use.js +65 -0
  197. package/scripts/hooks/entry/claude-code/pre-tool-use.js +89 -0
  198. package/scripts/hooks/entry/claude-code/session-end.js +87 -0
  199. package/scripts/hooks/entry/claude-code/session-start.js +46 -0
  200. package/scripts/hooks/entry/claude-code/stop.js +43 -0
  201. package/scripts/postinstall.js +139 -0
  202. package/templates/browser-test-flow.json +56 -0
  203. package/templates/bug-report.md +43 -0
  204. package/templates/component-detail.md +42 -0
  205. package/templates/component.stories.tsx +49 -0
  206. package/templates/context/constraints.md +83 -0
  207. package/templates/context/conventions.md +177 -0
  208. package/templates/context/stack.md +60 -0
  209. package/templates/correction-report.md +90 -0
  210. package/templates/feature-proposal.md +35 -0
  211. package/templates/hybrid/_base.md +254 -0
  212. package/templates/hybrid/_patterns.md +45 -0
  213. package/templates/hybrid/create-component.md +127 -0
  214. package/templates/hybrid/create-file.md +56 -0
  215. package/templates/hybrid/create-hook.md +145 -0
  216. package/templates/hybrid/create-service.md +70 -0
  217. package/templates/hybrid/fix-bug.md +33 -0
  218. package/templates/hybrid/modify-file.md +55 -0
  219. package/templates/story.md +68 -0
  220. package/templates/task.json +56 -0
  221. package/templates/trace.md +69 -0
@@ -0,0 +1,591 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow - Hybrid Mode Interactive Setup
5
+ *
6
+ * Guides user through enabling hybrid mode.
7
+ * Supports both local LLMs (Ollama, LM Studio) and cloud models
8
+ * (GPT-4o-mini, Claude Haiku, Gemini Flash).
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const readline = require('readline');
14
+ const { HttpClient } = require('./flow-http-client');
15
+ const { getProjectRoot, colors, getConfig } = require('./flow-utils');
16
+
17
+ const PROJECT_ROOT = getProjectRoot();
18
+ const WORKFLOW_DIR = path.join(PROJECT_ROOT, '.workflow');
19
+ const CONFIG_PATH = path.join(WORKFLOW_DIR, 'config.json');
20
+
21
+ const symbols = {
22
+ success: '✅',
23
+ error: '❌',
24
+ warning: '⚠️',
25
+ info: 'ℹ️',
26
+ check: '✓',
27
+ cross: '✗',
28
+ local: '🖥️',
29
+ cloud: '☁️'
30
+ };
31
+
32
+ // Cloud provider configurations
33
+ const CLOUD_PROVIDERS = {
34
+ openai: {
35
+ name: 'OpenAI',
36
+ models: ['gpt-4o-mini', 'gpt-4o'],
37
+ defaultModel: 'gpt-4o-mini',
38
+ envKey: 'OPENAI_API_KEY',
39
+ testEndpoint: 'https://api.openai.com/v1/models'
40
+ },
41
+ anthropic: {
42
+ name: 'Anthropic',
43
+ models: ['claude-3-5-haiku-latest', 'claude-3-haiku-20240307'],
44
+ defaultModel: 'claude-3-5-haiku-latest',
45
+ envKey: 'ANTHROPIC_API_KEY',
46
+ testEndpoint: 'https://api.anthropic.com/v1/messages'
47
+ },
48
+ google: {
49
+ name: 'Google',
50
+ models: ['gemini-2.0-flash-exp', 'gemini-1.5-flash'],
51
+ defaultModel: 'gemini-2.0-flash-exp',
52
+ envKey: 'GOOGLE_API_KEY',
53
+ testEndpoint: 'https://generativelanguage.googleapis.com/v1beta/models'
54
+ }
55
+ };
56
+
57
+ async function prompt(question) {
58
+ const rl = readline.createInterface({
59
+ input: process.stdin,
60
+ output: process.stdout
61
+ });
62
+
63
+ return new Promise(resolve => {
64
+ rl.question(question, answer => {
65
+ rl.close();
66
+ resolve(answer.trim());
67
+ });
68
+ });
69
+ }
70
+
71
+ class Spinner {
72
+ constructor(text) {
73
+ this.text = text;
74
+ this.frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
75
+ this.frameIndex = 0;
76
+ this.interval = null;
77
+ }
78
+
79
+ start() {
80
+ this.interval = setInterval(() => {
81
+ const frame = this.frames[this.frameIndex];
82
+ process.stdout.write(`\r${colors.cyan}${frame}${colors.reset} ${this.text}`);
83
+ this.frameIndex = (this.frameIndex + 1) % this.frames.length;
84
+ }, 80);
85
+ }
86
+
87
+ stop(finalText, success = true) {
88
+ clearInterval(this.interval);
89
+ const symbol = success ? colors.green + symbols.check : colors.red + symbols.cross;
90
+ process.stdout.write(`\r${symbol}${colors.reset} ${finalText || this.text}\n`);
91
+ }
92
+ }
93
+
94
+ async function checkEndpoint(url, timeout = 3000) {
95
+ return new Promise((resolve) => {
96
+ const req = http.get(url, { timeout }, (res) => {
97
+ let data = '';
98
+ res.on('data', chunk => data += chunk);
99
+ res.on('end', () => {
100
+ try {
101
+ resolve({ success: true, data: JSON.parse(data) });
102
+ } catch (err) {
103
+ resolve({ success: false, error: 'Invalid response' });
104
+ }
105
+ });
106
+ });
107
+ req.on('error', (e) => resolve({ success: false, error: err.message }));
108
+ req.on('timeout', () => {
109
+ req.destroy();
110
+ resolve({ success: false, error: 'Timeout' });
111
+ });
112
+ });
113
+ }
114
+
115
+ async function detectProviders() {
116
+ console.log(`\n${symbols.info} Detecting local LLM providers...\n`);
117
+
118
+ const spinner = new Spinner('Scanning...');
119
+ spinner.start();
120
+
121
+ const providers = [];
122
+
123
+ // Check Ollama
124
+ const ollamaResult = await checkEndpoint('http://localhost:11434/api/tags');
125
+ if (ollamaResult.success) {
126
+ providers.push({
127
+ id: 'ollama',
128
+ name: 'Ollama',
129
+ endpoint: 'http://localhost:11434',
130
+ available: true,
131
+ models: ollamaResult.data.models?.map(m => ({ id: m.name, name: m.name })) || []
132
+ });
133
+ } else {
134
+ providers.push({ id: 'ollama', name: 'Ollama', available: false, error: ollamaResult.error });
135
+ }
136
+
137
+ // Check LM Studio
138
+ const lmstudioResult = await checkEndpoint('http://localhost:1234/v1/models');
139
+ if (lmstudioResult.success) {
140
+ providers.push({
141
+ id: 'lmstudio',
142
+ name: 'LM Studio',
143
+ endpoint: 'http://localhost:1234',
144
+ available: true,
145
+ models: lmstudioResult.data.data?.map(m => ({ id: m.id, name: m.id })) || []
146
+ });
147
+ } else {
148
+ providers.push({ id: 'lmstudio', name: 'LM Studio', available: false, error: lmstudioResult.error });
149
+ }
150
+
151
+ spinner.stop('Detection complete', true);
152
+
153
+ return providers;
154
+ }
155
+
156
+ /**
157
+ * Ask user to choose between local LLM or cloud model executor
158
+ */
159
+ async function selectExecutorType() {
160
+ console.log(`\n${colors.cyan}Choose your executor type:${colors.reset}\n`);
161
+
162
+ console.log(` ${colors.cyan}[L]${colors.reset} ${symbols.local} Local LLM (FREE tokens)`);
163
+ console.log(` • Ollama, LM Studio`);
164
+ console.log(` • Requires local setup`);
165
+ console.log(` • Best for: Privacy, unlimited usage\n`);
166
+
167
+ console.log(` ${colors.cyan}[C]${colors.reset} ${symbols.cloud} Cloud Model (PAID tokens)`);
168
+ console.log(` • GPT-4o-mini, Claude Haiku, Gemini Flash`);
169
+ console.log(` • Requires API key`);
170
+ console.log(` • Best for: No local setup, consistent quality\n`);
171
+
172
+ const choice = await prompt(`Select executor type [L/C]: `);
173
+
174
+ if (choice.toLowerCase() === 'c') {
175
+ return 'cloud';
176
+ }
177
+ return 'local';
178
+ }
179
+
180
+ /**
181
+ * Detect available cloud providers by checking for API keys
182
+ */
183
+ function detectCloudProviders() {
184
+ const available = [];
185
+
186
+ for (const [id, config] of Object.entries(CLOUD_PROVIDERS)) {
187
+ const apiKey = process.env[config.envKey];
188
+ available.push({
189
+ id,
190
+ name: config.name,
191
+ models: config.models.map(m => ({ id: m, name: m })),
192
+ defaultModel: config.defaultModel,
193
+ envKey: config.envKey,
194
+ hasApiKey: !!apiKey,
195
+ apiKey: apiKey || null
196
+ });
197
+ }
198
+
199
+ return available;
200
+ }
201
+
202
+ /**
203
+ * Select a cloud provider
204
+ */
205
+ async function selectCloudProvider() {
206
+ const providers = detectCloudProviders();
207
+ const withKeys = providers.filter(p => p.hasApiKey);
208
+
209
+ console.log(`\n${colors.cyan}Available cloud providers:${colors.reset}\n`);
210
+
211
+ providers.forEach((p, i) => {
212
+ const status = p.hasApiKey
213
+ ? `${colors.green}${symbols.check} API key found${colors.reset}`
214
+ : `${colors.dim}No API key (${p.envKey})${colors.reset}`;
215
+ console.log(` ${colors.cyan}[${i + 1}]${colors.reset} ${p.name} - ${status}`);
216
+ });
217
+
218
+ if (withKeys.length === 0) {
219
+ console.log(`\n${colors.yellow}${symbols.warning} No API keys detected.${colors.reset}`);
220
+ console.log(`Set one of the following environment variables:\n`);
221
+ providers.forEach(p => {
222
+ console.log(` ${colors.cyan}${p.envKey}${colors.reset} for ${p.name}`);
223
+ });
224
+
225
+ const manualKey = await prompt(`\nWould you like to enter an API key now? [y/N]: `);
226
+ if (manualKey.toLowerCase() !== 'y') {
227
+ return null;
228
+ }
229
+
230
+ // Let them choose which provider and enter key
231
+ const providerChoice = await prompt(`Select provider [1-${providers.length}]: `);
232
+ const providerIndex = parseInt(providerChoice) - 1;
233
+ const selectedProvider = providers[providerIndex] || providers[0];
234
+
235
+ const apiKey = await prompt(`Enter ${selectedProvider.name} API key: `);
236
+ if (!apiKey) {
237
+ return null;
238
+ }
239
+
240
+ selectedProvider.apiKey = apiKey;
241
+ selectedProvider.hasApiKey = true;
242
+ return selectedProvider;
243
+ }
244
+
245
+ // If only one has a key, use it
246
+ if (withKeys.length === 1) {
247
+ console.log(`\nUsing ${withKeys[0].name} (only provider with API key)`);
248
+ return withKeys[0];
249
+ }
250
+
251
+ // Let user choose
252
+ const choice = await prompt(`\nSelect provider [1-${providers.length}]: `);
253
+ const index = parseInt(choice) - 1;
254
+
255
+ if (index >= 0 && index < providers.length) {
256
+ const selected = providers[index];
257
+ if (!selected.hasApiKey) {
258
+ const apiKey = await prompt(`Enter ${selected.name} API key: `);
259
+ if (!apiKey) {
260
+ return null;
261
+ }
262
+ selected.apiKey = apiKey;
263
+ selected.hasApiKey = true;
264
+ }
265
+ return selected;
266
+ }
267
+
268
+ return withKeys[0] || null;
269
+ }
270
+
271
+ /**
272
+ * Select a cloud model
273
+ */
274
+ async function selectCloudModel(provider) {
275
+ console.log(`\n${colors.cyan}Available ${provider.name} models:${colors.reset}\n`);
276
+
277
+ provider.models.forEach((m, i) => {
278
+ const isDefault = m.id === provider.defaultModel ? ` ${colors.dim}(recommended)${colors.reset}` : '';
279
+ console.log(` ${colors.cyan}[${i + 1}]${colors.reset} ${m.name}${isDefault}`);
280
+ });
281
+
282
+ const choice = await prompt(`\nSelect model [1-${provider.models.length}] (default: 1): `);
283
+ const index = parseInt(choice) - 1;
284
+
285
+ if (index >= 0 && index < provider.models.length) {
286
+ return provider.models[index];
287
+ }
288
+
289
+ return provider.models[0];
290
+ }
291
+
292
+ /**
293
+ * Test cloud provider connection using shared HttpClient
294
+ */
295
+ async function testCloudConnection(provider, model) {
296
+ console.log(`\n${symbols.info} Testing connection to ${provider.name}...`);
297
+
298
+ const spinner = new Spinner('Verifying API access...');
299
+ spinner.start();
300
+
301
+ try {
302
+ const testEndpoint = CLOUD_PROVIDERS[provider.id].testEndpoint;
303
+ const url = new URL(testEndpoint);
304
+
305
+ // Build provider-specific headers
306
+ const headers = { 'Content-Type': 'application/json' };
307
+ if (provider.id === 'openai') {
308
+ headers['Authorization'] = `Bearer ${provider.apiKey}`;
309
+ } else if (provider.id === 'anthropic') {
310
+ headers['x-api-key'] = provider.apiKey;
311
+ headers['anthropic-version'] = '2023-06-01';
312
+ }
313
+
314
+ // Add API key to URL for Google
315
+ let path = url.pathname;
316
+ if (provider.id === 'google' && provider.apiKey) {
317
+ path += `?key=${provider.apiKey}`;
318
+ }
319
+
320
+ const client = new HttpClient(url.origin, { headers, timeout: 10000 });
321
+ const response = await client.get(path);
322
+
323
+ // For Anthropic, 405 means endpoint reached (method not allowed but accessible)
324
+ if (provider.id === 'anthropic' && (response.status === 405 || response.status === 200)) {
325
+ spinner.stop('API connection verified!', true);
326
+ return true;
327
+ } else if (response.status >= 200 && response.status < 400) {
328
+ spinner.stop('API connection verified!', true);
329
+ return true;
330
+ } else if (response.status === 401 || response.status === 403) {
331
+ throw new Error('Invalid API key');
332
+ }
333
+
334
+ // Optimistic - endpoint reached
335
+ spinner.stop('API connection verified!', true);
336
+ return true;
337
+ } catch (err) {
338
+ spinner.stop(`Connection check: ${err.message}`, false);
339
+ // Don't fail completely - API might still work
340
+ return false;
341
+ }
342
+ }
343
+
344
+ async function selectProvider(providers) {
345
+ const available = providers.filter(p => p.available);
346
+
347
+ if (available.length === 0) {
348
+ console.log(`\n${colors.red}${symbols.error} No local LLM providers detected!${colors.reset}`);
349
+ console.log(`\nPlease start one of the following:`);
350
+ console.log(` ${colors.cyan}Ollama:${colors.reset} ollama serve`);
351
+ console.log(` ${colors.cyan}LM Studio:${colors.reset} Start the app and enable server`);
352
+ console.log(`\nThen run /wogi-hybrid again.`);
353
+ return null;
354
+ }
355
+
356
+ console.log(`\n${colors.green}${symbols.success} Found providers:${colors.reset}\n`);
357
+
358
+ available.forEach((p, i) => {
359
+ console.log(` ${colors.cyan}[${i + 1}]${colors.reset} ${p.name} (${p.endpoint})`);
360
+ console.log(` Models: ${p.models.length}`);
361
+ });
362
+
363
+ if (available.length === 1) {
364
+ console.log(`\nUsing ${available[0].name} (only available provider)`);
365
+ return available[0];
366
+ }
367
+
368
+ const choice = await prompt(`\nSelect provider [1-${available.length}]: `);
369
+ const index = parseInt(choice) - 1;
370
+
371
+ if (index >= 0 && index < available.length) {
372
+ return available[index];
373
+ }
374
+
375
+ return available[0];
376
+ }
377
+
378
+ async function selectModel(provider) {
379
+ if (!provider.models || provider.models.length === 0) {
380
+ console.log(`\n${colors.yellow}${symbols.warning} No models found on ${provider.name}${colors.reset}`);
381
+ console.log(`\nPlease load a model first:`);
382
+
383
+ if (provider.id === 'ollama') {
384
+ console.log(` ${colors.cyan}ollama pull nemotron-3-nano${colors.reset}`);
385
+ console.log(` ${colors.cyan}ollama pull qwen3-coder:30b${colors.reset}`);
386
+ } else {
387
+ console.log(` Open LM Studio and download a model`);
388
+ }
389
+
390
+ return null;
391
+ }
392
+
393
+ console.log(`\n${colors.cyan}Available models:${colors.reset}\n`);
394
+
395
+ provider.models.forEach((m, i) => {
396
+ console.log(` ${colors.cyan}[${i + 1}]${colors.reset} ${m.name}`);
397
+ });
398
+
399
+ const choice = await prompt(`\nSelect model [1-${provider.models.length}]: `);
400
+ const index = parseInt(choice) - 1;
401
+
402
+ if (index >= 0 && index < provider.models.length) {
403
+ return provider.models[index];
404
+ }
405
+
406
+ return provider.models[0];
407
+ }
408
+
409
+ async function saveConfig(executorType, provider, model) {
410
+ let config = {};
411
+
412
+ if (fs.existsSync(CONFIG_PATH)) {
413
+ config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
414
+ }
415
+
416
+ // Preserve existing hybrid settings if present
417
+ const existingHybrid = config.hybrid || {};
418
+
419
+ config.hybrid = {
420
+ enabled: true,
421
+ // New executor config structure
422
+ executor: {
423
+ type: executorType,
424
+ provider: provider.id,
425
+ providerEndpoint: executorType === 'local' ? provider.endpoint : null,
426
+ model: model.id,
427
+ apiKey: executorType === 'cloud' ? provider.apiKey : null
428
+ },
429
+ // Planner settings
430
+ planner: {
431
+ adaptToExecutor: true,
432
+ useAdapterKnowledge: true
433
+ },
434
+ // Preserve legacy fields for backward compatibility
435
+ provider: provider.id,
436
+ providerEndpoint: executorType === 'local' ? provider.endpoint : null,
437
+ model: model.id,
438
+ settings: {
439
+ temperature: existingHybrid.settings?.temperature ?? 0.7,
440
+ maxTokens: existingHybrid.settings?.maxTokens ?? (executorType === 'cloud' ? 4096 : 16384),
441
+ maxRetries: existingHybrid.settings?.maxRetries ?? 20,
442
+ timeout: existingHybrid.settings?.timeout ?? (executorType === 'cloud' ? 60000 : 120000),
443
+ autoExecute: existingHybrid.settings?.autoExecute ?? false,
444
+ createBranch: existingHybrid.settings?.createBranch ?? false,
445
+ tokenEstimation: existingHybrid.settings?.tokenEstimation ?? {
446
+ enabled: true,
447
+ minTokens: 1000,
448
+ maxTokens: 8000,
449
+ defaultLevel: 'medium',
450
+ logMetrics: true
451
+ }
452
+ },
453
+ templates: {
454
+ directory: existingHybrid.templates?.directory || 'templates/hybrid'
455
+ },
456
+ // Cloud provider reference
457
+ cloudProviders: existingHybrid.cloudProviders || CLOUD_PROVIDERS,
458
+ // Project context
459
+ projectContext: existingHybrid.projectContext || {}
460
+ };
461
+
462
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
463
+
464
+ console.log(`\n${colors.green}${symbols.success} Configuration saved!${colors.reset}`);
465
+ }
466
+
467
+ async function testConnection(provider, model) {
468
+ console.log(`\n${symbols.info} Testing connection to ${model.name}...`);
469
+
470
+ const spinner = new Spinner('Sending test prompt...');
471
+ spinner.start();
472
+
473
+ try {
474
+ const isOllama = provider.id === 'ollama';
475
+ const client = new HttpClient(provider.endpoint, { timeout: 30000 });
476
+
477
+ const path = isOllama ? '/api/generate' : '/v1/chat/completions';
478
+ const body = isOllama
479
+ ? { model: model.id, prompt: 'Say "OK"', stream: false }
480
+ : { model: model.id, messages: [{ role: 'user', content: 'Say "OK"' }], max_tokens: 10 };
481
+
482
+ await client.post(path, body);
483
+
484
+ spinner.stop('Connection successful!', true);
485
+ return true;
486
+ } catch (err) {
487
+ spinner.stop(`Connection failed: ${err.message}`, false);
488
+ return false;
489
+ }
490
+ }
491
+
492
+ async function main() {
493
+ console.log(`
494
+ ${colors.cyan}╔═══════════════════════════════════════════════════════════════╗
495
+ ║ Wogi Flow - Hybrid Mode Setup ║
496
+ ╚═══════════════════════════════════════════════════════════════╝${colors.reset}
497
+ `);
498
+
499
+ // Check if workflow dir exists
500
+ if (!fs.existsSync(WORKFLOW_DIR)) {
501
+ console.log(`${colors.red}${symbols.error} Wogi Flow not installed in this project.${colors.reset}`);
502
+ console.log(`Run /wogi-onboard first.`);
503
+ process.exit(1);
504
+ }
505
+
506
+ // Step 1: Choose executor type (local or cloud)
507
+ const executorType = await selectExecutorType();
508
+
509
+ let provider, model, connected;
510
+
511
+ if (executorType === 'cloud') {
512
+ // Cloud executor flow
513
+ provider = await selectCloudProvider();
514
+ if (!provider) {
515
+ console.log(`\n${colors.red}${symbols.error} Cloud provider setup cancelled.${colors.reset}`);
516
+ process.exit(1);
517
+ }
518
+
519
+ model = await selectCloudModel(provider);
520
+ connected = await testCloudConnection(provider, model);
521
+
522
+ if (!connected) {
523
+ const cont = await prompt('\nContinue anyway? [y/N]: ');
524
+ if (cont.toLowerCase() !== 'y') {
525
+ process.exit(1);
526
+ }
527
+ }
528
+ } else {
529
+ // Local LLM flow (existing behavior)
530
+ const providers = await detectProviders();
531
+
532
+ provider = await selectProvider(providers);
533
+ if (!provider) {
534
+ process.exit(1);
535
+ }
536
+
537
+ model = await selectModel(provider);
538
+ if (!model) {
539
+ process.exit(1);
540
+ }
541
+
542
+ connected = await testConnection(provider, model);
543
+ if (!connected) {
544
+ const cont = await prompt('\nContinue anyway? [y/N]: ');
545
+ if (cont.toLowerCase() !== 'y') {
546
+ process.exit(1);
547
+ }
548
+ }
549
+ }
550
+
551
+ // Save config
552
+ await saveConfig(executorType, provider, model);
553
+
554
+ // Summary
555
+ const executorIcon = executorType === 'cloud' ? symbols.cloud : symbols.local;
556
+ const executorLabel = executorType === 'cloud' ? 'Cloud' : 'Local';
557
+ const locationInfo = executorType === 'cloud'
558
+ ? `API: ${provider.name}`
559
+ : `Endpoint: ${provider.endpoint}`;
560
+
561
+ console.log(`
562
+ ${colors.green}═══════════════════════════════════════════════════════════════${colors.reset}
563
+ ${colors.green} Hybrid Mode Enabled! ${executorIcon}${colors.reset}
564
+ ${colors.green}═══════════════════════════════════════════════════════════════${colors.reset}
565
+
566
+ Executor: ${executorLabel} (${provider.name})
567
+ Model: ${model.name}
568
+ ${locationInfo}
569
+
570
+ ${colors.cyan}How it works:${colors.reset}
571
+ 1. Give me a task as usual
572
+ 2. I'll create an execution plan
573
+ 3. You review and approve
574
+ 4. ${model.name} executes ${executorType === 'cloud' ? 'via API' : 'locally'}
575
+ 5. I handle any failures
576
+
577
+ ${colors.cyan}Commands:${colors.reset}
578
+ /wogi-hybrid-off Disable hybrid mode
579
+ /wogi-hybrid-status Check configuration
580
+ /wogi-hybrid-edit Modify plan before execution
581
+
582
+ ${executorType === 'cloud'
583
+ ? `${colors.dim}Note: Cloud executor uses PAID API tokens${colors.reset}`
584
+ : `${colors.dim}Estimated token savings: 20-60% (varies with task complexity)${colors.reset}`}
585
+ `);
586
+ }
587
+
588
+ main().catch(e => {
589
+ console.error(`${colors.red}Error: ${err.message}${colors.reset}`);
590
+ process.exit(1);
591
+ });