@vibescope/mcp-server 0.3.0 → 0.3.3

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 (154) hide show
  1. package/dist/api-client/blockers.d.ts +46 -0
  2. package/dist/api-client/blockers.js +43 -0
  3. package/dist/api-client/cost.d.ts +112 -0
  4. package/dist/api-client/cost.js +76 -0
  5. package/dist/api-client/decisions.d.ts +55 -0
  6. package/dist/api-client/decisions.js +32 -0
  7. package/dist/api-client/discovery.d.ts +62 -0
  8. package/dist/api-client/discovery.js +21 -0
  9. package/dist/api-client/ideas.d.ts +75 -0
  10. package/dist/api-client/ideas.js +36 -0
  11. package/dist/api-client/index.d.ts +749 -0
  12. package/dist/api-client/index.js +291 -0
  13. package/dist/api-client/project.d.ts +132 -0
  14. package/dist/api-client/project.js +45 -0
  15. package/dist/api-client/session.d.ts +163 -0
  16. package/dist/api-client/session.js +52 -0
  17. package/dist/api-client/tasks.d.ts +328 -0
  18. package/dist/api-client/tasks.js +132 -0
  19. package/dist/api-client/types.d.ts +25 -0
  20. package/dist/api-client/types.js +4 -0
  21. package/dist/api-client/worktrees.d.ts +33 -0
  22. package/dist/api-client/worktrees.js +26 -0
  23. package/dist/api-client.d.ts +9 -0
  24. package/dist/api-client.js +104 -25
  25. package/dist/cli-init.d.ts +17 -0
  26. package/dist/cli-init.js +445 -0
  27. package/dist/cli.js +0 -0
  28. package/dist/handlers/cloud-agents.d.ts +21 -0
  29. package/dist/handlers/cloud-agents.js +91 -0
  30. package/dist/handlers/discovery.js +7 -0
  31. package/dist/handlers/index.d.ts +1 -0
  32. package/dist/handlers/index.js +3 -0
  33. package/dist/handlers/session.js +3 -1
  34. package/dist/handlers/tasks.js +10 -12
  35. package/dist/handlers/types.d.ts +2 -1
  36. package/dist/handlers/validation.js +5 -1
  37. package/dist/index.js +8 -3
  38. package/dist/token-tracking.js +2 -2
  39. package/dist/tools/blockers.d.ts +13 -0
  40. package/dist/tools/blockers.js +119 -0
  41. package/dist/tools/bodies-of-work.d.ts +19 -0
  42. package/dist/tools/bodies-of-work.js +280 -0
  43. package/dist/tools/cloud-agents.d.ts +9 -0
  44. package/dist/tools/cloud-agents.js +67 -0
  45. package/dist/tools/connectors.d.ts +14 -0
  46. package/dist/tools/connectors.js +188 -0
  47. package/dist/tools/cost.d.ts +11 -0
  48. package/dist/tools/cost.js +108 -0
  49. package/dist/tools/decisions.d.ts +12 -0
  50. package/dist/tools/decisions.js +108 -0
  51. package/dist/tools/deployment.d.ts +24 -0
  52. package/dist/tools/deployment.js +439 -0
  53. package/dist/tools/discovery.d.ts +10 -0
  54. package/dist/tools/discovery.js +73 -0
  55. package/dist/tools/fallback.d.ts +11 -0
  56. package/dist/tools/fallback.js +108 -0
  57. package/dist/tools/file-checkouts.d.ts +13 -0
  58. package/dist/tools/file-checkouts.js +141 -0
  59. package/dist/tools/findings.d.ts +13 -0
  60. package/dist/tools/findings.js +98 -0
  61. package/dist/tools/git-issues.d.ts +11 -0
  62. package/dist/tools/git-issues.js +127 -0
  63. package/dist/tools/ideas.d.ts +13 -0
  64. package/dist/tools/ideas.js +159 -0
  65. package/dist/tools/index.d.ts +71 -0
  66. package/dist/tools/index.js +98 -0
  67. package/dist/tools/milestones.d.ts +12 -0
  68. package/dist/tools/milestones.js +115 -0
  69. package/dist/tools/organizations.d.ts +17 -0
  70. package/dist/tools/organizations.js +221 -0
  71. package/dist/tools/progress.d.ts +9 -0
  72. package/dist/tools/progress.js +70 -0
  73. package/dist/tools/project.d.ts +13 -0
  74. package/dist/tools/project.js +199 -0
  75. package/dist/tools/requests.d.ts +10 -0
  76. package/dist/tools/requests.js +65 -0
  77. package/dist/tools/roles.d.ts +11 -0
  78. package/dist/tools/roles.js +109 -0
  79. package/dist/tools/session.d.ts +15 -0
  80. package/dist/tools/session.js +178 -0
  81. package/dist/tools/sprints.d.ts +18 -0
  82. package/dist/tools/sprints.js +295 -0
  83. package/dist/tools/tasks.d.ts +27 -0
  84. package/dist/tools/tasks.js +539 -0
  85. package/dist/tools/types.d.ts +7 -0
  86. package/dist/tools/types.js +6 -0
  87. package/dist/tools/validation.d.ts +10 -0
  88. package/dist/tools/validation.js +72 -0
  89. package/dist/tools/worktrees.d.ts +9 -0
  90. package/dist/tools/worktrees.js +63 -0
  91. package/dist/utils.d.ts +66 -0
  92. package/dist/utils.js +102 -0
  93. package/docs/TOOLS.md +55 -2
  94. package/package.json +5 -3
  95. package/scripts/generate-docs.ts +1 -1
  96. package/src/api-client/blockers.ts +86 -0
  97. package/src/api-client/cost.ts +185 -0
  98. package/src/api-client/decisions.ts +87 -0
  99. package/src/api-client/discovery.ts +81 -0
  100. package/src/api-client/ideas.ts +112 -0
  101. package/src/api-client/index.ts +378 -0
  102. package/src/api-client/project.ts +179 -0
  103. package/src/api-client/session.ts +220 -0
  104. package/src/api-client/tasks.ts +450 -0
  105. package/src/api-client/types.ts +32 -0
  106. package/src/api-client/worktrees.ts +53 -0
  107. package/src/api-client.test.ts +136 -9
  108. package/src/api-client.ts +125 -27
  109. package/src/cli-init.ts +504 -0
  110. package/src/handlers/__test-utils__.ts +2 -0
  111. package/src/handlers/cloud-agents.ts +138 -0
  112. package/src/handlers/discovery.ts +7 -0
  113. package/src/handlers/index.ts +3 -0
  114. package/src/handlers/session.ts +3 -1
  115. package/src/handlers/tasks.ts +10 -12
  116. package/src/handlers/tool-categories.test.ts +1 -1
  117. package/src/handlers/types.ts +2 -1
  118. package/src/handlers/validation.ts +6 -1
  119. package/src/index.test.ts +2 -2
  120. package/src/index.ts +8 -2
  121. package/src/token-tracking.ts +3 -2
  122. package/src/tools/blockers.ts +122 -0
  123. package/src/tools/bodies-of-work.ts +283 -0
  124. package/src/tools/cloud-agents.ts +70 -0
  125. package/src/tools/connectors.ts +191 -0
  126. package/src/tools/cost.ts +111 -0
  127. package/src/tools/decisions.ts +111 -0
  128. package/src/tools/deployment.ts +442 -0
  129. package/src/tools/discovery.ts +76 -0
  130. package/src/tools/fallback.ts +111 -0
  131. package/src/tools/file-checkouts.ts +145 -0
  132. package/src/tools/findings.ts +101 -0
  133. package/src/tools/git-issues.ts +130 -0
  134. package/src/tools/ideas.ts +162 -0
  135. package/src/tools/index.ts +131 -0
  136. package/src/tools/milestones.ts +118 -0
  137. package/src/tools/organizations.ts +224 -0
  138. package/src/tools/progress.ts +73 -0
  139. package/src/tools/project.ts +202 -0
  140. package/src/tools/requests.ts +68 -0
  141. package/src/tools/roles.ts +112 -0
  142. package/src/tools/session.ts +181 -0
  143. package/src/tools/sprints.ts +298 -0
  144. package/src/tools/tasks.ts +542 -0
  145. package/src/tools/tools.test.ts +222 -0
  146. package/src/tools/types.ts +9 -0
  147. package/src/tools/validation.ts +75 -0
  148. package/src/tools/worktrees.ts +66 -0
  149. package/src/tools.test.ts +1 -1
  150. package/src/utils.test.ts +229 -0
  151. package/src/utils.ts +117 -0
  152. package/dist/tools.d.ts +0 -2
  153. package/dist/tools.js +0 -3602
  154. package/src/tools.ts +0 -3607
@@ -0,0 +1,504 @@
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 { homedir, platform } from 'node:os';
13
+ import { join, dirname } from 'node:path';
14
+ import { exec, execSync } from 'node:child_process';
15
+ import { createInterface } from 'node:readline';
16
+
17
+ const VIBESCOPE_SETTINGS_URL = 'https://vibescope.dev/dashboard/settings';
18
+ const VIBESCOPE_API_URL = 'https://vibescope.dev';
19
+ const CREDENTIALS_DIR = join(homedir(), '.vibescope');
20
+ const CREDENTIALS_PATH = join(CREDENTIALS_DIR, 'credentials.json');
21
+
22
+ // ============================================================================
23
+ // Terminal Colors
24
+ // ============================================================================
25
+
26
+ const c = {
27
+ reset: '\x1b[0m',
28
+ bold: '\x1b[1m',
29
+ dim: '\x1b[2m',
30
+ green: '\x1b[32m',
31
+ yellow: '\x1b[33m',
32
+ blue: '\x1b[34m',
33
+ magenta: '\x1b[35m',
34
+ cyan: '\x1b[36m',
35
+ red: '\x1b[31m',
36
+ white: '\x1b[37m',
37
+ };
38
+
39
+ const icon = {
40
+ check: `${c.green}✔${c.reset}`,
41
+ cross: `${c.red}✘${c.reset}`,
42
+ arrow: `${c.cyan}→${c.reset}`,
43
+ dot: `${c.dim}·${c.reset}`,
44
+ warn: `${c.yellow}⚠${c.reset}`,
45
+ };
46
+
47
+ // ============================================================================
48
+ // Types
49
+ // ============================================================================
50
+
51
+ interface Agent {
52
+ id: string;
53
+ name: string;
54
+ detected: boolean;
55
+ configure: (apiKey: string) => Promise<void>;
56
+ }
57
+
58
+ // ============================================================================
59
+ // Prompts
60
+ // ============================================================================
61
+
62
+ function prompt(question: string): Promise<string> {
63
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
64
+ return new Promise((resolve) => {
65
+ rl.question(question, (answer) => {
66
+ rl.close();
67
+ resolve(answer.trim());
68
+ });
69
+ });
70
+ }
71
+
72
+ async function promptConfirm(question: string, defaultYes = true): Promise<boolean> {
73
+ const hint = defaultYes ? 'Y/n' : 'y/N';
74
+ const answer = await prompt(`${question} (${hint}): `);
75
+ if (!answer) return defaultYes;
76
+ return answer.toLowerCase().startsWith('y');
77
+ }
78
+
79
+ async function promptCheckboxes(label: string, items: { id: string; name: string; detected: boolean }[]): Promise<string[]> {
80
+ console.log(`\n${c.bold}${label}${c.reset}\n`);
81
+ items.forEach((item, i) => {
82
+ const tag = item.detected ? `${c.green}detected${c.reset}` : `${c.dim}not detected${c.reset}`;
83
+ console.log(` ${i + 1}) ${item.name} [${tag}]`);
84
+ });
85
+ console.log(` ${c.dim}a) All detected${c.reset}`);
86
+
87
+ const answer = await prompt(`\nSelect agents (comma-separated numbers, or 'a' for all detected): `);
88
+
89
+ if (answer.toLowerCase() === 'a') {
90
+ const detected = items.filter(i => i.detected).map(i => i.id);
91
+ return detected.length > 0 ? detected : items.map(i => i.id);
92
+ }
93
+
94
+ const indices = answer.split(',').map(s => parseInt(s.trim(), 10) - 1).filter(i => i >= 0 && i < items.length);
95
+ if (indices.length === 0) {
96
+ // Default to all detected
97
+ const detected = items.filter(i => i.detected).map(i => i.id);
98
+ return detected.length > 0 ? detected : [items[0].id];
99
+ }
100
+ return indices.map(i => items[i].id);
101
+ }
102
+
103
+ // ============================================================================
104
+ // Credential Storage
105
+ // ============================================================================
106
+
107
+ export function readCredentials(): { apiKey?: string } {
108
+ try {
109
+ if (existsSync(CREDENTIALS_PATH)) {
110
+ const data = JSON.parse(readFileSync(CREDENTIALS_PATH, 'utf-8'));
111
+ return { apiKey: data.apiKey };
112
+ }
113
+ } catch { /* ignore */ }
114
+ return {};
115
+ }
116
+
117
+ export function writeCredentials(apiKey: string): void {
118
+ if (!existsSync(CREDENTIALS_DIR)) {
119
+ mkdirSync(CREDENTIALS_DIR, { recursive: true });
120
+ }
121
+ writeFileSync(CREDENTIALS_PATH, JSON.stringify({ apiKey }, null, 2) + '\n', { mode: 0o600 });
122
+ try {
123
+ chmodSync(CREDENTIALS_PATH, 0o600);
124
+ } catch { /* Windows doesn't support chmod, that's ok */ }
125
+ }
126
+
127
+ /**
128
+ * Resolve API key from env var or credentials file.
129
+ * Used by the MCP server at startup.
130
+ */
131
+ export function resolveApiKey(): string | null {
132
+ // 1. Environment variable (highest priority)
133
+ if (process.env.VIBESCOPE_API_KEY) {
134
+ return process.env.VIBESCOPE_API_KEY;
135
+ }
136
+ // 2. Credentials file
137
+ const creds = readCredentials();
138
+ if (creds.apiKey) {
139
+ return creds.apiKey;
140
+ }
141
+ // 3. Not found
142
+ return null;
143
+ }
144
+
145
+ // ============================================================================
146
+ // Agent Detection & Configuration
147
+ // ============================================================================
148
+
149
+ function commandExists(cmd: string): boolean {
150
+ try {
151
+ execSync(`which ${cmd}`, { stdio: 'pipe', timeout: 3000 });
152
+ return true;
153
+ } catch {
154
+ // On Windows, try 'where'
155
+ if (platform() === 'win32') {
156
+ try {
157
+ execSync(`where ${cmd}`, { stdio: 'pipe', timeout: 3000 });
158
+ return true;
159
+ } catch { /* not found */ }
160
+ }
161
+ return false;
162
+ }
163
+ }
164
+
165
+ function dirExists(path: string): boolean {
166
+ return existsSync(path);
167
+ }
168
+
169
+ function detectAgents(): Agent[] {
170
+ const home = homedir();
171
+
172
+ const agents: Agent[] = [
173
+ {
174
+ id: 'claude-code',
175
+ name: 'Claude Code',
176
+ detected: commandExists('claude') || dirExists(join(home, '.claude')),
177
+ configure: configureClaudeCode,
178
+ },
179
+ {
180
+ id: 'cursor',
181
+ name: 'Cursor',
182
+ detected: commandExists('cursor') || dirExists(join(home, '.cursor')),
183
+ configure: configureCursor,
184
+ },
185
+ {
186
+ id: 'windsurf',
187
+ name: 'Windsurf',
188
+ detected: dirExists(join(home, '.windsurf')) || dirExists(join(home, '.codeium')),
189
+ configure: configureWindsurf,
190
+ },
191
+ {
192
+ id: 'gemini',
193
+ name: 'Gemini CLI',
194
+ detected: commandExists('gemini') || dirExists(join(home, '.gemini')),
195
+ configure: configureGemini,
196
+ },
197
+ ];
198
+
199
+ return agents;
200
+ }
201
+
202
+ // ============================================================================
203
+ // Agent Configurators
204
+ // ============================================================================
205
+
206
+ async function configureClaudeCode(apiKey: string): Promise<void> {
207
+ // Try using `claude mcp add` command
208
+ try {
209
+ execSync('claude --version', { stdio: 'pipe', timeout: 5000 });
210
+ // Use project scope for claude mcp add
211
+ try {
212
+ execSync(
213
+ `claude mcp add vibescope -e VIBESCOPE_API_KEY=${apiKey} -- npx -y -p @vibescope/mcp-server@latest vibescope-mcp`,
214
+ { stdio: 'pipe', timeout: 15000 }
215
+ );
216
+ console.log(` ${icon.check} Claude Code configured via ${c.cyan}claude mcp add${c.reset}`);
217
+ return;
218
+ } catch {
219
+ // claude mcp add not available (older version), fall through to manual config
220
+ }
221
+ } catch { /* claude CLI not available */ }
222
+
223
+ // Fallback: write .mcp.json
224
+ const configPath = join(process.cwd(), '.mcp.json');
225
+ writeMcpJson(configPath, apiKey);
226
+ console.log(` ${icon.check} Wrote ${c.cyan}.mcp.json${c.reset} (Claude Code project config)`);
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
+ command: 'npx',
247
+ args: ['-y', '-p', '@vibescope/mcp-server@latest', 'vibescope-mcp'],
248
+ env: { VIBESCOPE_API_KEY: apiKey },
249
+ timeout: 30000,
250
+ trust: true,
251
+ };
252
+ existing.mcpServers = mcpServers;
253
+ writeJsonFile(configPath, existing);
254
+ console.log(` ${icon.check} Wrote ${c.cyan}~/.gemini/settings.json${c.reset}`);
255
+ }
256
+
257
+ // ============================================================================
258
+ // Config File Helpers
259
+ // ============================================================================
260
+
261
+ function readJsonFile(path: string): Record<string, unknown> {
262
+ try {
263
+ if (existsSync(path)) {
264
+ return JSON.parse(readFileSync(path, 'utf-8'));
265
+ }
266
+ } catch { /* ignore */ }
267
+ return {};
268
+ }
269
+
270
+ function writeJsonFile(path: string, data: Record<string, unknown>): void {
271
+ const dir = dirname(path);
272
+ if (!existsSync(dir)) {
273
+ mkdirSync(dir, { recursive: true });
274
+ }
275
+ writeFileSync(path, JSON.stringify(data, null, 2) + '\n');
276
+ }
277
+
278
+ function writeMcpJson(configPath: string, apiKey: string): void {
279
+ const existing = readJsonFile(configPath);
280
+ const mcpServers = (existing.mcpServers as Record<string, unknown>) || {};
281
+ mcpServers['vibescope'] = {
282
+ command: 'npx',
283
+ args: ['-y', '-p', '@vibescope/mcp-server@latest', 'vibescope-mcp'],
284
+ env: { VIBESCOPE_API_KEY: apiKey },
285
+ };
286
+ existing.mcpServers = mcpServers;
287
+ writeJsonFile(configPath, existing);
288
+ }
289
+
290
+ function checkExistingConfig(agentId: string): boolean {
291
+ switch (agentId) {
292
+ case 'claude-code':
293
+ return existsSync(join(process.cwd(), '.mcp.json')) &&
294
+ readJsonFile(join(process.cwd(), '.mcp.json')).mcpServers !== undefined &&
295
+ 'vibescope' in ((readJsonFile(join(process.cwd(), '.mcp.json')).mcpServers as Record<string, unknown>) || {});
296
+ case 'cursor':
297
+ return existsSync(join(process.cwd(), '.cursor', 'mcp.json')) &&
298
+ 'vibescope' in ((readJsonFile(join(process.cwd(), '.cursor', 'mcp.json')).mcpServers as Record<string, unknown>) || {});
299
+ case 'windsurf':
300
+ return 'vibescope' in ((readJsonFile(join(homedir(), '.codeium', 'windsurf', 'mcp_config.json')).mcpServers as Record<string, unknown>) || {});
301
+ case 'gemini':
302
+ return 'vibescope' in ((readJsonFile(join(homedir(), '.gemini', 'settings.json')).mcpServers as Record<string, unknown>) || {});
303
+ default:
304
+ return false;
305
+ }
306
+ }
307
+
308
+ // ============================================================================
309
+ // API Key Validation
310
+ // ============================================================================
311
+
312
+ async function validateApiKey(apiKey: string): Promise<{ valid: boolean; message: string }> {
313
+ try {
314
+ const response = await fetch(`${VIBESCOPE_API_URL}/api/mcp/auth/validate`, {
315
+ method: 'POST',
316
+ headers: {
317
+ 'Content-Type': 'application/json',
318
+ 'X-API-Key': apiKey,
319
+ },
320
+ body: JSON.stringify({ api_key: apiKey }),
321
+ });
322
+ const data = await response.json() as { valid?: boolean; error?: string };
323
+ if (response.ok && data.valid) {
324
+ return { valid: true, message: 'API key is valid' };
325
+ }
326
+ return { valid: false, message: data.error || 'Invalid API key' };
327
+ } catch {
328
+ return { valid: true, message: 'Could not validate (network issue), proceeding' };
329
+ }
330
+ }
331
+
332
+ // ============================================================================
333
+ // Browser
334
+ // ============================================================================
335
+
336
+ function openBrowser(url: string): Promise<void> {
337
+ return new Promise((resolve) => {
338
+ const plat = platform();
339
+ const cmd = plat === 'darwin' ? `open "${url}"` :
340
+ plat === 'win32' ? `start "" "${url}"` :
341
+ `xdg-open "${url}"`;
342
+ exec(cmd, () => resolve());
343
+ });
344
+ }
345
+
346
+ // ============================================================================
347
+ // Main Init Flow
348
+ // ============================================================================
349
+
350
+ async function runInit(): Promise<void> {
351
+ console.log(`
352
+ ${c.bold}${c.magenta} ╦ ╦╦╔╗ ╔═╗╔═╗╔═╗╔═╗╔═╗╔═╗${c.reset}
353
+ ${c.bold}${c.magenta} ╚╗╔╝║╠╩╗║╣ ╚═╗║ ║ ║╠═╝║╣ ${c.reset}
354
+ ${c.bold}${c.magenta} ╚╝ ╩╚═╝╚═╝╚═╝╚═╝╚═╝╩ ╚═╝${c.reset}
355
+ ${c.dim} AI project tracking for vibe coders${c.reset}
356
+ `);
357
+
358
+ // Step 1: Detect agents
359
+ const agents = detectAgents();
360
+ const detected = agents.filter(a => a.detected);
361
+
362
+ if (detected.length > 0) {
363
+ console.log(`${icon.check} Detected agents:`);
364
+ detected.forEach(a => console.log(` ${icon.dot} ${a.name}`));
365
+ } else {
366
+ console.log(`${icon.warn} No AI agents detected automatically`);
367
+ }
368
+
369
+ // Step 2: Select agents
370
+ const selectedIds = await promptCheckboxes(
371
+ 'Which agents do you want to configure?',
372
+ agents.map(a => ({ id: a.id, name: a.name, detected: a.detected }))
373
+ );
374
+ const selectedAgents = agents.filter(a => selectedIds.includes(a.id));
375
+
376
+ if (selectedAgents.length === 0) {
377
+ console.log(`\n${icon.cross} No agents selected. Exiting.`);
378
+ process.exit(0);
379
+ }
380
+
381
+ // Step 3: API Key
382
+ let apiKey: string | null = null;
383
+
384
+ // Check env var first
385
+ if (process.env.VIBESCOPE_API_KEY) {
386
+ apiKey = process.env.VIBESCOPE_API_KEY;
387
+ console.log(`\n${icon.check} Using API key from ${c.cyan}VIBESCOPE_API_KEY${c.reset} env var`);
388
+ }
389
+
390
+ // Check credentials file
391
+ if (!apiKey) {
392
+ const creds = readCredentials();
393
+ if (creds.apiKey) {
394
+ apiKey = creds.apiKey;
395
+ console.log(`\n${icon.check} Found existing API key in ${c.cyan}~/.vibescope/credentials.json${c.reset}`);
396
+ const reuse = await promptConfirm('Use existing API key?', true);
397
+ if (!reuse) apiKey = null;
398
+ }
399
+ }
400
+
401
+ // Prompt for key if needed
402
+ if (!apiKey) {
403
+ console.log(`\n${c.bold}API Key Setup${c.reset}`);
404
+ console.log(`${icon.arrow} Get your API key at ${c.cyan}${VIBESCOPE_SETTINGS_URL}${c.reset}`);
405
+
406
+ const openIt = await promptConfirm('Open settings page in browser?', true);
407
+ if (openIt) await openBrowser(VIBESCOPE_SETTINGS_URL);
408
+
409
+ for (let attempt = 0; attempt < 3; attempt++) {
410
+ apiKey = await prompt(`\n${c.bold}Paste your API key:${c.reset} `);
411
+ if (!apiKey) {
412
+ console.log(`${icon.cross} API key is required`);
413
+ continue;
414
+ }
415
+ process.stdout.write(` Validating... `);
416
+ const result = await validateApiKey(apiKey);
417
+ if (result.valid) {
418
+ console.log(`${icon.check} ${result.message}`);
419
+ break;
420
+ } else {
421
+ console.log(`${icon.cross} ${result.message}`);
422
+ apiKey = null;
423
+ }
424
+ }
425
+
426
+ if (!apiKey) {
427
+ console.log(`\n${icon.cross} Could not get a valid API key. Exiting.`);
428
+ process.exit(1);
429
+ }
430
+
431
+ // Store credentials
432
+ try {
433
+ writeCredentials(apiKey);
434
+ console.log(`\n${icon.check} API key saved to ${c.cyan}~/.vibescope/credentials.json${c.reset}`);
435
+ } catch (err) {
436
+ console.log(`\n${icon.warn} Could not save credentials: ${err instanceof Error ? err.message : 'unknown error'}`);
437
+ console.log(` ${c.dim}Set VIBESCOPE_API_KEY env var as fallback${c.reset}`);
438
+ }
439
+ }
440
+
441
+ // Step 4: Configure each agent
442
+ console.log(`\n${c.bold}Configuring agents...${c.reset}\n`);
443
+
444
+ for (const agent of selectedAgents) {
445
+ const hasExisting = checkExistingConfig(agent.id);
446
+ if (hasExisting) {
447
+ const overwrite = await promptConfirm(` ${agent.name} already configured. Update?`, true);
448
+ if (!overwrite) {
449
+ console.log(` ${icon.dot} Skipped ${agent.name}`);
450
+ continue;
451
+ }
452
+ }
453
+ try {
454
+ await agent.configure(apiKey);
455
+ } catch (err) {
456
+ console.log(` ${icon.cross} Failed to configure ${agent.name}: ${err instanceof Error ? err.message : 'unknown error'}`);
457
+ }
458
+ }
459
+
460
+ // Done!
461
+ console.log(`
462
+ ${c.green}${c.bold}Setup complete!${c.reset}
463
+
464
+ ${c.bold}Next steps:${c.reset}
465
+ ${icon.arrow} Restart your AI agent / IDE
466
+ ${icon.arrow} Start coding — Vibescope tracks automatically
467
+
468
+ ${c.dim}Need help? https://vibescope.dev/docs${c.reset}
469
+ `);
470
+ }
471
+
472
+ // ============================================================================
473
+ // CLI Entry Point
474
+ // ============================================================================
475
+
476
+ async function main() {
477
+ const args = process.argv.slice(2);
478
+ const command = args[0];
479
+
480
+ if (command === 'init' || !command) {
481
+ await runInit();
482
+ } else if (command === '--help' || command === '-h' || command === 'help') {
483
+ console.log(`
484
+ ${c.bold}Vibescope CLI${c.reset}
485
+
486
+ Usage:
487
+ npx vibescope init Set up Vibescope for your AI agents
488
+ npx vibescope help Show this help
489
+
490
+ Docs: https://vibescope.dev/docs
491
+ `);
492
+ } else {
493
+ console.log(`Unknown command: ${command}\nRun ${c.cyan}npx vibescope help${c.reset} for usage.`);
494
+ process.exit(1);
495
+ }
496
+ }
497
+
498
+ const isMainModule = import.meta.url === `file://${process.argv[1]?.replace(/\\/g, '/')}`;
499
+ if (isMainModule || process.argv[1]?.endsWith('cli-init.js')) {
500
+ main().catch((err) => {
501
+ console.error(`${icon.cross} ${err instanceof Error ? err.message : 'Unknown error'}`);
502
+ process.exit(1);
503
+ });
504
+ }
@@ -52,6 +52,8 @@ export function createMockContext(
52
52
  instanceId: options.instanceId ?? 'instance-abc',
53
53
  currentSessionId: sessionId,
54
54
  currentPersona: options.persona ?? 'Wave',
55
+ currentRole: null,
56
+ currentProjectId: null,
55
57
  tokenUsage: options.tokenUsage ?? defaultTokenUsage,
56
58
  },
57
59
  updateSession: vi.fn(),
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Cloud Agents Handlers
3
+ *
4
+ * Handles cloud agent management:
5
+ * - cleanup_stale_cloud_agents
6
+ * - list_cloud_agents
7
+ */
8
+
9
+ import type { Handler, HandlerRegistry } from './types.js';
10
+ import { success, error } from './types.js';
11
+ import { parseArgs, uuidValidator, createEnumValidator } from '../validators.js';
12
+ import { getApiClient } from '../api-client.js';
13
+
14
+ // Argument schemas
15
+ const cleanupStaleAgentsSchema = {
16
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
17
+ stale_minutes: { type: 'number' as const, default: 5 },
18
+ include_running: { type: 'boolean' as const, default: false },
19
+ dry_run: { type: 'boolean' as const, default: false },
20
+ };
21
+
22
+ const listCloudAgentsSchema = {
23
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
24
+ status: {
25
+ type: 'string' as const,
26
+ default: 'all',
27
+ validate: createEnumValidator(['starting', 'running', 'stopped', 'failed', 'all'])
28
+ },
29
+ };
30
+
31
+ interface CleanupResponse {
32
+ cleaned: number;
33
+ failed?: number;
34
+ wouldClean?: number;
35
+ dryRun?: boolean;
36
+ agents: Array<{
37
+ id: string;
38
+ name?: string;
39
+ success?: boolean;
40
+ error?: string;
41
+ status?: string;
42
+ createdAt?: string;
43
+ }>;
44
+ message?: string;
45
+ }
46
+
47
+ interface ListAgentsResponse {
48
+ agents: Array<{
49
+ id: string;
50
+ name?: string;
51
+ status: string;
52
+ created_at: string;
53
+ last_heartbeat?: string;
54
+ public_ip?: string;
55
+ ecs_task_id?: string;
56
+ }>;
57
+ }
58
+
59
+ /**
60
+ * Clean up stale cloud agents that failed to start or lost connection.
61
+ * Only operates on agents in the specified project (security scoped).
62
+ */
63
+ export const cleanupStaleCloudAgents: Handler = async (args, ctx) => {
64
+ const { project_id, stale_minutes, include_running, dry_run } = parseArgs(args, cleanupStaleAgentsSchema);
65
+
66
+ // Ensure user has an active session with this project (security check)
67
+ if (ctx.session.currentProjectId && ctx.session.currentProjectId !== project_id) {
68
+ return error('Cannot cleanup agents for a different project than your current session');
69
+ }
70
+
71
+ const apiClient = getApiClient();
72
+
73
+ // Call the cleanup endpoint via fetch (since it's a new endpoint not in the client)
74
+ const response = await apiClient.proxy<CleanupResponse>('cleanup_stale_cloud_agents', {
75
+ project_id,
76
+ staleMinutes: stale_minutes,
77
+ includeRunning: include_running,
78
+ dryRun: dry_run,
79
+ });
80
+
81
+ if (!response.ok) {
82
+ return error(response.error || 'Failed to cleanup stale agents');
83
+ }
84
+
85
+ const data = response.data!;
86
+
87
+ if (dry_run) {
88
+ return success({
89
+ dryRun: true,
90
+ wouldClean: data.wouldClean || 0,
91
+ agents: data.agents,
92
+ message: `Found ${data.wouldClean || 0} stale agents that would be cleaned up`
93
+ });
94
+ }
95
+
96
+ return success({
97
+ cleaned: data.cleaned,
98
+ failed: data.failed || 0,
99
+ agents: data.agents,
100
+ message: `Cleaned up ${data.cleaned} stale agents`
101
+ });
102
+ };
103
+
104
+ /**
105
+ * List cloud agents for a project with optional status filter.
106
+ */
107
+ export const listCloudAgents: Handler = async (args, ctx) => {
108
+ const { project_id, status } = parseArgs(args, listCloudAgentsSchema);
109
+
110
+ // Ensure user has an active session with this project (security check)
111
+ if (ctx.session.currentProjectId && ctx.session.currentProjectId !== project_id) {
112
+ return error('Cannot list agents for a different project than your current session');
113
+ }
114
+
115
+ const apiClient = getApiClient();
116
+
117
+ const response = await apiClient.proxy<ListAgentsResponse>('list_cloud_agents', {
118
+ project_id,
119
+ status: status === 'all' ? undefined : status,
120
+ });
121
+
122
+ if (!response.ok) {
123
+ return error(response.error || 'Failed to list cloud agents');
124
+ }
125
+
126
+ return success({
127
+ agents: response.data!.agents,
128
+ count: response.data!.agents.length
129
+ });
130
+ };
131
+
132
+ /**
133
+ * Cloud agents handlers registry
134
+ */
135
+ export const cloudAgentHandlers: HandlerRegistry = {
136
+ cleanup_stale_cloud_agents: cleanupStaleCloudAgents,
137
+ list_cloud_agents: listCloudAgents,
138
+ };
@@ -324,6 +324,13 @@ export const TOOL_CATEGORIES: Record<string, { description: string; tools: Array
324
324
  { name: 'get_connector_events', brief: 'Event history' },
325
325
  ],
326
326
  },
327
+ cloud_agents: {
328
+ description: 'Cloud agent management and cleanup',
329
+ tools: [
330
+ { name: 'cleanup_stale_cloud_agents', brief: 'Clean up stale cloud agents' },
331
+ { name: 'list_cloud_agents', brief: 'List cloud agents for project' },
332
+ ],
333
+ },
327
334
  };
328
335
 
329
336
  export const discoverTools: Handler = async (args) => {
@@ -28,6 +28,7 @@ export * from './sprints.js';
28
28
  export * from './file-checkouts.js';
29
29
  export * from './roles.js';
30
30
  export * from './connectors.js';
31
+ export * from './cloud-agents.js';
31
32
 
32
33
  import type { HandlerRegistry } from './types.js';
33
34
  import { milestoneHandlers } from './milestones.js';
@@ -52,6 +53,7 @@ import { sprintHandlers } from './sprints.js';
52
53
  import { fileCheckoutHandlers } from './file-checkouts.js';
53
54
  import { roleHandlers } from './roles.js';
54
55
  import { connectorHandlers } from './connectors.js';
56
+ import { cloudAgentHandlers } from './cloud-agents.js';
55
57
 
56
58
  /**
57
59
  * Build the complete handler registry from all modules
@@ -80,5 +82,6 @@ export function buildHandlerRegistry(): HandlerRegistry {
80
82
  ...fileCheckoutHandlers,
81
83
  ...roleHandlers,
82
84
  ...connectorHandlers,
85
+ ...cloudAgentHandlers,
83
86
  };
84
87
  }