@vibescope/mcp-server 0.4.5 → 0.4.7

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 (176) hide show
  1. package/CHANGELOG.md +84 -84
  2. package/README.md +194 -194
  3. package/dist/api-client/project.d.ts +1 -0
  4. package/dist/api-client.d.ts +4 -1
  5. package/dist/api-client.js +24 -7
  6. package/dist/cli-init.js +25 -24
  7. package/dist/cli.js +26 -26
  8. package/dist/handlers/chat.d.ts +2 -0
  9. package/dist/handlers/chat.js +25 -0
  10. package/dist/handlers/discovery.js +12 -0
  11. package/dist/handlers/project.js +4 -2
  12. package/dist/handlers/tool-docs.js +1203 -1137
  13. package/dist/handlers/version.js +1 -1
  14. package/dist/index.js +159 -87
  15. package/dist/setup.js +13 -7
  16. package/dist/templates/agent-guidelines.d.ts +1 -1
  17. package/dist/templates/agent-guidelines.js +205 -187
  18. package/dist/templates/help-content.js +1621 -1621
  19. package/dist/tools/bodies-of-work.js +6 -6
  20. package/dist/tools/chat.d.ts +1 -0
  21. package/dist/tools/chat.js +24 -0
  22. package/dist/tools/cloud-agents.js +22 -22
  23. package/dist/tools/features.d.ts +13 -0
  24. package/dist/tools/features.js +151 -0
  25. package/dist/tools/index.d.ts +3 -1
  26. package/dist/tools/index.js +4 -1
  27. package/dist/tools/milestones.js +2 -2
  28. package/dist/tools/project.js +4 -0
  29. package/dist/tools/requests.js +1 -1
  30. package/dist/tools/session.js +11 -11
  31. package/dist/tools/sprints.js +9 -9
  32. package/dist/tools/tasks.js +35 -35
  33. package/dist/tools/worktrees.js +14 -14
  34. package/dist/tools.d.ts +2 -0
  35. package/dist/tools.js +3602 -0
  36. package/dist/utils.js +11 -11
  37. package/dist/version.d.ts +9 -3
  38. package/dist/version.js +56 -8
  39. package/docs/TOOLS.md +2663 -2559
  40. package/package.json +53 -53
  41. package/scripts/generate-docs.ts +212 -212
  42. package/scripts/version-bump.ts +203 -203
  43. package/src/api-client/blockers.ts +86 -86
  44. package/src/api-client/bodies-of-work.ts +194 -194
  45. package/src/api-client/chat.ts +50 -50
  46. package/src/api-client/connectors.ts +152 -152
  47. package/src/api-client/cost.ts +185 -185
  48. package/src/api-client/decisions.ts +87 -87
  49. package/src/api-client/deployment.ts +313 -313
  50. package/src/api-client/discovery.ts +81 -81
  51. package/src/api-client/fallback.ts +52 -52
  52. package/src/api-client/file-checkouts.ts +115 -115
  53. package/src/api-client/findings.ts +100 -100
  54. package/src/api-client/git-issues.ts +88 -88
  55. package/src/api-client/ideas.ts +112 -112
  56. package/src/api-client/index.ts +592 -592
  57. package/src/api-client/milestones.ts +83 -83
  58. package/src/api-client/organizations.ts +185 -185
  59. package/src/api-client/progress.ts +94 -94
  60. package/src/api-client/project.ts +180 -179
  61. package/src/api-client/requests.ts +54 -54
  62. package/src/api-client/session.ts +220 -220
  63. package/src/api-client/sprints.ts +227 -227
  64. package/src/api-client/subtasks.ts +57 -57
  65. package/src/api-client/tasks.ts +450 -450
  66. package/src/api-client/types.ts +32 -32
  67. package/src/api-client/validation.ts +60 -60
  68. package/src/api-client/worktrees.ts +53 -53
  69. package/src/api-client.test.ts +847 -847
  70. package/src/api-client.ts +2723 -2706
  71. package/src/cli-init.ts +558 -557
  72. package/src/cli.test.ts +284 -284
  73. package/src/cli.ts +204 -204
  74. package/src/handlers/__test-setup__.ts +240 -240
  75. package/src/handlers/__test-utils__.ts +89 -89
  76. package/src/handlers/blockers.test.ts +468 -468
  77. package/src/handlers/blockers.ts +172 -172
  78. package/src/handlers/bodies-of-work.test.ts +704 -704
  79. package/src/handlers/bodies-of-work.ts +526 -526
  80. package/src/handlers/chat.test.ts +185 -185
  81. package/src/handlers/chat.ts +101 -69
  82. package/src/handlers/cloud-agents.test.ts +438 -438
  83. package/src/handlers/cloud-agents.ts +156 -156
  84. package/src/handlers/connectors.test.ts +834 -834
  85. package/src/handlers/connectors.ts +229 -229
  86. package/src/handlers/cost.test.ts +462 -462
  87. package/src/handlers/cost.ts +285 -285
  88. package/src/handlers/decisions.test.ts +382 -382
  89. package/src/handlers/decisions.ts +153 -153
  90. package/src/handlers/deployment.test.ts +551 -551
  91. package/src/handlers/deployment.ts +570 -570
  92. package/src/handlers/discovery.test.ts +206 -206
  93. package/src/handlers/discovery.ts +427 -415
  94. package/src/handlers/fallback.test.ts +537 -537
  95. package/src/handlers/fallback.ts +194 -194
  96. package/src/handlers/file-checkouts.test.ts +750 -750
  97. package/src/handlers/file-checkouts.ts +185 -185
  98. package/src/handlers/findings.test.ts +633 -633
  99. package/src/handlers/findings.ts +239 -239
  100. package/src/handlers/git-issues.test.ts +631 -631
  101. package/src/handlers/git-issues.ts +136 -136
  102. package/src/handlers/ideas.test.ts +644 -644
  103. package/src/handlers/ideas.ts +207 -207
  104. package/src/handlers/index.ts +93 -93
  105. package/src/handlers/milestones.test.ts +475 -475
  106. package/src/handlers/milestones.ts +180 -180
  107. package/src/handlers/organizations.test.ts +826 -826
  108. package/src/handlers/organizations.ts +315 -315
  109. package/src/handlers/progress.test.ts +269 -269
  110. package/src/handlers/progress.ts +77 -77
  111. package/src/handlers/project.test.ts +546 -546
  112. package/src/handlers/project.ts +242 -239
  113. package/src/handlers/requests.test.ts +303 -303
  114. package/src/handlers/requests.ts +99 -99
  115. package/src/handlers/roles.test.ts +305 -305
  116. package/src/handlers/roles.ts +219 -219
  117. package/src/handlers/session.test.ts +998 -998
  118. package/src/handlers/session.ts +1105 -1105
  119. package/src/handlers/sprints.test.ts +732 -732
  120. package/src/handlers/sprints.ts +537 -537
  121. package/src/handlers/tasks.test.ts +931 -931
  122. package/src/handlers/tasks.ts +1133 -1133
  123. package/src/handlers/tool-categories.test.ts +66 -66
  124. package/src/handlers/tool-docs.test.ts +511 -511
  125. package/src/handlers/tool-docs.ts +1571 -1499
  126. package/src/handlers/types.test.ts +259 -259
  127. package/src/handlers/types.ts +176 -176
  128. package/src/handlers/validation.test.ts +582 -582
  129. package/src/handlers/validation.ts +164 -164
  130. package/src/handlers/version.ts +63 -63
  131. package/src/index.test.ts +674 -674
  132. package/src/index.ts +884 -807
  133. package/src/setup.test.ts +243 -233
  134. package/src/setup.ts +410 -404
  135. package/src/templates/agent-guidelines.ts +233 -215
  136. package/src/templates/help-content.ts +1751 -1751
  137. package/src/token-tracking.test.ts +463 -463
  138. package/src/token-tracking.ts +167 -167
  139. package/src/tools/blockers.ts +122 -122
  140. package/src/tools/bodies-of-work.ts +283 -283
  141. package/src/tools/chat.ts +72 -46
  142. package/src/tools/cloud-agents.ts +101 -101
  143. package/src/tools/connectors.ts +191 -191
  144. package/src/tools/cost.ts +111 -111
  145. package/src/tools/decisions.ts +111 -111
  146. package/src/tools/deployment.ts +455 -455
  147. package/src/tools/discovery.ts +76 -76
  148. package/src/tools/fallback.ts +111 -111
  149. package/src/tools/features.ts +154 -0
  150. package/src/tools/file-checkouts.ts +145 -145
  151. package/src/tools/findings.ts +101 -101
  152. package/src/tools/git-issues.ts +130 -130
  153. package/src/tools/ideas.ts +162 -162
  154. package/src/tools/index.ts +141 -137
  155. package/src/tools/milestones.ts +118 -118
  156. package/src/tools/organizations.ts +224 -224
  157. package/src/tools/progress.ts +73 -73
  158. package/src/tools/project.ts +206 -202
  159. package/src/tools/requests.ts +68 -68
  160. package/src/tools/roles.ts +112 -112
  161. package/src/tools/session.ts +181 -181
  162. package/src/tools/sprints.ts +298 -298
  163. package/src/tools/tasks.ts +550 -550
  164. package/src/tools/tools.test.ts +222 -222
  165. package/src/tools/types.ts +9 -9
  166. package/src/tools/validation.ts +75 -75
  167. package/src/tools/version.ts +34 -34
  168. package/src/tools/worktrees.ts +66 -66
  169. package/src/tools.test.ts +416 -416
  170. package/src/utils.test.ts +1014 -1014
  171. package/src/utils.ts +586 -586
  172. package/src/validators.test.ts +223 -223
  173. package/src/validators.ts +249 -249
  174. package/src/version.ts +162 -109
  175. package/tsconfig.json +16 -16
  176. package/vitest.config.ts +14 -14
package/src/cli-init.ts CHANGED
@@ -1,557 +1,558 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * Vibescope Init CLI
5
- *
6
- * Usage: npx vibescope init
7
- *
8
- * Detects installed AI agents, configures MCP, and stores credentials.
9
- */
10
-
11
- import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from 'node:fs';
12
- import { getAgentGuidelinesTemplate, VIBESCOPE_SECTION_START, VIBESCOPE_SECTION_END } from './templates/agent-guidelines.js';
13
- import { homedir, platform } from 'node:os';
14
- import { join, dirname } from 'node:path';
15
- import { exec, execSync } from 'node:child_process';
16
- import { createInterface } from 'node:readline';
17
-
18
- const VIBESCOPE_SETTINGS_URL = 'https://vibescope.dev/dashboard/settings';
19
- const VIBESCOPE_API_URL = 'https://vibescope.dev';
20
- const CREDENTIALS_DIR = join(homedir(), '.vibescope');
21
- const CREDENTIALS_PATH = join(CREDENTIALS_DIR, 'credentials.json');
22
-
23
- // ============================================================================
24
- // Terminal Colors
25
- // ============================================================================
26
-
27
- const c = {
28
- reset: '\x1b[0m',
29
- bold: '\x1b[1m',
30
- dim: '\x1b[2m',
31
- green: '\x1b[32m',
32
- yellow: '\x1b[33m',
33
- blue: '\x1b[34m',
34
- magenta: '\x1b[35m',
35
- cyan: '\x1b[36m',
36
- red: '\x1b[31m',
37
- white: '\x1b[37m',
38
- };
39
-
40
- const icon = {
41
- check: `${c.green}✔${c.reset}`,
42
- cross: `${c.red}✘${c.reset}`,
43
- arrow: `${c.cyan}→${c.reset}`,
44
- dot: `${c.dim}·${c.reset}`,
45
- warn: `${c.yellow}⚠${c.reset}`,
46
- };
47
-
48
- // ============================================================================
49
- // Types
50
- // ============================================================================
51
-
52
- interface Agent {
53
- id: string;
54
- name: string;
55
- detected: boolean;
56
- configure: (apiKey: string) => Promise<void>;
57
- }
58
-
59
- // ============================================================================
60
- // Prompts
61
- // ============================================================================
62
-
63
- function prompt(question: string): Promise<string> {
64
- const rl = createInterface({ input: process.stdin, output: process.stdout });
65
- return new Promise((resolve) => {
66
- rl.question(question, (answer) => {
67
- rl.close();
68
- resolve(answer.trim());
69
- });
70
- });
71
- }
72
-
73
- async function promptConfirm(question: string, defaultYes = true): Promise<boolean> {
74
- const hint = defaultYes ? 'Y/n' : 'y/N';
75
- const answer = await prompt(`${question} (${hint}): `);
76
- if (!answer) return defaultYes;
77
- return answer.toLowerCase().startsWith('y');
78
- }
79
-
80
- async function promptCheckboxes(label: string, items: { id: string; name: string; detected: boolean }[]): Promise<string[]> {
81
- console.log(`\n${c.bold}${label}${c.reset}\n`);
82
- items.forEach((item, i) => {
83
- const tag = item.detected ? `${c.green}detected${c.reset}` : `${c.dim}not detected${c.reset}`;
84
- console.log(` ${i + 1}) ${item.name} [${tag}]`);
85
- });
86
- console.log(` ${c.dim}a) All detected${c.reset}`);
87
-
88
- const answer = await prompt(`\nSelect agents (comma-separated numbers, or 'a' for all detected): `);
89
-
90
- if (answer.toLowerCase() === 'a') {
91
- const detected = items.filter(i => i.detected).map(i => i.id);
92
- return detected.length > 0 ? detected : items.map(i => i.id);
93
- }
94
-
95
- const indices = answer.split(',').map(s => parseInt(s.trim(), 10) - 1).filter(i => i >= 0 && i < items.length);
96
- if (indices.length === 0) {
97
- // Default to all detected
98
- const detected = items.filter(i => i.detected).map(i => i.id);
99
- return detected.length > 0 ? detected : [items[0].id];
100
- }
101
- return indices.map(i => items[i].id);
102
- }
103
-
104
- // ============================================================================
105
- // Credential Storage
106
- // ============================================================================
107
-
108
- export function readCredentials(): { apiKey?: string } {
109
- try {
110
- if (existsSync(CREDENTIALS_PATH)) {
111
- const data = JSON.parse(readFileSync(CREDENTIALS_PATH, 'utf-8'));
112
- return { apiKey: data.apiKey };
113
- }
114
- } catch { /* ignore */ }
115
- return {};
116
- }
117
-
118
- export function writeCredentials(apiKey: string): void {
119
- if (!existsSync(CREDENTIALS_DIR)) {
120
- mkdirSync(CREDENTIALS_DIR, { recursive: true });
121
- }
122
- writeFileSync(CREDENTIALS_PATH, JSON.stringify({ apiKey }, null, 2) + '\n', { mode: 0o600 });
123
- try {
124
- chmodSync(CREDENTIALS_PATH, 0o600);
125
- } catch { /* Windows doesn't support chmod, that's ok */ }
126
- }
127
-
128
- /**
129
- * Resolve API key from env var or credentials file.
130
- * Used by the MCP server at startup.
131
- */
132
- export function resolveApiKey(): string | null {
133
- // 1. Environment variable (highest priority)
134
- if (process.env.VIBESCOPE_API_KEY) {
135
- return process.env.VIBESCOPE_API_KEY;
136
- }
137
- // 2. Credentials file
138
- const creds = readCredentials();
139
- if (creds.apiKey) {
140
- return creds.apiKey;
141
- }
142
- // 3. Not found
143
- return null;
144
- }
145
-
146
- // ============================================================================
147
- // Agent Detection & Configuration
148
- // ============================================================================
149
-
150
- function commandExists(cmd: string): boolean {
151
- try {
152
- execSync(`which ${cmd}`, { stdio: 'pipe', timeout: 3000 });
153
- return true;
154
- } catch {
155
- // On Windows, try 'where'
156
- if (platform() === 'win32') {
157
- try {
158
- execSync(`where ${cmd}`, { stdio: 'pipe', timeout: 3000 });
159
- return true;
160
- } catch { /* not found */ }
161
- }
162
- return false;
163
- }
164
- }
165
-
166
- function dirExists(path: string): boolean {
167
- return existsSync(path);
168
- }
169
-
170
- function detectAgents(): Agent[] {
171
- const home = homedir();
172
-
173
- const agents: Agent[] = [
174
- {
175
- id: 'claude-code',
176
- name: 'Claude Code',
177
- detected: commandExists('claude') || dirExists(join(home, '.claude')),
178
- configure: configureClaudeCode,
179
- },
180
- {
181
- id: 'cursor',
182
- name: 'Cursor',
183
- detected: commandExists('cursor') || dirExists(join(home, '.cursor')),
184
- configure: configureCursor,
185
- },
186
- {
187
- id: 'windsurf',
188
- name: 'Windsurf',
189
- detected: dirExists(join(home, '.windsurf')) || dirExists(join(home, '.codeium')),
190
- configure: configureWindsurf,
191
- },
192
- {
193
- id: 'gemini',
194
- name: 'Gemini CLI',
195
- detected: commandExists('gemini') || dirExists(join(home, '.gemini')),
196
- configure: configureGemini,
197
- },
198
- ];
199
-
200
- return agents;
201
- }
202
-
203
- // ============================================================================
204
- // Agent Configurators
205
- // ============================================================================
206
-
207
- async function configureClaudeCode(apiKey: string): Promise<void> {
208
- // Write project-level .mcp.json (works reliably across all platforms)
209
- const configPath = join(process.cwd(), '.mcp.json');
210
- writeMcpJson(configPath, apiKey);
211
- console.log(` ${icon.check} Wrote ${c.cyan}.mcp.json${c.reset} (Claude Code project config)`);
212
-
213
- // Also offer to write user-level config for global access
214
- const globalConfig = platform() === 'win32'
215
- ? join(homedir(), '.claude', 'settings.json')
216
- : join(homedir(), '.claude', 'settings.json');
217
-
218
- const addGlobal = await promptConfirm(` Also add Vibescope globally (all projects)?`, true);
219
- if (addGlobal) {
220
- const existing = readJsonFile(globalConfig);
221
- const mcpServers = (existing.mcpServers as Record<string, unknown>) || {};
222
- mcpServers['vibescope'] = buildMcpServerConfig(apiKey);
223
- existing.mcpServers = mcpServers;
224
- writeJsonFile(globalConfig, existing);
225
- console.log(` ${icon.check} Wrote ${c.cyan}~/.claude/settings.json${c.reset} (global config)`);
226
- }
227
- }
228
-
229
- async function configureCursor(apiKey: string): Promise<void> {
230
- const configPath = join(process.cwd(), '.cursor', 'mcp.json');
231
- writeMcpJson(configPath, apiKey);
232
- console.log(` ${icon.check} Wrote ${c.cyan}.cursor/mcp.json${c.reset}`);
233
- }
234
-
235
- async function configureWindsurf(apiKey: string): Promise<void> {
236
- const configPath = join(homedir(), '.codeium', 'windsurf', 'mcp_config.json');
237
- writeMcpJson(configPath, apiKey);
238
- console.log(` ${icon.check} Wrote ${c.cyan}~/.codeium/windsurf/mcp_config.json${c.reset}`);
239
- }
240
-
241
- async function configureGemini(apiKey: string): Promise<void> {
242
- const configPath = join(homedir(), '.gemini', 'settings.json');
243
- const existing = readJsonFile(configPath);
244
- const mcpServers = (existing.mcpServers as Record<string, unknown>) || {};
245
- mcpServers['vibescope'] = {
246
- ...buildMcpServerConfig(apiKey),
247
- timeout: 30000,
248
- trust: true,
249
- };
250
- existing.mcpServers = mcpServers;
251
- writeJsonFile(configPath, existing);
252
- console.log(` ${icon.check} Wrote ${c.cyan}~/.gemini/settings.json${c.reset}`);
253
- }
254
-
255
- // ============================================================================
256
- // Config File Helpers
257
- // ============================================================================
258
-
259
- function readJsonFile(path: string): Record<string, unknown> {
260
- try {
261
- if (existsSync(path)) {
262
- return JSON.parse(readFileSync(path, 'utf-8'));
263
- }
264
- } catch { /* ignore */ }
265
- return {};
266
- }
267
-
268
- function writeJsonFile(path: string, data: Record<string, unknown>): void {
269
- const dir = dirname(path);
270
- if (!existsSync(dir)) {
271
- mkdirSync(dir, { recursive: true });
272
- }
273
- writeFileSync(path, JSON.stringify(data, null, 2) + '\n');
274
- }
275
-
276
- function buildMcpServerConfig(apiKey: string): Record<string, unknown> {
277
- const isWindows = platform() === 'win32';
278
- if (isWindows) {
279
- return {
280
- command: 'cmd',
281
- args: ['/c', 'npx', '-y', '-p', '@vibescope/mcp-server@latest', 'vibescope-mcp'],
282
- env: { VIBESCOPE_API_KEY: apiKey },
283
- };
284
- }
285
- return {
286
- command: 'npx',
287
- args: ['-y', '-p', '@vibescope/mcp-server@latest', 'vibescope-mcp'],
288
- env: { VIBESCOPE_API_KEY: apiKey },
289
- };
290
- }
291
-
292
- function writeMcpJson(configPath: string, apiKey: string): void {
293
- const existing = readJsonFile(configPath);
294
- const mcpServers = (existing.mcpServers as Record<string, unknown>) || {};
295
- mcpServers['vibescope'] = buildMcpServerConfig(apiKey);
296
- existing.mcpServers = mcpServers;
297
- writeJsonFile(configPath, existing);
298
- }
299
-
300
- function checkExistingConfig(agentId: string): boolean {
301
- switch (agentId) {
302
- case 'claude-code':
303
- return existsSync(join(process.cwd(), '.mcp.json')) &&
304
- readJsonFile(join(process.cwd(), '.mcp.json')).mcpServers !== undefined &&
305
- 'vibescope' in ((readJsonFile(join(process.cwd(), '.mcp.json')).mcpServers as Record<string, unknown>) || {});
306
- case 'cursor':
307
- return existsSync(join(process.cwd(), '.cursor', 'mcp.json')) &&
308
- 'vibescope' in ((readJsonFile(join(process.cwd(), '.cursor', 'mcp.json')).mcpServers as Record<string, unknown>) || {});
309
- case 'windsurf':
310
- return 'vibescope' in ((readJsonFile(join(homedir(), '.codeium', 'windsurf', 'mcp_config.json')).mcpServers as Record<string, unknown>) || {});
311
- case 'gemini':
312
- return 'vibescope' in ((readJsonFile(join(homedir(), '.gemini', 'settings.json')).mcpServers as Record<string, unknown>) || {});
313
- default:
314
- return false;
315
- }
316
- }
317
-
318
- // ============================================================================
319
- // API Key Validation
320
- // ============================================================================
321
-
322
- async function validateApiKey(apiKey: string): Promise<{ valid: boolean; message: string }> {
323
- try {
324
- const response = await fetch(`${VIBESCOPE_API_URL}/api/mcp/auth/validate`, {
325
- method: 'POST',
326
- headers: {
327
- 'Content-Type': 'application/json',
328
- 'X-API-Key': apiKey,
329
- },
330
- body: JSON.stringify({ api_key: apiKey }),
331
- });
332
- const data = await response.json() as { valid?: boolean; error?: string };
333
- if (response.ok && data.valid) {
334
- return { valid: true, message: 'API key is valid' };
335
- }
336
- return { valid: false, message: data.error || 'Invalid API key' };
337
- } catch {
338
- return { valid: true, message: 'Could not validate (network issue), proceeding' };
339
- }
340
- }
341
-
342
- // ============================================================================
343
- // Browser
344
- // ============================================================================
345
-
346
- function openBrowser(url: string): Promise<void> {
347
- return new Promise((resolve) => {
348
- const plat = platform();
349
- const cmd = plat === 'darwin' ? `open "${url}"` :
350
- plat === 'win32' ? `start "" "${url}"` :
351
- `xdg-open "${url}"`;
352
- exec(cmd, () => resolve());
353
- });
354
- }
355
-
356
- // ============================================================================
357
- // Main Init Flow
358
- // ============================================================================
359
-
360
- async function runInit(): Promise<void> {
361
- console.log(`
362
- ${c.bold}${c.magenta} ╦ ╦╦╔╗ ╔═╗╔═╗╔═╗╔═╗╔═╗╔═╗${c.reset}
363
- ${c.bold}${c.magenta} ╚╗╔╝║╠╩╗║╣ ╚═╗║ ║╠═╝║╣ ${c.reset}
364
- ${c.bold}${c.magenta} ╚╝ ╩╚═╝╚═╝╚═╝╚═╝╚═╝╩ ╚═╝${c.reset}
365
- ${c.dim} AI project tracking for vibe coders${c.reset}
366
- `);
367
-
368
- // Step 1: Detect agents
369
- const agents = detectAgents();
370
- const detected = agents.filter(a => a.detected);
371
-
372
- if (detected.length > 0) {
373
- console.log(`${icon.check} Detected agents:`);
374
- detected.forEach(a => console.log(` ${icon.dot} ${a.name}`));
375
- } else {
376
- console.log(`${icon.warn} No AI agents detected automatically`);
377
- }
378
-
379
- // Step 2: Select agents
380
- const selectedIds = await promptCheckboxes(
381
- 'Which agents do you want to configure?',
382
- agents.map(a => ({ id: a.id, name: a.name, detected: a.detected }))
383
- );
384
- const selectedAgents = agents.filter(a => selectedIds.includes(a.id));
385
-
386
- if (selectedAgents.length === 0) {
387
- console.log(`\n${icon.cross} No agents selected. Exiting.`);
388
- process.exit(0);
389
- }
390
-
391
- // Step 3: API Key
392
- let apiKey: string | null = null;
393
-
394
- // Check env var first
395
- if (process.env.VIBESCOPE_API_KEY) {
396
- apiKey = process.env.VIBESCOPE_API_KEY;
397
- console.log(`\n${icon.check} Using API key from ${c.cyan}VIBESCOPE_API_KEY${c.reset} env var`);
398
- }
399
-
400
- // Check credentials file
401
- if (!apiKey) {
402
- const creds = readCredentials();
403
- if (creds.apiKey) {
404
- apiKey = creds.apiKey;
405
- console.log(`\n${icon.check} Found existing API key in ${c.cyan}~/.vibescope/credentials.json${c.reset}`);
406
- const reuse = await promptConfirm('Use existing API key?', true);
407
- if (!reuse) apiKey = null;
408
- }
409
- }
410
-
411
- // Prompt for key if needed
412
- if (!apiKey) {
413
- console.log(`\n${c.bold}API Key Setup${c.reset}`);
414
- console.log(`${icon.arrow} Get your API key at ${c.cyan}${VIBESCOPE_SETTINGS_URL}${c.reset}`);
415
-
416
- const openIt = await promptConfirm('Open settings page in browser?', true);
417
- if (openIt) await openBrowser(VIBESCOPE_SETTINGS_URL);
418
-
419
- for (let attempt = 0; attempt < 3; attempt++) {
420
- apiKey = await prompt(`\n${c.bold}Paste your API key:${c.reset} `);
421
- if (!apiKey) {
422
- console.log(`${icon.cross} API key is required`);
423
- continue;
424
- }
425
- process.stdout.write(` Validating... `);
426
- const result = await validateApiKey(apiKey);
427
- if (result.valid) {
428
- console.log(`${icon.check} ${result.message}`);
429
- break;
430
- } else {
431
- console.log(`${icon.cross} ${result.message}`);
432
- apiKey = null;
433
- }
434
- }
435
-
436
- if (!apiKey) {
437
- console.log(`\n${icon.cross} Could not get a valid API key. Exiting.`);
438
- process.exit(1);
439
- }
440
-
441
- // Store credentials
442
- try {
443
- writeCredentials(apiKey);
444
- console.log(`\n${icon.check} API key saved to ${c.cyan}~/.vibescope/credentials.json${c.reset}`);
445
- } catch (err) {
446
- console.log(`\n${icon.warn} Could not save credentials: ${err instanceof Error ? err.message : 'unknown error'}`);
447
- console.log(` ${c.dim}Set VIBESCOPE_API_KEY env var as fallback${c.reset}`);
448
- }
449
- }
450
-
451
- // Step 4: Configure each agent
452
- console.log(`\n${c.bold}Configuring agents...${c.reset}\n`);
453
-
454
- for (const agent of selectedAgents) {
455
- const hasExisting = checkExistingConfig(agent.id);
456
- if (hasExisting) {
457
- const overwrite = await promptConfirm(` ${agent.name} already configured. Update?`, true);
458
- if (!overwrite) {
459
- console.log(` ${icon.dot} Skipped ${agent.name}`);
460
- continue;
461
- }
462
- }
463
- try {
464
- await agent.configure(apiKey);
465
- } catch (err) {
466
- console.log(` ${icon.cross} Failed to configure ${agent.name}: ${err instanceof Error ? err.message : 'unknown error'}`);
467
- }
468
- }
469
-
470
- // Step 5: Generate or update .claude/CLAUDE.md
471
- const claudeMdPath = join(process.cwd(), '.claude', 'CLAUDE.md');
472
- const claudeDir = join(process.cwd(), '.claude');
473
- if (!existsSync(claudeDir)) {
474
- mkdirSync(claudeDir, { recursive: true });
475
- }
476
-
477
- const vibescopeGuidelines = getAgentGuidelinesTemplate();
478
-
479
- if (existsSync(claudeMdPath)) {
480
- const existing = readFileSync(claudeMdPath, 'utf-8');
481
- const hasVibescopeSection = existing.includes(VIBESCOPE_SECTION_START);
482
-
483
- if (hasVibescopeSection) {
484
- // Replace existing Vibescope section
485
- const update = await promptConfirm(`\n ${c.cyan}.claude/CLAUDE.md${c.reset} already has Vibescope guidelines. Update them?`, true);
486
- if (update) {
487
- const startIdx = existing.indexOf(VIBESCOPE_SECTION_START);
488
- const endIdx = existing.indexOf(VIBESCOPE_SECTION_END);
489
- if (startIdx !== -1 && endIdx !== -1) {
490
- const updated = existing.substring(0, startIdx) + vibescopeGuidelines + existing.substring(endIdx + VIBESCOPE_SECTION_END.length);
491
- writeFileSync(claudeMdPath, updated);
492
- console.log(` ${icon.check} Updated Vibescope section in ${c.cyan}.claude/CLAUDE.md${c.reset}`);
493
- }
494
- } else {
495
- console.log(` ${icon.dot} Skipped CLAUDE.md update`);
496
- }
497
- } else {
498
- // Append Vibescope guidelines to existing file
499
- const append = await promptConfirm(`\n ${c.cyan}.claude/CLAUDE.md${c.reset} exists. Append Vibescope guidelines?`, true);
500
- if (append) {
501
- const separator = existing.endsWith('\n') ? '\n' : '\n\n';
502
- writeFileSync(claudeMdPath, existing + separator + vibescopeGuidelines);
503
- console.log(` ${icon.check} Appended Vibescope guidelines to ${c.cyan}.claude/CLAUDE.md${c.reset}`);
504
- } else {
505
- console.log(` ${icon.dot} Skipped CLAUDE.md`);
506
- }
507
- }
508
- } else {
509
- writeFileSync(claudeMdPath, vibescopeGuidelines);
510
- console.log(` ${icon.check} Created ${c.cyan}.claude/CLAUDE.md${c.reset} with agent guidelines`);
511
- }
512
-
513
- // Done!
514
- console.log(`
515
- ${c.green}${c.bold}Setup complete!${c.reset}
516
-
517
- ${c.bold}Next steps:${c.reset}
518
- ${icon.arrow} Restart your AI agent / IDE
519
- ${icon.arrow} Start coding Vibescope tracks automatically
520
-
521
- ${c.dim}Need help? https://vibescope.dev/docs${c.reset}
522
- `);
523
- }
524
-
525
- // ============================================================================
526
- // CLI Entry Point
527
- // ============================================================================
528
-
529
- async function main() {
530
- const args = process.argv.slice(2);
531
- const command = args[0];
532
-
533
- if (command === 'init' || !command) {
534
- await runInit();
535
- } else if (command === '--help' || command === '-h' || command === 'help') {
536
- console.log(`
537
- ${c.bold}Vibescope CLI${c.reset}
538
-
539
- Usage:
540
- npx vibescope init Set up Vibescope for your AI agents
541
- npx vibescope help Show this help
542
-
543
- Docs: https://vibescope.dev/docs
544
- `);
545
- } else {
546
- console.log(`Unknown command: ${command}\nRun ${c.cyan}npx vibescope help${c.reset} for usage.`);
547
- process.exit(1);
548
- }
549
- }
550
-
551
- const isMainModule = import.meta.url === `file://${process.argv[1]?.replace(/\\/g, '/')}`;
552
- if (isMainModule || process.argv[1]?.endsWith('cli-init.js')) {
553
- main().catch((err) => {
554
- console.error(`${icon.cross} ${err instanceof Error ? err.message : 'Unknown error'}`);
555
- process.exit(1);
556
- });
557
- }
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Vibescope Init CLI
5
+ *
6
+ * Usage: npx vibescope init
7
+ *
8
+ * Detects installed AI agents, configures MCP, and stores credentials.
9
+ */
10
+
11
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from 'node:fs';
12
+ import { getAgentGuidelinesTemplate, VIBESCOPE_SECTION_START, VIBESCOPE_SECTION_END } from './templates/agent-guidelines.js';
13
+ import { homedir, platform } from 'node:os';
14
+ import { join, dirname } from 'node:path';
15
+ import { exec, execSync } from 'node:child_process';
16
+ import { createInterface } from 'node:readline';
17
+
18
+ const VIBESCOPE_SETTINGS_URL = 'https://vibescope.dev/dashboard/settings';
19
+ const VIBESCOPE_API_URL = 'https://vibescope.dev';
20
+ const CREDENTIALS_DIR = join(homedir(), '.vibescope');
21
+ const CREDENTIALS_PATH = join(CREDENTIALS_DIR, 'credentials.json');
22
+
23
+ // ============================================================================
24
+ // Terminal Colors
25
+ // ============================================================================
26
+
27
+ const c = {
28
+ reset: '\x1b[0m',
29
+ bold: '\x1b[1m',
30
+ dim: '\x1b[2m',
31
+ green: '\x1b[32m',
32
+ yellow: '\x1b[33m',
33
+ blue: '\x1b[34m',
34
+ magenta: '\x1b[35m',
35
+ cyan: '\x1b[36m',
36
+ red: '\x1b[31m',
37
+ white: '\x1b[37m',
38
+ };
39
+
40
+ const icon = {
41
+ check: `${c.green}✔${c.reset}`,
42
+ cross: `${c.red}✘${c.reset}`,
43
+ arrow: `${c.cyan}→${c.reset}`,
44
+ dot: `${c.dim}·${c.reset}`,
45
+ warn: `${c.yellow}⚠${c.reset}`,
46
+ };
47
+
48
+ // ============================================================================
49
+ // Types
50
+ // ============================================================================
51
+
52
+ interface Agent {
53
+ id: string;
54
+ name: string;
55
+ detected: boolean;
56
+ configure: (apiKey: string) => Promise<void>;
57
+ }
58
+
59
+ // ============================================================================
60
+ // Prompts
61
+ // ============================================================================
62
+
63
+ function prompt(question: string): Promise<string> {
64
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
65
+ return new Promise((resolve) => {
66
+ rl.question(question, (answer) => {
67
+ rl.close();
68
+ resolve(answer.trim());
69
+ });
70
+ });
71
+ }
72
+
73
+ async function promptConfirm(question: string, defaultYes = true): Promise<boolean> {
74
+ const hint = defaultYes ? 'Y/n' : 'y/N';
75
+ const answer = await prompt(`${question} (${hint}): `);
76
+ if (!answer) return defaultYes;
77
+ return answer.toLowerCase().startsWith('y');
78
+ }
79
+
80
+ async function promptCheckboxes(label: string, items: { id: string; name: string; detected: boolean }[]): Promise<string[]> {
81
+ console.log(`\n${c.bold}${label}${c.reset}\n`);
82
+ items.forEach((item, i) => {
83
+ const tag = item.detected ? `${c.green}detected${c.reset}` : `${c.dim}not detected${c.reset}`;
84
+ console.log(` ${i + 1}) ${item.name} [${tag}]`);
85
+ });
86
+ console.log(` ${c.dim}a) All detected${c.reset}`);
87
+
88
+ const answer = await prompt(`\nSelect agents (comma-separated numbers, or 'a' for all detected): `);
89
+
90
+ if (answer.toLowerCase() === 'a') {
91
+ const detected = items.filter(i => i.detected).map(i => i.id);
92
+ return detected.length > 0 ? detected : items.map(i => i.id);
93
+ }
94
+
95
+ const indices = answer.split(',').map(s => parseInt(s.trim(), 10) - 1).filter(i => i >= 0 && i < items.length);
96
+ if (indices.length === 0) {
97
+ // Default to all detected
98
+ const detected = items.filter(i => i.detected).map(i => i.id);
99
+ return detected.length > 0 ? detected : [items[0].id];
100
+ }
101
+ return indices.map(i => items[i].id);
102
+ }
103
+
104
+ // ============================================================================
105
+ // Credential Storage
106
+ // ============================================================================
107
+
108
+ export function readCredentials(): { apiKey?: string } {
109
+ try {
110
+ if (existsSync(CREDENTIALS_PATH)) {
111
+ const data = JSON.parse(readFileSync(CREDENTIALS_PATH, 'utf-8'));
112
+ return { apiKey: data.apiKey };
113
+ }
114
+ } catch { /* ignore */ }
115
+ return {};
116
+ }
117
+
118
+ export function writeCredentials(apiKey: string): void {
119
+ if (!existsSync(CREDENTIALS_DIR)) {
120
+ mkdirSync(CREDENTIALS_DIR, { recursive: true });
121
+ }
122
+ writeFileSync(CREDENTIALS_PATH, JSON.stringify({ apiKey }, null, 2) + '\n', { mode: 0o600 });
123
+ try {
124
+ chmodSync(CREDENTIALS_PATH, 0o600);
125
+ } catch { /* Windows doesn't support chmod, that's ok */ }
126
+ }
127
+
128
+ /**
129
+ * Resolve API key from env var or credentials file.
130
+ * Used by the MCP server at startup.
131
+ */
132
+ export function resolveApiKey(): string | null {
133
+ // 1. Environment variable (highest priority)
134
+ if (process.env.VIBESCOPE_API_KEY) {
135
+ return process.env.VIBESCOPE_API_KEY;
136
+ }
137
+ // 2. Credentials file
138
+ const creds = readCredentials();
139
+ if (creds.apiKey) {
140
+ return creds.apiKey;
141
+ }
142
+ // 3. Not found
143
+ return null;
144
+ }
145
+
146
+ // ============================================================================
147
+ // Agent Detection & Configuration
148
+ // ============================================================================
149
+
150
+ function commandExists(cmd: string): boolean {
151
+ try {
152
+ execSync(`which ${cmd}`, { stdio: 'pipe', timeout: 3000 });
153
+ return true;
154
+ } catch {
155
+ // On Windows, try 'where'
156
+ if (platform() === 'win32') {
157
+ try {
158
+ execSync(`where ${cmd}`, { stdio: 'pipe', timeout: 3000 });
159
+ return true;
160
+ } catch { /* not found */ }
161
+ }
162
+ return false;
163
+ }
164
+ }
165
+
166
+ function dirExists(path: string): boolean {
167
+ return existsSync(path);
168
+ }
169
+
170
+ function detectAgents(): Agent[] {
171
+ const home = homedir();
172
+
173
+ const agents: Agent[] = [
174
+ {
175
+ id: 'claude-code',
176
+ name: 'Claude Code',
177
+ detected: commandExists('claude') || dirExists(join(home, '.claude')),
178
+ configure: configureClaudeCode,
179
+ },
180
+ {
181
+ id: 'cursor',
182
+ name: 'Cursor',
183
+ detected: commandExists('cursor') || dirExists(join(home, '.cursor')),
184
+ configure: configureCursor,
185
+ },
186
+ {
187
+ id: 'windsurf',
188
+ name: 'Windsurf',
189
+ detected: dirExists(join(home, '.windsurf')) || dirExists(join(home, '.codeium')),
190
+ configure: configureWindsurf,
191
+ },
192
+ {
193
+ id: 'gemini',
194
+ name: 'Gemini CLI',
195
+ detected: commandExists('gemini') || dirExists(join(home, '.gemini')),
196
+ configure: configureGemini,
197
+ },
198
+ ];
199
+
200
+ return agents;
201
+ }
202
+
203
+ // ============================================================================
204
+ // Agent Configurators
205
+ // ============================================================================
206
+
207
+ async function configureClaudeCode(apiKey: string): Promise<void> {
208
+ // Write project-level .mcp.json (works reliably across all platforms)
209
+ const configPath = join(process.cwd(), '.mcp.json');
210
+ writeMcpJson(configPath, apiKey);
211
+ console.log(` ${icon.check} Wrote ${c.cyan}.mcp.json${c.reset} (Claude Code project config)`);
212
+
213
+ // Also offer to write user-level config for global access
214
+ const globalConfig = platform() === 'win32'
215
+ ? join(homedir(), '.claude', 'settings.json')
216
+ : join(homedir(), '.claude', 'settings.json');
217
+
218
+ const addGlobal = await promptConfirm(` Also add Vibescope globally (all projects)?`, true);
219
+ if (addGlobal) {
220
+ const existing = readJsonFile(globalConfig);
221
+ const mcpServers = (existing.mcpServers as Record<string, unknown>) || {};
222
+ mcpServers['vibescope'] = buildMcpServerConfig(apiKey);
223
+ existing.mcpServers = mcpServers;
224
+ writeJsonFile(globalConfig, existing);
225
+ console.log(` ${icon.check} Wrote ${c.cyan}~/.claude/settings.json${c.reset} (global config)`);
226
+ }
227
+ }
228
+
229
+ async function configureCursor(apiKey: string): Promise<void> {
230
+ const configPath = join(process.cwd(), '.cursor', 'mcp.json');
231
+ writeMcpJson(configPath, apiKey);
232
+ console.log(` ${icon.check} Wrote ${c.cyan}.cursor/mcp.json${c.reset}`);
233
+ }
234
+
235
+ async function configureWindsurf(apiKey: string): Promise<void> {
236
+ const configPath = join(homedir(), '.codeium', 'windsurf', 'mcp_config.json');
237
+ writeMcpJson(configPath, apiKey);
238
+ console.log(` ${icon.check} Wrote ${c.cyan}~/.codeium/windsurf/mcp_config.json${c.reset}`);
239
+ }
240
+
241
+ async function configureGemini(apiKey: string): Promise<void> {
242
+ const configPath = join(homedir(), '.gemini', 'settings.json');
243
+ const existing = readJsonFile(configPath);
244
+ const mcpServers = (existing.mcpServers as Record<string, unknown>) || {};
245
+ mcpServers['vibescope'] = {
246
+ ...buildMcpServerConfig(apiKey),
247
+ timeout: 30000,
248
+ trust: true,
249
+ };
250
+ existing.mcpServers = mcpServers;
251
+ writeJsonFile(configPath, existing);
252
+ console.log(` ${icon.check} Wrote ${c.cyan}~/.gemini/settings.json${c.reset}`);
253
+ }
254
+
255
+ // ============================================================================
256
+ // Config File Helpers
257
+ // ============================================================================
258
+
259
+ function readJsonFile(path: string): Record<string, unknown> {
260
+ try {
261
+ if (existsSync(path)) {
262
+ return JSON.parse(readFileSync(path, 'utf-8'));
263
+ }
264
+ } catch { /* ignore */ }
265
+ return {};
266
+ }
267
+
268
+ function writeJsonFile(path: string, data: Record<string, unknown>): void {
269
+ const dir = dirname(path);
270
+ if (!existsSync(dir)) {
271
+ mkdirSync(dir, { recursive: true });
272
+ }
273
+ writeFileSync(path, JSON.stringify(data, null, 2) + '\n');
274
+ }
275
+
276
+ function buildMcpServerConfig(apiKey: string): Record<string, unknown> {
277
+ const isWindows = platform() === 'win32';
278
+ // Prefer globally installed binary (instant start) with npx fallback
279
+ if (isWindows) {
280
+ return {
281
+ command: 'cmd',
282
+ args: ['/c', 'where vibescope-mcp >nul 2>&1 && vibescope-mcp || npx -y -p @vibescope/mcp-server@latest vibescope-mcp'],
283
+ env: { VIBESCOPE_API_KEY: apiKey },
284
+ };
285
+ }
286
+ return {
287
+ command: 'bash',
288
+ args: ['-c', 'command -v vibescope-mcp >/dev/null 2>&1 && exec vibescope-mcp || exec npx -y -p @vibescope/mcp-server@latest vibescope-mcp'],
289
+ env: { VIBESCOPE_API_KEY: apiKey },
290
+ };
291
+ }
292
+
293
+ function writeMcpJson(configPath: string, apiKey: string): void {
294
+ const existing = readJsonFile(configPath);
295
+ const mcpServers = (existing.mcpServers as Record<string, unknown>) || {};
296
+ mcpServers['vibescope'] = buildMcpServerConfig(apiKey);
297
+ existing.mcpServers = mcpServers;
298
+ writeJsonFile(configPath, existing);
299
+ }
300
+
301
+ function checkExistingConfig(agentId: string): boolean {
302
+ switch (agentId) {
303
+ case 'claude-code':
304
+ return existsSync(join(process.cwd(), '.mcp.json')) &&
305
+ readJsonFile(join(process.cwd(), '.mcp.json')).mcpServers !== undefined &&
306
+ 'vibescope' in ((readJsonFile(join(process.cwd(), '.mcp.json')).mcpServers as Record<string, unknown>) || {});
307
+ case 'cursor':
308
+ return existsSync(join(process.cwd(), '.cursor', 'mcp.json')) &&
309
+ 'vibescope' in ((readJsonFile(join(process.cwd(), '.cursor', 'mcp.json')).mcpServers as Record<string, unknown>) || {});
310
+ case 'windsurf':
311
+ return 'vibescope' in ((readJsonFile(join(homedir(), '.codeium', 'windsurf', 'mcp_config.json')).mcpServers as Record<string, unknown>) || {});
312
+ case 'gemini':
313
+ return 'vibescope' in ((readJsonFile(join(homedir(), '.gemini', 'settings.json')).mcpServers as Record<string, unknown>) || {});
314
+ default:
315
+ return false;
316
+ }
317
+ }
318
+
319
+ // ============================================================================
320
+ // API Key Validation
321
+ // ============================================================================
322
+
323
+ async function validateApiKey(apiKey: string): Promise<{ valid: boolean; message: string }> {
324
+ try {
325
+ const response = await fetch(`${VIBESCOPE_API_URL}/api/mcp/auth/validate`, {
326
+ method: 'POST',
327
+ headers: {
328
+ 'Content-Type': 'application/json',
329
+ 'X-API-Key': apiKey,
330
+ },
331
+ body: JSON.stringify({ api_key: apiKey }),
332
+ });
333
+ const data = await response.json() as { valid?: boolean; error?: string };
334
+ if (response.ok && data.valid) {
335
+ return { valid: true, message: 'API key is valid' };
336
+ }
337
+ return { valid: false, message: data.error || 'Invalid API key' };
338
+ } catch {
339
+ return { valid: true, message: 'Could not validate (network issue), proceeding' };
340
+ }
341
+ }
342
+
343
+ // ============================================================================
344
+ // Browser
345
+ // ============================================================================
346
+
347
+ function openBrowser(url: string): Promise<void> {
348
+ return new Promise((resolve) => {
349
+ const plat = platform();
350
+ const cmd = plat === 'darwin' ? `open "${url}"` :
351
+ plat === 'win32' ? `start "" "${url}"` :
352
+ `xdg-open "${url}"`;
353
+ exec(cmd, () => resolve());
354
+ });
355
+ }
356
+
357
+ // ============================================================================
358
+ // Main Init Flow
359
+ // ============================================================================
360
+
361
+ async function runInit(): Promise<void> {
362
+ console.log(`
363
+ ${c.bold}${c.magenta} ╦╦╔╗ ╔═╗╔═╗╔═╗╔═╗╔═╗╔═╗${c.reset}
364
+ ${c.bold}${c.magenta} ╚╗╔╝║╠╩╗║╣ ╚═╗║ ║ ║╠═╝║╣ ${c.reset}
365
+ ${c.bold}${c.magenta} ╚╝ ╩╚═╝╚═╝╚═╝╚═╝╚═╝╩ ╚═╝${c.reset}
366
+ ${c.dim} AI project tracking for vibe coders${c.reset}
367
+ `);
368
+
369
+ // Step 1: Detect agents
370
+ const agents = detectAgents();
371
+ const detected = agents.filter(a => a.detected);
372
+
373
+ if (detected.length > 0) {
374
+ console.log(`${icon.check} Detected agents:`);
375
+ detected.forEach(a => console.log(` ${icon.dot} ${a.name}`));
376
+ } else {
377
+ console.log(`${icon.warn} No AI agents detected automatically`);
378
+ }
379
+
380
+ // Step 2: Select agents
381
+ const selectedIds = await promptCheckboxes(
382
+ 'Which agents do you want to configure?',
383
+ agents.map(a => ({ id: a.id, name: a.name, detected: a.detected }))
384
+ );
385
+ const selectedAgents = agents.filter(a => selectedIds.includes(a.id));
386
+
387
+ if (selectedAgents.length === 0) {
388
+ console.log(`\n${icon.cross} No agents selected. Exiting.`);
389
+ process.exit(0);
390
+ }
391
+
392
+ // Step 3: API Key
393
+ let apiKey: string | null = null;
394
+
395
+ // Check env var first
396
+ if (process.env.VIBESCOPE_API_KEY) {
397
+ apiKey = process.env.VIBESCOPE_API_KEY;
398
+ console.log(`\n${icon.check} Using API key from ${c.cyan}VIBESCOPE_API_KEY${c.reset} env var`);
399
+ }
400
+
401
+ // Check credentials file
402
+ if (!apiKey) {
403
+ const creds = readCredentials();
404
+ if (creds.apiKey) {
405
+ apiKey = creds.apiKey;
406
+ console.log(`\n${icon.check} Found existing API key in ${c.cyan}~/.vibescope/credentials.json${c.reset}`);
407
+ const reuse = await promptConfirm('Use existing API key?', true);
408
+ if (!reuse) apiKey = null;
409
+ }
410
+ }
411
+
412
+ // Prompt for key if needed
413
+ if (!apiKey) {
414
+ console.log(`\n${c.bold}API Key Setup${c.reset}`);
415
+ console.log(`${icon.arrow} Get your API key at ${c.cyan}${VIBESCOPE_SETTINGS_URL}${c.reset}`);
416
+
417
+ const openIt = await promptConfirm('Open settings page in browser?', true);
418
+ if (openIt) await openBrowser(VIBESCOPE_SETTINGS_URL);
419
+
420
+ for (let attempt = 0; attempt < 3; attempt++) {
421
+ apiKey = await prompt(`\n${c.bold}Paste your API key:${c.reset} `);
422
+ if (!apiKey) {
423
+ console.log(`${icon.cross} API key is required`);
424
+ continue;
425
+ }
426
+ process.stdout.write(` Validating... `);
427
+ const result = await validateApiKey(apiKey);
428
+ if (result.valid) {
429
+ console.log(`${icon.check} ${result.message}`);
430
+ break;
431
+ } else {
432
+ console.log(`${icon.cross} ${result.message}`);
433
+ apiKey = null;
434
+ }
435
+ }
436
+
437
+ if (!apiKey) {
438
+ console.log(`\n${icon.cross} Could not get a valid API key. Exiting.`);
439
+ process.exit(1);
440
+ }
441
+
442
+ // Store credentials
443
+ try {
444
+ writeCredentials(apiKey);
445
+ console.log(`\n${icon.check} API key saved to ${c.cyan}~/.vibescope/credentials.json${c.reset}`);
446
+ } catch (err) {
447
+ console.log(`\n${icon.warn} Could not save credentials: ${err instanceof Error ? err.message : 'unknown error'}`);
448
+ console.log(` ${c.dim}Set VIBESCOPE_API_KEY env var as fallback${c.reset}`);
449
+ }
450
+ }
451
+
452
+ // Step 4: Configure each agent
453
+ console.log(`\n${c.bold}Configuring agents...${c.reset}\n`);
454
+
455
+ for (const agent of selectedAgents) {
456
+ const hasExisting = checkExistingConfig(agent.id);
457
+ if (hasExisting) {
458
+ const overwrite = await promptConfirm(` ${agent.name} already configured. Update?`, true);
459
+ if (!overwrite) {
460
+ console.log(` ${icon.dot} Skipped ${agent.name}`);
461
+ continue;
462
+ }
463
+ }
464
+ try {
465
+ await agent.configure(apiKey);
466
+ } catch (err) {
467
+ console.log(` ${icon.cross} Failed to configure ${agent.name}: ${err instanceof Error ? err.message : 'unknown error'}`);
468
+ }
469
+ }
470
+
471
+ // Step 5: Generate or update .claude/CLAUDE.md
472
+ const claudeMdPath = join(process.cwd(), '.claude', 'CLAUDE.md');
473
+ const claudeDir = join(process.cwd(), '.claude');
474
+ if (!existsSync(claudeDir)) {
475
+ mkdirSync(claudeDir, { recursive: true });
476
+ }
477
+
478
+ const vibescopeGuidelines = getAgentGuidelinesTemplate();
479
+
480
+ if (existsSync(claudeMdPath)) {
481
+ const existing = readFileSync(claudeMdPath, 'utf-8');
482
+ const hasVibescopeSection = existing.includes(VIBESCOPE_SECTION_START);
483
+
484
+ if (hasVibescopeSection) {
485
+ // Replace existing Vibescope section
486
+ const update = await promptConfirm(`\n ${c.cyan}.claude/CLAUDE.md${c.reset} already has Vibescope guidelines. Update them?`, true);
487
+ if (update) {
488
+ const startIdx = existing.indexOf(VIBESCOPE_SECTION_START);
489
+ const endIdx = existing.indexOf(VIBESCOPE_SECTION_END);
490
+ if (startIdx !== -1 && endIdx !== -1) {
491
+ const updated = existing.substring(0, startIdx) + vibescopeGuidelines + existing.substring(endIdx + VIBESCOPE_SECTION_END.length);
492
+ writeFileSync(claudeMdPath, updated);
493
+ console.log(` ${icon.check} Updated Vibescope section in ${c.cyan}.claude/CLAUDE.md${c.reset}`);
494
+ }
495
+ } else {
496
+ console.log(` ${icon.dot} Skipped CLAUDE.md update`);
497
+ }
498
+ } else {
499
+ // Append Vibescope guidelines to existing file
500
+ const append = await promptConfirm(`\n ${c.cyan}.claude/CLAUDE.md${c.reset} exists. Append Vibescope guidelines?`, true);
501
+ if (append) {
502
+ const separator = existing.endsWith('\n') ? '\n' : '\n\n';
503
+ writeFileSync(claudeMdPath, existing + separator + vibescopeGuidelines);
504
+ console.log(` ${icon.check} Appended Vibescope guidelines to ${c.cyan}.claude/CLAUDE.md${c.reset}`);
505
+ } else {
506
+ console.log(` ${icon.dot} Skipped CLAUDE.md`);
507
+ }
508
+ }
509
+ } else {
510
+ writeFileSync(claudeMdPath, vibescopeGuidelines);
511
+ console.log(` ${icon.check} Created ${c.cyan}.claude/CLAUDE.md${c.reset} with agent guidelines`);
512
+ }
513
+
514
+ // Done!
515
+ console.log(`
516
+ ${c.green}${c.bold}Setup complete!${c.reset}
517
+
518
+ ${c.bold}Next steps:${c.reset}
519
+ ${icon.arrow} Restart your AI agent / IDE
520
+ ${icon.arrow} Start coding — Vibescope tracks automatically
521
+
522
+ ${c.dim}Need help? https://vibescope.dev/docs${c.reset}
523
+ `);
524
+ }
525
+
526
+ // ============================================================================
527
+ // CLI Entry Point
528
+ // ============================================================================
529
+
530
+ async function main() {
531
+ const args = process.argv.slice(2);
532
+ const command = args[0];
533
+
534
+ if (command === 'init' || !command) {
535
+ await runInit();
536
+ } else if (command === '--help' || command === '-h' || command === 'help') {
537
+ console.log(`
538
+ ${c.bold}Vibescope CLI${c.reset}
539
+
540
+ Usage:
541
+ npx vibescope init Set up Vibescope for your AI agents
542
+ npx vibescope help Show this help
543
+
544
+ Docs: https://vibescope.dev/docs
545
+ `);
546
+ } else {
547
+ console.log(`Unknown command: ${command}\nRun ${c.cyan}npx vibescope help${c.reset} for usage.`);
548
+ process.exit(1);
549
+ }
550
+ }
551
+
552
+ const isMainModule = import.meta.url === `file://${process.argv[1]?.replace(/\\/g, '/')}`;
553
+ if (isMainModule || process.argv[1]?.endsWith('cli-init.js')) {
554
+ main().catch((err) => {
555
+ console.error(`${icon.cross} ${err instanceof Error ? err.message : 'Unknown error'}`);
556
+ process.exit(1);
557
+ });
558
+ }