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,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
|
+
});
|