groove-dev 0.22.3 → 0.22.4
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.
|
@@ -1,10 +1,33 @@
|
|
|
1
1
|
// GROOVE CLI — start command
|
|
2
2
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
3
3
|
|
|
4
|
+
import { existsSync } from 'fs';
|
|
5
|
+
import { resolve } from 'path';
|
|
4
6
|
import { Daemon } from '@groove-dev/daemon';
|
|
5
7
|
import chalk from 'chalk';
|
|
8
|
+
import { runSetupWizard, saveKeysViaDaemon } from '../setup.js';
|
|
6
9
|
|
|
7
10
|
export async function start(options) {
|
|
11
|
+
const grooveDir = resolve(process.cwd(), '.groove');
|
|
12
|
+
const isFirstRun = !existsSync(resolve(grooveDir, 'config.json'));
|
|
13
|
+
|
|
14
|
+
// ── First-run interactive wizard ────────────────────────────
|
|
15
|
+
let setupKeys = {};
|
|
16
|
+
if (isFirstRun) {
|
|
17
|
+
try {
|
|
18
|
+
const result = await runSetupWizard();
|
|
19
|
+
setupKeys = result.keys || {};
|
|
20
|
+
} catch (err) {
|
|
21
|
+
// If stdin is not interactive (piped), skip wizard
|
|
22
|
+
if (err.code === 'ERR_USE_AFTER_CLOSE') {
|
|
23
|
+
console.log(chalk.dim(' Non-interactive mode — skipping setup wizard.'));
|
|
24
|
+
} else {
|
|
25
|
+
throw err;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ── Start daemon ────────────────────────────────────────────
|
|
8
31
|
console.log(chalk.bold('GROOVE') + ' starting daemon...');
|
|
9
32
|
|
|
10
33
|
try {
|
|
@@ -15,7 +38,6 @@ export async function start(options) {
|
|
|
15
38
|
|
|
16
39
|
const shutdown = async () => {
|
|
17
40
|
console.log('\nShutting down...');
|
|
18
|
-
// Force exit after 3s if stop hangs
|
|
19
41
|
const forceTimer = setTimeout(() => process.exit(1), 3000);
|
|
20
42
|
forceTimer.unref();
|
|
21
43
|
try { await daemon.stop(); } catch { /* ignore */ }
|
|
@@ -26,6 +48,12 @@ export async function start(options) {
|
|
|
26
48
|
process.on('SIGTERM', shutdown);
|
|
27
49
|
|
|
28
50
|
await daemon.start();
|
|
51
|
+
|
|
52
|
+
// Save API keys from wizard (after daemon is running)
|
|
53
|
+
if (Object.keys(setupKeys).length > 0) {
|
|
54
|
+
await saveKeysViaDaemon(setupKeys, daemon.port);
|
|
55
|
+
}
|
|
56
|
+
|
|
29
57
|
console.log(chalk.green('Ready.'));
|
|
30
58
|
} catch (err) {
|
|
31
59
|
console.error(chalk.red('Failed to start:'), err.message);
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
// GROOVE CLI — Interactive First-Run Setup Wizard
|
|
2
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
3
|
+
|
|
4
|
+
import { createInterface } from 'readline';
|
|
5
|
+
import { execSync, execFileSync } from 'child_process';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
|
|
8
|
+
const rl = () => createInterface({ input: process.stdin, output: process.stdout });
|
|
9
|
+
|
|
10
|
+
function ask(prompt) {
|
|
11
|
+
return new Promise((resolve) => {
|
|
12
|
+
const r = rl();
|
|
13
|
+
r.question(prompt, (answer) => { r.close(); resolve(answer.trim()); });
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function askMasked(prompt) {
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
const r = rl();
|
|
20
|
+
r.question(prompt, (answer) => { r.close(); resolve(answer.trim()); });
|
|
21
|
+
// Mask input by overwriting with *
|
|
22
|
+
r._writeToOutput = function (str) {
|
|
23
|
+
if (str.includes('\n') || str.includes('\r')) {
|
|
24
|
+
r.output.write('\n');
|
|
25
|
+
} else {
|
|
26
|
+
r.output.write('*');
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function cmd(command) {
|
|
33
|
+
try {
|
|
34
|
+
return execSync(command, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
35
|
+
} catch { return null; }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isInstalled(name) {
|
|
39
|
+
try {
|
|
40
|
+
execFileSync('which', [name], { stdio: 'pipe' });
|
|
41
|
+
return true;
|
|
42
|
+
} catch { return false; }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const PROVIDERS = [
|
|
46
|
+
{
|
|
47
|
+
id: 'claude-code',
|
|
48
|
+
name: 'Claude Code',
|
|
49
|
+
cli: 'claude',
|
|
50
|
+
install: 'npm i -g @anthropic-ai/claude-code',
|
|
51
|
+
auth: 'subscription',
|
|
52
|
+
description: 'Anthropic\'s CLI agent. Uses your Claude subscription (no API key needed).',
|
|
53
|
+
recommended: true,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: 'codex',
|
|
57
|
+
name: 'Codex',
|
|
58
|
+
cli: 'codex',
|
|
59
|
+
install: 'npm i -g @openai/codex',
|
|
60
|
+
auth: 'api-key',
|
|
61
|
+
envKey: 'OPENAI_API_KEY',
|
|
62
|
+
description: 'OpenAI\'s coding agent. Requires an OpenAI API key.',
|
|
63
|
+
keyHelp: 'Get your key at https://platform.openai.com/api-keys',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 'gemini',
|
|
67
|
+
name: 'Gemini CLI',
|
|
68
|
+
cli: 'gemini',
|
|
69
|
+
install: 'npm i -g @google/gemini-cli',
|
|
70
|
+
auth: 'api-key',
|
|
71
|
+
envKey: 'GEMINI_API_KEY',
|
|
72
|
+
description: 'Google\'s coding agent. Requires a Gemini API key.',
|
|
73
|
+
keyHelp: 'Get your key at https://aistudio.google.com/apikey',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: 'ollama',
|
|
77
|
+
name: 'Ollama',
|
|
78
|
+
cli: 'ollama',
|
|
79
|
+
install: process.platform === 'darwin' ? 'brew install ollama' : 'See https://ollama.ai/download',
|
|
80
|
+
auth: 'local',
|
|
81
|
+
description: 'Run models locally. No API key, no cloud. Requires 8GB+ RAM.',
|
|
82
|
+
},
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
export async function runSetupWizard() {
|
|
86
|
+
console.log('');
|
|
87
|
+
console.log(chalk.bold(' ┌──────────────────────────────────────────┐'));
|
|
88
|
+
console.log(chalk.bold(' │') + ' Welcome to ' + chalk.bold.cyan('GROOVE') + ' ' + chalk.bold('│'));
|
|
89
|
+
console.log(chalk.bold(' │') + ' Agent orchestration for AI coding ' + chalk.bold('│'));
|
|
90
|
+
console.log(chalk.bold(' └──────────────────────────────────────────┘'));
|
|
91
|
+
console.log('');
|
|
92
|
+
console.log(chalk.dim(' Let\'s get you set up. This takes about a minute.'));
|
|
93
|
+
console.log('');
|
|
94
|
+
|
|
95
|
+
// ── Step 1: System check ────────────────────────────────────
|
|
96
|
+
console.log(chalk.bold(' 1. System Check'));
|
|
97
|
+
console.log('');
|
|
98
|
+
|
|
99
|
+
// Node.js
|
|
100
|
+
const nodeVersion = process.version;
|
|
101
|
+
const nodeMajor = parseInt(nodeVersion.slice(1), 10);
|
|
102
|
+
if (nodeMajor >= 20) {
|
|
103
|
+
console.log(chalk.green(' ✓') + ` Node.js ${nodeVersion}`);
|
|
104
|
+
} else {
|
|
105
|
+
console.log(chalk.red(' ✗') + ` Node.js ${nodeVersion} — Groove requires Node.js 20+`);
|
|
106
|
+
console.log(chalk.dim(' Install the latest: https://nodejs.org'));
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// npm
|
|
111
|
+
const npmVersion = cmd('npm --version');
|
|
112
|
+
if (npmVersion) {
|
|
113
|
+
console.log(chalk.green(' ✓') + ` npm ${npmVersion}`);
|
|
114
|
+
} else {
|
|
115
|
+
console.log(chalk.red(' ✗') + ' npm not found');
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// git
|
|
120
|
+
if (isInstalled('git')) {
|
|
121
|
+
console.log(chalk.green(' ✓') + ` git ${cmd('git --version')?.replace('git version ', '') || ''}`);
|
|
122
|
+
} else {
|
|
123
|
+
console.log(chalk.yellow(' !') + ' git not found — agents may need it for version control');
|
|
124
|
+
console.log(chalk.dim(' Install: https://git-scm.com'));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
console.log('');
|
|
128
|
+
|
|
129
|
+
// ── Step 2: Provider scan ───────────────────────────────────
|
|
130
|
+
console.log(chalk.bold(' 2. AI Providers'));
|
|
131
|
+
console.log('');
|
|
132
|
+
|
|
133
|
+
const installed = [];
|
|
134
|
+
const available = [];
|
|
135
|
+
|
|
136
|
+
for (const p of PROVIDERS) {
|
|
137
|
+
if (isInstalled(p.cli)) {
|
|
138
|
+
installed.push(p);
|
|
139
|
+
const rec = p.recommended ? chalk.cyan(' (recommended)') : '';
|
|
140
|
+
console.log(chalk.green(' ✓') + ` ${p.name}${rec}`);
|
|
141
|
+
} else {
|
|
142
|
+
available.push(p);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (installed.length > 0 && available.length > 0) {
|
|
147
|
+
console.log('');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── Step 3: Install missing providers ───────────────────────
|
|
151
|
+
if (available.length > 0) {
|
|
152
|
+
console.log(chalk.dim(' Available to install:'));
|
|
153
|
+
available.forEach((p, i) => {
|
|
154
|
+
const rec = p.recommended ? chalk.cyan(' (recommended)') : '';
|
|
155
|
+
console.log(` ${chalk.bold(i + 1)}. ${p.name}${rec} — ${p.description}`);
|
|
156
|
+
});
|
|
157
|
+
console.log(` ${chalk.bold('0')}. Skip — I'll install later`);
|
|
158
|
+
console.log('');
|
|
159
|
+
|
|
160
|
+
const answer = await ask(chalk.bold(' Which providers would you like to install? ') + chalk.dim('(e.g. 1,2 or 0 to skip) '));
|
|
161
|
+
const selections = answer.split(/[,\s]+/).map((s) => parseInt(s, 10)).filter((n) => n > 0 && n <= available.length);
|
|
162
|
+
|
|
163
|
+
if (selections.length > 0) {
|
|
164
|
+
console.log('');
|
|
165
|
+
for (const idx of selections) {
|
|
166
|
+
const p = available[idx - 1];
|
|
167
|
+
console.log(` Installing ${chalk.bold(p.name)}...`);
|
|
168
|
+
try {
|
|
169
|
+
execSync(p.install, { stdio: 'inherit' });
|
|
170
|
+
console.log(chalk.green(` ✓ ${p.name} installed`));
|
|
171
|
+
installed.push(p);
|
|
172
|
+
} catch {
|
|
173
|
+
console.log(chalk.red(` ✗ ${p.name} failed to install`));
|
|
174
|
+
console.log(chalk.dim(` Try manually: ${p.install}`));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (installed.length === 0) {
|
|
181
|
+
console.log('');
|
|
182
|
+
console.log(chalk.yellow(' No providers installed.'));
|
|
183
|
+
console.log(chalk.dim(' You\'ll need at least one to spawn agents.'));
|
|
184
|
+
console.log(chalk.dim(' Recommended: npm i -g @anthropic-ai/claude-code'));
|
|
185
|
+
console.log('');
|
|
186
|
+
const cont = await ask(chalk.bold(' Continue anyway? ') + chalk.dim('[Y/n] '));
|
|
187
|
+
if (cont.toLowerCase() === 'n') {
|
|
188
|
+
console.log(chalk.dim(' Run `groove start` again after installing a provider.'));
|
|
189
|
+
process.exit(0);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
console.log('');
|
|
194
|
+
|
|
195
|
+
// ── Step 4: API key setup ───────────────────────────────────
|
|
196
|
+
const needsKey = installed.filter((p) => p.auth === 'api-key');
|
|
197
|
+
const keys = {};
|
|
198
|
+
|
|
199
|
+
if (needsKey.length > 0) {
|
|
200
|
+
console.log(chalk.bold(' 3. API Keys'));
|
|
201
|
+
console.log('');
|
|
202
|
+
|
|
203
|
+
for (const p of needsKey) {
|
|
204
|
+
console.log(` ${chalk.bold(p.name)} requires an API key.`);
|
|
205
|
+
if (p.keyHelp) console.log(chalk.dim(` ${p.keyHelp}`));
|
|
206
|
+
|
|
207
|
+
const key = await askMasked(` Enter ${p.name} API key ${chalk.dim('(or press Enter to skip)')}: `);
|
|
208
|
+
if (key) {
|
|
209
|
+
keys[p.id] = key;
|
|
210
|
+
console.log(chalk.green(` ✓ ${p.name} key saved`));
|
|
211
|
+
} else {
|
|
212
|
+
console.log(chalk.dim(` Skipped — set it later in Settings or: groove set-key ${p.id} <key>`));
|
|
213
|
+
}
|
|
214
|
+
console.log('');
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ── Step 5: Claude Code auth check ──────────────────────────
|
|
219
|
+
const hasClaude = installed.some((p) => p.id === 'claude-code');
|
|
220
|
+
if (hasClaude) {
|
|
221
|
+
console.log(chalk.bold(` ${needsKey.length > 0 ? '4' : '3'}. Claude Code Auth`));
|
|
222
|
+
console.log('');
|
|
223
|
+
console.log(chalk.dim(' Claude Code uses your Anthropic subscription (not API keys).'));
|
|
224
|
+
console.log(chalk.dim(' If you haven\'t logged in yet, run `claude` in a terminal to authenticate.'));
|
|
225
|
+
|
|
226
|
+
// Quick check if claude is authenticated
|
|
227
|
+
try {
|
|
228
|
+
const out = cmd('claude --version');
|
|
229
|
+
if (out) {
|
|
230
|
+
console.log(chalk.green(' ✓') + ` Claude Code ${out} installed`);
|
|
231
|
+
}
|
|
232
|
+
} catch { /* ignore */ }
|
|
233
|
+
console.log('');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ── Done! ──────────────────────────────────────────────────
|
|
237
|
+
console.log(chalk.bold(' Setup complete!'));
|
|
238
|
+
console.log('');
|
|
239
|
+
console.log(` Providers: ${installed.map((p) => p.name).join(', ') || 'none'}`);
|
|
240
|
+
if (Object.keys(keys).length > 0) {
|
|
241
|
+
console.log(` Keys configured: ${Object.keys(keys).map((k) => PROVIDERS.find((p) => p.id === k)?.name || k).join(', ')}`);
|
|
242
|
+
}
|
|
243
|
+
console.log('');
|
|
244
|
+
console.log(chalk.dim(' Starting daemon...'));
|
|
245
|
+
console.log('');
|
|
246
|
+
|
|
247
|
+
return { installed, keys };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* After the daemon is running, save API keys via the credential store.
|
|
252
|
+
*/
|
|
253
|
+
export async function saveKeysViaDaemon(keys, port = 31415) {
|
|
254
|
+
for (const [provider, key] of Object.entries(keys)) {
|
|
255
|
+
try {
|
|
256
|
+
const res = await fetch(`http://localhost:${port}/api/credentials/${provider}`, {
|
|
257
|
+
method: 'POST',
|
|
258
|
+
headers: { 'Content-Type': 'application/json' },
|
|
259
|
+
body: JSON.stringify({ key }),
|
|
260
|
+
});
|
|
261
|
+
if (!res.ok) throw new Error(await res.text());
|
|
262
|
+
} catch (err) {
|
|
263
|
+
console.log(chalk.yellow(` Warning: Could not save ${provider} key: ${err.message}`));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "groove-dev",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.4",
|
|
4
4
|
"description": "Open-source agent orchestration layer — the AI company OS. MCP integrations (Slack, Gmail, Stripe, 15+), agent scheduling (cron), business roles (CMO, CFO, EA). GUI dashboard, multi-agent coordination, zero cold-start, infinite sessions. Works with Claude Code, Codex, Gemini CLI, Ollama.",
|
|
5
5
|
"license": "FSL-1.1-Apache-2.0",
|
|
6
6
|
"author": "Groove Dev <hello@groovedev.ai> (https://groovedev.ai)",
|
|
@@ -1,10 +1,33 @@
|
|
|
1
1
|
// GROOVE CLI — start command
|
|
2
2
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
3
3
|
|
|
4
|
+
import { existsSync } from 'fs';
|
|
5
|
+
import { resolve } from 'path';
|
|
4
6
|
import { Daemon } from '@groove-dev/daemon';
|
|
5
7
|
import chalk from 'chalk';
|
|
8
|
+
import { runSetupWizard, saveKeysViaDaemon } from '../setup.js';
|
|
6
9
|
|
|
7
10
|
export async function start(options) {
|
|
11
|
+
const grooveDir = resolve(process.cwd(), '.groove');
|
|
12
|
+
const isFirstRun = !existsSync(resolve(grooveDir, 'config.json'));
|
|
13
|
+
|
|
14
|
+
// ── First-run interactive wizard ────────────────────────────
|
|
15
|
+
let setupKeys = {};
|
|
16
|
+
if (isFirstRun) {
|
|
17
|
+
try {
|
|
18
|
+
const result = await runSetupWizard();
|
|
19
|
+
setupKeys = result.keys || {};
|
|
20
|
+
} catch (err) {
|
|
21
|
+
// If stdin is not interactive (piped), skip wizard
|
|
22
|
+
if (err.code === 'ERR_USE_AFTER_CLOSE') {
|
|
23
|
+
console.log(chalk.dim(' Non-interactive mode — skipping setup wizard.'));
|
|
24
|
+
} else {
|
|
25
|
+
throw err;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ── Start daemon ────────────────────────────────────────────
|
|
8
31
|
console.log(chalk.bold('GROOVE') + ' starting daemon...');
|
|
9
32
|
|
|
10
33
|
try {
|
|
@@ -15,7 +38,6 @@ export async function start(options) {
|
|
|
15
38
|
|
|
16
39
|
const shutdown = async () => {
|
|
17
40
|
console.log('\nShutting down...');
|
|
18
|
-
// Force exit after 3s if stop hangs
|
|
19
41
|
const forceTimer = setTimeout(() => process.exit(1), 3000);
|
|
20
42
|
forceTimer.unref();
|
|
21
43
|
try { await daemon.stop(); } catch { /* ignore */ }
|
|
@@ -26,6 +48,12 @@ export async function start(options) {
|
|
|
26
48
|
process.on('SIGTERM', shutdown);
|
|
27
49
|
|
|
28
50
|
await daemon.start();
|
|
51
|
+
|
|
52
|
+
// Save API keys from wizard (after daemon is running)
|
|
53
|
+
if (Object.keys(setupKeys).length > 0) {
|
|
54
|
+
await saveKeysViaDaemon(setupKeys, daemon.port);
|
|
55
|
+
}
|
|
56
|
+
|
|
29
57
|
console.log(chalk.green('Ready.'));
|
|
30
58
|
} catch (err) {
|
|
31
59
|
console.error(chalk.red('Failed to start:'), err.message);
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
// GROOVE CLI — Interactive First-Run Setup Wizard
|
|
2
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
3
|
+
|
|
4
|
+
import { createInterface } from 'readline';
|
|
5
|
+
import { execSync, execFileSync } from 'child_process';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
|
|
8
|
+
const rl = () => createInterface({ input: process.stdin, output: process.stdout });
|
|
9
|
+
|
|
10
|
+
function ask(prompt) {
|
|
11
|
+
return new Promise((resolve) => {
|
|
12
|
+
const r = rl();
|
|
13
|
+
r.question(prompt, (answer) => { r.close(); resolve(answer.trim()); });
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function askMasked(prompt) {
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
const r = rl();
|
|
20
|
+
r.question(prompt, (answer) => { r.close(); resolve(answer.trim()); });
|
|
21
|
+
// Mask input by overwriting with *
|
|
22
|
+
r._writeToOutput = function (str) {
|
|
23
|
+
if (str.includes('\n') || str.includes('\r')) {
|
|
24
|
+
r.output.write('\n');
|
|
25
|
+
} else {
|
|
26
|
+
r.output.write('*');
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function cmd(command) {
|
|
33
|
+
try {
|
|
34
|
+
return execSync(command, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
35
|
+
} catch { return null; }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isInstalled(name) {
|
|
39
|
+
try {
|
|
40
|
+
execFileSync('which', [name], { stdio: 'pipe' });
|
|
41
|
+
return true;
|
|
42
|
+
} catch { return false; }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const PROVIDERS = [
|
|
46
|
+
{
|
|
47
|
+
id: 'claude-code',
|
|
48
|
+
name: 'Claude Code',
|
|
49
|
+
cli: 'claude',
|
|
50
|
+
install: 'npm i -g @anthropic-ai/claude-code',
|
|
51
|
+
auth: 'subscription',
|
|
52
|
+
description: 'Anthropic\'s CLI agent. Uses your Claude subscription (no API key needed).',
|
|
53
|
+
recommended: true,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: 'codex',
|
|
57
|
+
name: 'Codex',
|
|
58
|
+
cli: 'codex',
|
|
59
|
+
install: 'npm i -g @openai/codex',
|
|
60
|
+
auth: 'api-key',
|
|
61
|
+
envKey: 'OPENAI_API_KEY',
|
|
62
|
+
description: 'OpenAI\'s coding agent. Requires an OpenAI API key.',
|
|
63
|
+
keyHelp: 'Get your key at https://platform.openai.com/api-keys',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 'gemini',
|
|
67
|
+
name: 'Gemini CLI',
|
|
68
|
+
cli: 'gemini',
|
|
69
|
+
install: 'npm i -g @google/gemini-cli',
|
|
70
|
+
auth: 'api-key',
|
|
71
|
+
envKey: 'GEMINI_API_KEY',
|
|
72
|
+
description: 'Google\'s coding agent. Requires a Gemini API key.',
|
|
73
|
+
keyHelp: 'Get your key at https://aistudio.google.com/apikey',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: 'ollama',
|
|
77
|
+
name: 'Ollama',
|
|
78
|
+
cli: 'ollama',
|
|
79
|
+
install: process.platform === 'darwin' ? 'brew install ollama' : 'See https://ollama.ai/download',
|
|
80
|
+
auth: 'local',
|
|
81
|
+
description: 'Run models locally. No API key, no cloud. Requires 8GB+ RAM.',
|
|
82
|
+
},
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
export async function runSetupWizard() {
|
|
86
|
+
console.log('');
|
|
87
|
+
console.log(chalk.bold(' ┌──────────────────────────────────────────┐'));
|
|
88
|
+
console.log(chalk.bold(' │') + ' Welcome to ' + chalk.bold.cyan('GROOVE') + ' ' + chalk.bold('│'));
|
|
89
|
+
console.log(chalk.bold(' │') + ' Agent orchestration for AI coding ' + chalk.bold('│'));
|
|
90
|
+
console.log(chalk.bold(' └──────────────────────────────────────────┘'));
|
|
91
|
+
console.log('');
|
|
92
|
+
console.log(chalk.dim(' Let\'s get you set up. This takes about a minute.'));
|
|
93
|
+
console.log('');
|
|
94
|
+
|
|
95
|
+
// ── Step 1: System check ────────────────────────────────────
|
|
96
|
+
console.log(chalk.bold(' 1. System Check'));
|
|
97
|
+
console.log('');
|
|
98
|
+
|
|
99
|
+
// Node.js
|
|
100
|
+
const nodeVersion = process.version;
|
|
101
|
+
const nodeMajor = parseInt(nodeVersion.slice(1), 10);
|
|
102
|
+
if (nodeMajor >= 20) {
|
|
103
|
+
console.log(chalk.green(' ✓') + ` Node.js ${nodeVersion}`);
|
|
104
|
+
} else {
|
|
105
|
+
console.log(chalk.red(' ✗') + ` Node.js ${nodeVersion} — Groove requires Node.js 20+`);
|
|
106
|
+
console.log(chalk.dim(' Install the latest: https://nodejs.org'));
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// npm
|
|
111
|
+
const npmVersion = cmd('npm --version');
|
|
112
|
+
if (npmVersion) {
|
|
113
|
+
console.log(chalk.green(' ✓') + ` npm ${npmVersion}`);
|
|
114
|
+
} else {
|
|
115
|
+
console.log(chalk.red(' ✗') + ' npm not found');
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// git
|
|
120
|
+
if (isInstalled('git')) {
|
|
121
|
+
console.log(chalk.green(' ✓') + ` git ${cmd('git --version')?.replace('git version ', '') || ''}`);
|
|
122
|
+
} else {
|
|
123
|
+
console.log(chalk.yellow(' !') + ' git not found — agents may need it for version control');
|
|
124
|
+
console.log(chalk.dim(' Install: https://git-scm.com'));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
console.log('');
|
|
128
|
+
|
|
129
|
+
// ── Step 2: Provider scan ───────────────────────────────────
|
|
130
|
+
console.log(chalk.bold(' 2. AI Providers'));
|
|
131
|
+
console.log('');
|
|
132
|
+
|
|
133
|
+
const installed = [];
|
|
134
|
+
const available = [];
|
|
135
|
+
|
|
136
|
+
for (const p of PROVIDERS) {
|
|
137
|
+
if (isInstalled(p.cli)) {
|
|
138
|
+
installed.push(p);
|
|
139
|
+
const rec = p.recommended ? chalk.cyan(' (recommended)') : '';
|
|
140
|
+
console.log(chalk.green(' ✓') + ` ${p.name}${rec}`);
|
|
141
|
+
} else {
|
|
142
|
+
available.push(p);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (installed.length > 0 && available.length > 0) {
|
|
147
|
+
console.log('');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── Step 3: Install missing providers ───────────────────────
|
|
151
|
+
if (available.length > 0) {
|
|
152
|
+
console.log(chalk.dim(' Available to install:'));
|
|
153
|
+
available.forEach((p, i) => {
|
|
154
|
+
const rec = p.recommended ? chalk.cyan(' (recommended)') : '';
|
|
155
|
+
console.log(` ${chalk.bold(i + 1)}. ${p.name}${rec} — ${p.description}`);
|
|
156
|
+
});
|
|
157
|
+
console.log(` ${chalk.bold('0')}. Skip — I'll install later`);
|
|
158
|
+
console.log('');
|
|
159
|
+
|
|
160
|
+
const answer = await ask(chalk.bold(' Which providers would you like to install? ') + chalk.dim('(e.g. 1,2 or 0 to skip) '));
|
|
161
|
+
const selections = answer.split(/[,\s]+/).map((s) => parseInt(s, 10)).filter((n) => n > 0 && n <= available.length);
|
|
162
|
+
|
|
163
|
+
if (selections.length > 0) {
|
|
164
|
+
console.log('');
|
|
165
|
+
for (const idx of selections) {
|
|
166
|
+
const p = available[idx - 1];
|
|
167
|
+
console.log(` Installing ${chalk.bold(p.name)}...`);
|
|
168
|
+
try {
|
|
169
|
+
execSync(p.install, { stdio: 'inherit' });
|
|
170
|
+
console.log(chalk.green(` ✓ ${p.name} installed`));
|
|
171
|
+
installed.push(p);
|
|
172
|
+
} catch {
|
|
173
|
+
console.log(chalk.red(` ✗ ${p.name} failed to install`));
|
|
174
|
+
console.log(chalk.dim(` Try manually: ${p.install}`));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (installed.length === 0) {
|
|
181
|
+
console.log('');
|
|
182
|
+
console.log(chalk.yellow(' No providers installed.'));
|
|
183
|
+
console.log(chalk.dim(' You\'ll need at least one to spawn agents.'));
|
|
184
|
+
console.log(chalk.dim(' Recommended: npm i -g @anthropic-ai/claude-code'));
|
|
185
|
+
console.log('');
|
|
186
|
+
const cont = await ask(chalk.bold(' Continue anyway? ') + chalk.dim('[Y/n] '));
|
|
187
|
+
if (cont.toLowerCase() === 'n') {
|
|
188
|
+
console.log(chalk.dim(' Run `groove start` again after installing a provider.'));
|
|
189
|
+
process.exit(0);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
console.log('');
|
|
194
|
+
|
|
195
|
+
// ── Step 4: API key setup ───────────────────────────────────
|
|
196
|
+
const needsKey = installed.filter((p) => p.auth === 'api-key');
|
|
197
|
+
const keys = {};
|
|
198
|
+
|
|
199
|
+
if (needsKey.length > 0) {
|
|
200
|
+
console.log(chalk.bold(' 3. API Keys'));
|
|
201
|
+
console.log('');
|
|
202
|
+
|
|
203
|
+
for (const p of needsKey) {
|
|
204
|
+
console.log(` ${chalk.bold(p.name)} requires an API key.`);
|
|
205
|
+
if (p.keyHelp) console.log(chalk.dim(` ${p.keyHelp}`));
|
|
206
|
+
|
|
207
|
+
const key = await askMasked(` Enter ${p.name} API key ${chalk.dim('(or press Enter to skip)')}: `);
|
|
208
|
+
if (key) {
|
|
209
|
+
keys[p.id] = key;
|
|
210
|
+
console.log(chalk.green(` ✓ ${p.name} key saved`));
|
|
211
|
+
} else {
|
|
212
|
+
console.log(chalk.dim(` Skipped — set it later in Settings or: groove set-key ${p.id} <key>`));
|
|
213
|
+
}
|
|
214
|
+
console.log('');
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ── Step 5: Claude Code auth check ──────────────────────────
|
|
219
|
+
const hasClaude = installed.some((p) => p.id === 'claude-code');
|
|
220
|
+
if (hasClaude) {
|
|
221
|
+
console.log(chalk.bold(` ${needsKey.length > 0 ? '4' : '3'}. Claude Code Auth`));
|
|
222
|
+
console.log('');
|
|
223
|
+
console.log(chalk.dim(' Claude Code uses your Anthropic subscription (not API keys).'));
|
|
224
|
+
console.log(chalk.dim(' If you haven\'t logged in yet, run `claude` in a terminal to authenticate.'));
|
|
225
|
+
|
|
226
|
+
// Quick check if claude is authenticated
|
|
227
|
+
try {
|
|
228
|
+
const out = cmd('claude --version');
|
|
229
|
+
if (out) {
|
|
230
|
+
console.log(chalk.green(' ✓') + ` Claude Code ${out} installed`);
|
|
231
|
+
}
|
|
232
|
+
} catch { /* ignore */ }
|
|
233
|
+
console.log('');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ── Done! ──────────────────────────────────────────────────
|
|
237
|
+
console.log(chalk.bold(' Setup complete!'));
|
|
238
|
+
console.log('');
|
|
239
|
+
console.log(` Providers: ${installed.map((p) => p.name).join(', ') || 'none'}`);
|
|
240
|
+
if (Object.keys(keys).length > 0) {
|
|
241
|
+
console.log(` Keys configured: ${Object.keys(keys).map((k) => PROVIDERS.find((p) => p.id === k)?.name || k).join(', ')}`);
|
|
242
|
+
}
|
|
243
|
+
console.log('');
|
|
244
|
+
console.log(chalk.dim(' Starting daemon...'));
|
|
245
|
+
console.log('');
|
|
246
|
+
|
|
247
|
+
return { installed, keys };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* After the daemon is running, save API keys via the credential store.
|
|
252
|
+
*/
|
|
253
|
+
export async function saveKeysViaDaemon(keys, port = 31415) {
|
|
254
|
+
for (const [provider, key] of Object.entries(keys)) {
|
|
255
|
+
try {
|
|
256
|
+
const res = await fetch(`http://localhost:${port}/api/credentials/${provider}`, {
|
|
257
|
+
method: 'POST',
|
|
258
|
+
headers: { 'Content-Type': 'application/json' },
|
|
259
|
+
body: JSON.stringify({ key }),
|
|
260
|
+
});
|
|
261
|
+
if (!res.ok) throw new Error(await res.text());
|
|
262
|
+
} catch (err) {
|
|
263
|
+
console.log(chalk.yellow(` Warning: Could not save ${provider} key: ${err.message}`));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|