@vibescope/mcp-server 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cli.ts CHANGED
@@ -1,195 +1,212 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * Vibescope CLI - Enforcement verification tool
5
- *
6
- * Used by Claude Code Stop hook to verify agent compliance with Vibescope tracking.
7
- * Exit codes:
8
- * 0 = Compliant (allow exit)
9
- * 1 = Non-compliant (block exit, loop back)
10
- * 2 = Error (allow exit with warning)
11
- */
12
-
13
- import { execSync } from 'child_process';
14
-
15
- // ============================================================================
16
- // Types
17
- // ============================================================================
18
-
19
- export interface VerificationResult {
20
- status: 'compliant' | 'non_compliant' | 'no_session' | 'error';
21
- reason: string;
22
- continuation_prompt?: string;
23
- details?: {
24
- session_started: boolean;
25
- project_id: string | null;
26
- project_name: string | null;
27
- git_url: string | null;
28
- in_progress_tasks: number;
29
- tasks_completed_this_session: number;
30
- progress_logs_this_session: number;
31
- blockers_logged_this_session: number;
32
- session_duration_minutes: number | null;
33
- };
34
- }
35
-
36
- // ============================================================================
37
- // Configuration (read at runtime for testability)
38
- // ============================================================================
39
-
40
- function getApiKey(): string | undefined {
41
- return process.env.VIBESCOPE_API_KEY;
42
- }
43
-
44
- function getApiUrl(): string {
45
- return process.env.VIBESCOPE_API_URL || 'https://vibescope.dev';
46
- }
47
-
48
- // ============================================================================
49
- // Git URL Detection
50
- // ============================================================================
51
-
52
- export function normalizeGitUrl(url: string): string {
53
- // Remove .git suffix
54
- let normalized = url.replace(/\.git$/, '');
55
- // Convert SSH to HTTPS format
56
- if (normalized.startsWith('git@')) {
57
- normalized = normalized
58
- .replace(/^git@/, 'https://')
59
- .replace(/:([^/])/, '/$1');
60
- }
61
- return normalized;
62
- }
63
-
64
- export function detectGitUrl(): string | null {
65
- try {
66
- const url = execSync('git config --get remote.origin.url', {
67
- encoding: 'utf8',
68
- timeout: 5000,
69
- stdio: ['pipe', 'pipe', 'pipe'],
70
- }).trim();
71
-
72
- return normalizeGitUrl(url);
73
- } catch {
74
- return null;
75
- }
76
- }
77
-
78
- // ============================================================================
79
- // Verification Logic
80
- // ============================================================================
81
-
82
- export async function verify(
83
- gitUrl?: string,
84
- projectId?: string
85
- ): Promise<VerificationResult> {
86
- // Check environment (read at runtime for testability)
87
- const apiKey = getApiKey();
88
- if (!apiKey) {
89
- return {
90
- status: 'error',
91
- reason: 'VIBESCOPE_API_KEY environment variable not set',
92
- };
93
- }
94
-
95
- // Auto-detect git URL if not provided
96
- if (!gitUrl && !projectId) {
97
- gitUrl = detectGitUrl() || undefined;
98
- }
99
-
100
- try {
101
- const response = await fetch(`${getApiUrl()}/api/mcp/verify`, {
102
- method: 'POST',
103
- headers: {
104
- 'Content-Type': 'application/json',
105
- },
106
- body: JSON.stringify({
107
- api_key: apiKey,
108
- git_url: gitUrl,
109
- project_id: projectId,
110
- }),
111
- });
112
-
113
- const result = await response.json() as VerificationResult;
114
- return result;
115
- } catch (err) {
116
- return {
117
- status: 'error',
118
- reason: err instanceof Error ? err.message : 'Network error',
119
- };
120
- }
121
- }
122
-
123
- // ============================================================================
124
- // CLI Entry Point
125
- // ============================================================================
126
-
127
- async function main() {
128
- const args = process.argv.slice(2);
129
- const command = args[0];
130
-
131
- if (command === 'verify') {
132
- // Parse --git-url and --project-id flags
133
- let gitUrl: string | undefined;
134
- let projectId: string | undefined;
135
-
136
- for (let i = 1; i < args.length; i++) {
137
- if (args[i] === '--git-url' && args[i + 1]) {
138
- gitUrl = args[++i];
139
- } else if (args[i] === '--project-id' && args[i + 1]) {
140
- projectId = args[++i];
141
- }
142
- }
143
-
144
- const result = await verify(gitUrl, projectId);
145
- console.log(JSON.stringify(result, null, 2));
146
-
147
- // Exit codes: 0=compliant, 1=non-compliant, 2=error
148
- if (result.status === 'compliant') {
149
- process.exit(0);
150
- } else if (result.status === 'error') {
151
- process.exit(2);
152
- } else {
153
- process.exit(1);
154
- }
155
- } else if (command === 'help' || command === '--help' || command === '-h') {
156
- console.log(`
157
- Vibescope CLI - Enforcement verification tool
158
-
159
- Usage:
160
- vibescope-cli verify [options] Check Vibescope compliance before exit
161
-
162
- Options:
163
- --git-url <url> Git repository URL (auto-detected if not provided)
164
- --project-id <id> Vibescope project UUID
165
-
166
- Exit Codes:
167
- 0 Compliant - agent can exit
168
- 1 Non-compliant - agent should continue work
169
- 2 Error - allow exit with warning
170
-
171
- Environment Variables:
172
- VIBESCOPE_API_KEY Required - Your Vibescope API key
173
- VIBESCOPE_API_URL Optional - API URL (default: https://vibescope.dev)
174
- `);
175
- process.exit(0);
176
- } else {
177
- console.error('Usage: vibescope-cli verify [--git-url <url>] [--project-id <id>]');
178
- console.error(' vibescope-cli --help');
179
- process.exit(2);
180
- }
181
- }
182
-
183
- // Only run main when executed directly (not when imported for testing)
184
- const isMainModule = import.meta.url === `file://${process.argv[1]?.replace(/\\/g, '/')}`;
185
- if (isMainModule || process.argv[1]?.endsWith('cli.js')) {
186
- main().catch((err) => {
187
- console.error(
188
- JSON.stringify({
189
- status: 'error',
190
- reason: err instanceof Error ? err.message : 'Unknown error',
191
- })
192
- );
193
- process.exit(2);
194
- });
195
- }
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Vibescope CLI - Setup wizard and enforcement verification tool
5
+ *
6
+ * Commands:
7
+ * setup - Interactive wizard to configure Vibescope for your IDE
8
+ * verify - Check agent compliance with Vibescope tracking (used by hooks)
9
+ *
10
+ * Exit codes (for verify command):
11
+ * 0 = Compliant (allow exit)
12
+ * 1 = Non-compliant (block exit, loop back)
13
+ * 2 = Error (allow exit with warning)
14
+ */
15
+
16
+ import { execSync } from 'child_process';
17
+ import { runSetup } from './setup.js';
18
+
19
+ // ============================================================================
20
+ // Types
21
+ // ============================================================================
22
+
23
+ export interface VerificationResult {
24
+ status: 'compliant' | 'non_compliant' | 'no_session' | 'error';
25
+ reason: string;
26
+ continuation_prompt?: string;
27
+ details?: {
28
+ session_started: boolean;
29
+ project_id: string | null;
30
+ project_name: string | null;
31
+ git_url: string | null;
32
+ in_progress_tasks: number;
33
+ tasks_completed_this_session: number;
34
+ progress_logs_this_session: number;
35
+ blockers_logged_this_session: number;
36
+ session_duration_minutes: number | null;
37
+ };
38
+ }
39
+
40
+ // ============================================================================
41
+ // Configuration (read at runtime for testability)
42
+ // ============================================================================
43
+
44
+ function getApiKey(): string | undefined {
45
+ return process.env.VIBESCOPE_API_KEY;
46
+ }
47
+
48
+ function getApiUrl(): string {
49
+ return process.env.VIBESCOPE_API_URL || 'https://vibescope.dev';
50
+ }
51
+
52
+ // ============================================================================
53
+ // Git URL Detection
54
+ // ============================================================================
55
+
56
+ export function normalizeGitUrl(url: string): string {
57
+ // Remove .git suffix
58
+ let normalized = url.replace(/\.git$/, '');
59
+ // Convert SSH to HTTPS format
60
+ if (normalized.startsWith('git@')) {
61
+ normalized = normalized
62
+ .replace(/^git@/, 'https://')
63
+ .replace(/:([^/])/, '/$1');
64
+ }
65
+ return normalized;
66
+ }
67
+
68
+ export function detectGitUrl(): string | null {
69
+ try {
70
+ const url = execSync('git config --get remote.origin.url', {
71
+ encoding: 'utf8',
72
+ timeout: 5000,
73
+ stdio: ['pipe', 'pipe', 'pipe'],
74
+ }).trim();
75
+
76
+ return normalizeGitUrl(url);
77
+ } catch {
78
+ return null;
79
+ }
80
+ }
81
+
82
+ // ============================================================================
83
+ // Verification Logic
84
+ // ============================================================================
85
+
86
+ export async function verify(
87
+ gitUrl?: string,
88
+ projectId?: string
89
+ ): Promise<VerificationResult> {
90
+ // Check environment (read at runtime for testability)
91
+ const apiKey = getApiKey();
92
+ if (!apiKey) {
93
+ return {
94
+ status: 'error',
95
+ reason: 'VIBESCOPE_API_KEY environment variable not set',
96
+ };
97
+ }
98
+
99
+ // Auto-detect git URL if not provided
100
+ if (!gitUrl && !projectId) {
101
+ gitUrl = detectGitUrl() || undefined;
102
+ }
103
+
104
+ try {
105
+ const response = await fetch(`${getApiUrl()}/api/mcp/verify`, {
106
+ method: 'POST',
107
+ headers: {
108
+ 'Content-Type': 'application/json',
109
+ },
110
+ body: JSON.stringify({
111
+ api_key: apiKey,
112
+ git_url: gitUrl,
113
+ project_id: projectId,
114
+ }),
115
+ });
116
+
117
+ const result = await response.json() as VerificationResult;
118
+ return result;
119
+ } catch (err) {
120
+ return {
121
+ status: 'error',
122
+ reason: err instanceof Error ? err.message : 'Network error',
123
+ };
124
+ }
125
+ }
126
+
127
+ // ============================================================================
128
+ // CLI Entry Point
129
+ // ============================================================================
130
+
131
+ async function main() {
132
+ const args = process.argv.slice(2);
133
+ const command = args[0];
134
+
135
+ if (command === 'setup') {
136
+ // Run interactive setup wizard
137
+ await runSetup();
138
+ process.exit(0);
139
+ } else if (command === 'verify') {
140
+ // Parse --git-url and --project-id flags
141
+ let gitUrl: string | undefined;
142
+ let projectId: string | undefined;
143
+
144
+ for (let i = 1; i < args.length; i++) {
145
+ if (args[i] === '--git-url' && args[i + 1]) {
146
+ gitUrl = args[++i];
147
+ } else if (args[i] === '--project-id' && args[i + 1]) {
148
+ projectId = args[++i];
149
+ }
150
+ }
151
+
152
+ const result = await verify(gitUrl, projectId);
153
+ console.log(JSON.stringify(result, null, 2));
154
+
155
+ // Exit codes: 0=compliant, 1=non-compliant, 2=error
156
+ if (result.status === 'compliant') {
157
+ process.exit(0);
158
+ } else if (result.status === 'error') {
159
+ process.exit(2);
160
+ } else {
161
+ process.exit(1);
162
+ }
163
+ } else if (command === 'help' || command === '--help' || command === '-h') {
164
+ console.log(`
165
+ Vibescope CLI - Setup wizard and enforcement verification tool
166
+
167
+ Usage:
168
+ vibescope-cli setup Interactive setup wizard for your IDE
169
+ vibescope-cli verify [options] Check Vibescope compliance before exit
170
+
171
+ Setup:
172
+ Configures Vibescope MCP integration for:
173
+ - Claude Code (CLI)
174
+ - Claude Desktop
175
+ - Cursor
176
+ - Gemini CLI
177
+
178
+ Verify Options:
179
+ --git-url <url> Git repository URL (auto-detected if not provided)
180
+ --project-id <id> Vibescope project UUID
181
+
182
+ Exit Codes (verify):
183
+ 0 Compliant - agent can exit
184
+ 1 Non-compliant - agent should continue work
185
+ 2 Error - allow exit with warning
186
+
187
+ Environment Variables:
188
+ VIBESCOPE_API_KEY Required for verify - Your Vibescope API key
189
+ VIBESCOPE_API_URL Optional - API URL (default: https://vibescope.dev)
190
+ `);
191
+ process.exit(0);
192
+ } else {
193
+ console.error('Usage: vibescope-cli setup');
194
+ console.error(' vibescope-cli verify [--git-url <url>] [--project-id <id>]');
195
+ console.error(' vibescope-cli --help');
196
+ process.exit(2);
197
+ }
198
+ }
199
+
200
+ // Only run main when executed directly (not when imported for testing)
201
+ const isMainModule = import.meta.url === `file://${process.argv[1]?.replace(/\\/g, '/')}`;
202
+ if (isMainModule || process.argv[1]?.endsWith('cli.js')) {
203
+ main().catch((err) => {
204
+ console.error(
205
+ JSON.stringify({
206
+ status: 'error',
207
+ reason: err instanceof Error ? err.message : 'Unknown error',
208
+ })
209
+ );
210
+ process.exit(2);
211
+ });
212
+ }
@@ -0,0 +1,233 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+
3
+ // Mock node:fs before importing setup module
4
+ vi.mock('node:fs', () => ({
5
+ existsSync: vi.fn(),
6
+ readFileSync: vi.fn(),
7
+ writeFileSync: vi.fn(),
8
+ mkdirSync: vi.fn(),
9
+ }));
10
+
11
+ // Mock node:os
12
+ vi.mock('node:os', () => ({
13
+ homedir: vi.fn(() => '/home/testuser'),
14
+ platform: vi.fn(() => 'darwin'),
15
+ }));
16
+
17
+ import { existsSync } from 'node:fs';
18
+ import { homedir, platform } from 'node:os';
19
+ import { detectIdes, generateMcpConfig, type IdeConfig } from './setup.js';
20
+
21
+ describe('Setup module', () => {
22
+ beforeEach(() => {
23
+ vi.clearAllMocks();
24
+ vi.mocked(homedir).mockReturnValue('/home/testuser');
25
+ vi.mocked(platform).mockReturnValue('darwin');
26
+ });
27
+
28
+ describe('detectIdes', () => {
29
+ it('should always include Claude Code as detected', () => {
30
+ vi.mocked(existsSync).mockReturnValue(false);
31
+ const ides = detectIdes();
32
+ const claudeCode = ides.find(ide => ide.name === 'claude-code');
33
+
34
+ expect(claudeCode).toBeDefined();
35
+ expect(claudeCode?.detected).toBe(true);
36
+ expect(claudeCode?.configPath).toBe('.mcp.json');
37
+ expect(claudeCode?.configFormat).toBe('mcp-json');
38
+ });
39
+
40
+ it('should always include Gemini CLI as detected', () => {
41
+ vi.mocked(existsSync).mockReturnValue(false);
42
+ const ides = detectIdes();
43
+ const gemini = ides.find(ide => ide.name === 'gemini');
44
+
45
+ expect(gemini).toBeDefined();
46
+ expect(gemini?.detected).toBe(true);
47
+ expect(gemini?.configFormat).toBe('settings-json');
48
+ });
49
+
50
+ it('should detect Claude Desktop when directory exists', () => {
51
+ vi.mocked(existsSync).mockImplementation((path: unknown) => {
52
+ if (typeof path === 'string' && path.includes('Claude')) {
53
+ return true;
54
+ }
55
+ return false;
56
+ });
57
+
58
+ const ides = detectIdes();
59
+ const claudeDesktop = ides.find(ide => ide.name === 'claude-desktop');
60
+
61
+ expect(claudeDesktop).toBeDefined();
62
+ expect(claudeDesktop?.detected).toBe(true);
63
+ });
64
+
65
+ it('should not detect Claude Desktop when directory does not exist', () => {
66
+ vi.mocked(existsSync).mockReturnValue(false);
67
+
68
+ const ides = detectIdes();
69
+ const claudeDesktop = ides.find(ide => ide.name === 'claude-desktop');
70
+
71
+ expect(claudeDesktop).toBeDefined();
72
+ expect(claudeDesktop?.detected).toBe(false);
73
+ });
74
+
75
+ it('should detect Cursor when directory exists', () => {
76
+ vi.mocked(existsSync).mockImplementation((path: unknown) => {
77
+ if (typeof path === 'string' && path.includes('Cursor')) {
78
+ return true;
79
+ }
80
+ return false;
81
+ });
82
+
83
+ const ides = detectIdes();
84
+ const cursor = ides.find(ide => ide.name === 'cursor');
85
+
86
+ expect(cursor).toBeDefined();
87
+ expect(cursor?.detected).toBe(true);
88
+ });
89
+
90
+ it('should return all four IDE configs', () => {
91
+ vi.mocked(existsSync).mockReturnValue(false);
92
+ const ides = detectIdes();
93
+
94
+ expect(ides).toHaveLength(4);
95
+ expect(ides.map(ide => ide.name)).toEqual([
96
+ 'claude-code',
97
+ 'claude-desktop',
98
+ 'cursor',
99
+ 'gemini',
100
+ ]);
101
+ });
102
+
103
+ it('should use correct config paths on macOS', () => {
104
+ vi.mocked(platform).mockReturnValue('darwin');
105
+ vi.mocked(homedir).mockReturnValue('/Users/testuser');
106
+ vi.mocked(existsSync).mockReturnValue(true);
107
+
108
+ const ides = detectIdes();
109
+
110
+ const claudeDesktop = ides.find(ide => ide.name === 'claude-desktop');
111
+ // Note: path.join uses OS separator, so we check for path components
112
+ expect(claudeDesktop?.configPath).toMatch(/Library.*Application Support.*Claude/);
113
+
114
+ const cursor = ides.find(ide => ide.name === 'cursor');
115
+ expect(cursor?.configPath).toMatch(/Library.*Application Support.*Cursor/);
116
+
117
+ const gemini = ides.find(ide => ide.name === 'gemini');
118
+ expect(gemini?.configPath).toMatch(/Users.*testuser.*\.gemini.*settings\.json/);
119
+ });
120
+
121
+ it('should use correct config paths on Windows', () => {
122
+ vi.mocked(platform).mockReturnValue('win32');
123
+ vi.mocked(homedir).mockReturnValue('C:\\Users\\testuser');
124
+ vi.mocked(existsSync).mockReturnValue(true);
125
+
126
+ const ides = detectIdes();
127
+
128
+ const claudeDesktop = ides.find(ide => ide.name === 'claude-desktop');
129
+ // Note: path.join uses OS separator, so we check for path components
130
+ expect(claudeDesktop?.configPath).toMatch(/AppData.*Roaming.*Claude/);
131
+
132
+ const cursor = ides.find(ide => ide.name === 'cursor');
133
+ expect(cursor?.configPath).toMatch(/AppData.*Roaming.*Cursor/);
134
+ });
135
+
136
+ it('should use correct config paths on Linux', () => {
137
+ vi.mocked(platform).mockReturnValue('linux');
138
+ vi.mocked(homedir).mockReturnValue('/home/testuser');
139
+ vi.mocked(existsSync).mockReturnValue(true);
140
+
141
+ const ides = detectIdes();
142
+
143
+ const claudeDesktop = ides.find(ide => ide.name === 'claude-desktop');
144
+ // Note: path.join uses OS separator, so we check for path components
145
+ expect(claudeDesktop?.configPath).toMatch(/\.config.*Claude/);
146
+
147
+ const cursor = ides.find(ide => ide.name === 'cursor');
148
+ expect(cursor?.configPath).toMatch(/\.config.*Cursor/);
149
+ });
150
+ });
151
+
152
+ describe('IdeConfig types', () => {
153
+ it('should have correct config format for MCP-based IDEs', () => {
154
+ vi.mocked(existsSync).mockReturnValue(true);
155
+ const ides = detectIdes();
156
+
157
+ const claudeCode = ides.find(ide => ide.name === 'claude-code');
158
+ const claudeDesktop = ides.find(ide => ide.name === 'claude-desktop');
159
+ const cursor = ides.find(ide => ide.name === 'cursor');
160
+
161
+ expect(claudeCode?.configFormat).toBe('mcp-json');
162
+ expect(claudeDesktop?.configFormat).toBe('mcp-json');
163
+ expect(cursor?.configFormat).toBe('mcp-json');
164
+ });
165
+
166
+ it('should have correct config format for Gemini CLI', () => {
167
+ vi.mocked(existsSync).mockReturnValue(true);
168
+ const ides = detectIdes();
169
+
170
+ const gemini = ides.find(ide => ide.name === 'gemini');
171
+ expect(gemini?.configFormat).toBe('settings-json');
172
+ });
173
+ });
174
+
175
+ describe('generateMcpConfig', () => {
176
+ it('should generate correct config structure for standard MCP IDEs', () => {
177
+ const claudeCodeIde: IdeConfig = {
178
+ name: 'claude-code',
179
+ displayName: 'Claude Code (CLI)',
180
+ configPath: '.mcp.json',
181
+ detected: true,
182
+ configFormat: 'mcp-json',
183
+ };
184
+
185
+ const config = generateMcpConfig('test-api-key', claudeCodeIde);
186
+ const mcpServers = config.mcpServers as Record<string, unknown>;
187
+ const vibescope = mcpServers.vibescope as Record<string, unknown>;
188
+
189
+ expect(vibescope.command).toBe('npx');
190
+ expect(vibescope.args).toContain('@vibescope/mcp-server@latest');
191
+ expect((vibescope.env as Record<string, string>).VIBESCOPE_API_KEY).toBe('test-api-key');
192
+ // Standard MCP config should NOT have timeout/trust
193
+ expect(vibescope.timeout).toBeUndefined();
194
+ expect(vibescope.trust).toBeUndefined();
195
+ });
196
+
197
+ it('should include timeout and trust for Gemini CLI config', () => {
198
+ const geminiIde: IdeConfig = {
199
+ name: 'gemini',
200
+ displayName: 'Gemini CLI',
201
+ configPath: '~/.gemini/settings.json',
202
+ detected: true,
203
+ configFormat: 'settings-json',
204
+ };
205
+
206
+ const config = generateMcpConfig('test-api-key', geminiIde);
207
+ const mcpServers = config.mcpServers as Record<string, unknown>;
208
+ const vibescope = mcpServers.vibescope as Record<string, unknown>;
209
+
210
+ expect(vibescope.command).toBe('npx');
211
+ expect(vibescope.args).toContain('@vibescope/mcp-server@latest');
212
+ expect((vibescope.env as Record<string, string>).VIBESCOPE_API_KEY).toBe('test-api-key');
213
+ expect(vibescope.timeout).toBe(30000);
214
+ expect(vibescope.trust).toBe(true);
215
+ });
216
+
217
+ it('should use correct npx args format', () => {
218
+ const ide: IdeConfig = {
219
+ name: 'claude-code',
220
+ displayName: 'Claude Code (CLI)',
221
+ configPath: '.mcp.json',
222
+ detected: true,
223
+ configFormat: 'mcp-json',
224
+ };
225
+
226
+ const config = generateMcpConfig('my-key', ide);
227
+ const mcpServers = config.mcpServers as Record<string, unknown>;
228
+ const vibescope = mcpServers.vibescope as Record<string, unknown>;
229
+
230
+ expect(vibescope.args).toEqual(['-y', '-p', '@vibescope/mcp-server@latest', 'vibescope-mcp']);
231
+ });
232
+ });
233
+ });