@vibe-assurance/cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,178 @@
1
+ # @vibe-assurance/cli
2
+
3
+ Connect Claude Code to your Vibe Assurance governance platform.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g @vibe-assurance/cli
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ 1. **Login to Vibe Assurance:**
14
+ ```bash
15
+ vibe login
16
+ ```
17
+ This opens your browser for authentication.
18
+
19
+ 2. **Configure Claude Code:**
20
+ ```bash
21
+ vibe setup-claude
22
+ ```
23
+ This adds the Vibe Assurance MCP server to your Claude Code configuration.
24
+
25
+ 3. **Restart Claude Code** and start using governance tools!
26
+
27
+ ## Usage
28
+
29
+ ### Available Commands
30
+
31
+ | Command | Description |
32
+ |---------|-------------|
33
+ | `vibe login` | Authenticate with Vibe Assurance |
34
+ | `vibe logout` | Clear stored credentials |
35
+ | `vibe mcp-server` | Start the MCP server (used by Claude Code) |
36
+ | `vibe setup-claude` | Configure Claude Code to use Vibe Assurance |
37
+
38
+ ### Available Tools in Claude Code
39
+
40
+ Once configured, Claude Code has access to these tools:
41
+
42
+ | Tool | Description |
43
+ |------|-------------|
44
+ | `vibe_get_role` | Get an AI analyst role prompt |
45
+ | `vibe_list_roles` | List available roles |
46
+ | `vibe_get_context` | Get your governance context (CRs, risks, vulns) |
47
+ | `vibe_get_template` | Get a document template |
48
+ | `vibe_list_templates` | List available templates |
49
+ | `vibe_store_artifact` | Store a created document |
50
+ | `vibe_update_artifact` | Update an existing artifact |
51
+ | `vibe_list_artifacts` | List your stored artifacts |
52
+ | `vibe_get_artifact` | Get a specific artifact |
53
+ | `vibe_delete_artifact` | Delete an artifact |
54
+
55
+ ### Example Session
56
+
57
+ ```
58
+ User: Create a change request for adding user authentication
59
+
60
+ Claude: I'll help you create a change request. Let me get your context first.
61
+
62
+ [Calls vibe_get_context]
63
+ [Calls vibe_get_role("implementation-planner")]
64
+ [Calls vibe_get_template("change-request")]
65
+
66
+ Based on your governance context, your next CR ID is CR-2026-001.
67
+
68
+ [Creates the CR documents...]
69
+
70
+ Done! Would you like me to store this in Vibe Assurance?
71
+
72
+ User: Yes
73
+
74
+ Claude: [Calls vibe_store_artifact(...)]
75
+
76
+ Your CR-2026-001 has been stored! View it at:
77
+ https://vibeassurance.app/governance/artifacts/CR-2026-001
78
+ ```
79
+
80
+ ## Configuration
81
+
82
+ ### Environment Variables
83
+
84
+ | Variable | Default | Description |
85
+ |----------|---------|-------------|
86
+ | `VIBE_API_URL` | `https://agent-platform-prod.azurewebsites.net` | API base URL |
87
+
88
+ For development, you can point to a local instance:
89
+
90
+ ```bash
91
+ VIBE_API_URL=http://localhost:3000 vibe login
92
+ ```
93
+
94
+ ### Credential Storage
95
+
96
+ Credentials are stored securely using:
97
+ - **macOS**: Keychain Access
98
+ - **Windows**: Credential Manager
99
+ - **Linux**: libsecret (if available)
100
+
101
+ If the system keychain is unavailable, credentials fall back to `~/.vibe/credentials.json` with restricted permissions (600).
102
+
103
+ ## Requirements
104
+
105
+ - **Node.js 18** or later
106
+ - **Vibe Assurance account**
107
+ - **Claude Code**
108
+
109
+ ## Troubleshooting
110
+
111
+ ### "Not authenticated" error
112
+
113
+ Run `vibe login` to authenticate. If you see this error during MCP server startup, your session may have expired.
114
+
115
+ ### MCP server not connecting
116
+
117
+ 1. Run `vibe setup-claude` again
118
+ 2. Restart Claude Code completely (not just reload)
119
+ 3. Check that `vibe` command is in your PATH
120
+
121
+ ### "Port 38274 is already in use"
122
+
123
+ Another `vibe login` process is running. Wait for it to complete or terminate it.
124
+
125
+ ### Keychain access denied
126
+
127
+ The CLI will automatically fall back to file-based storage. This is secure but less convenient.
128
+
129
+ On Linux, install `libsecret` for keychain support:
130
+ ```bash
131
+ # Ubuntu/Debian
132
+ sudo apt-get install libsecret-1-dev
133
+
134
+ # Fedora
135
+ sudo dnf install libsecret-devel
136
+ ```
137
+
138
+ ### Debug mode
139
+
140
+ To see detailed logs:
141
+ ```bash
142
+ DEBUG=vibe* vibe mcp-server
143
+ ```
144
+
145
+ ## Uninstalling
146
+
147
+ ```bash
148
+ # Remove the CLI
149
+ npm uninstall -g @vibe-assurance/cli
150
+
151
+ # Clear stored credentials
152
+ vibe logout
153
+
154
+ # Or manually:
155
+ rm -rf ~/.vibe/
156
+ ```
157
+
158
+ Then remove the MCP configuration from Claude Code:
159
+ 1. Open `~/.claude/mcp.json` (or your platform's config location)
160
+ 2. Remove the `vibeassurance` entry from `mcpServers`
161
+ 3. Restart Claude Code
162
+
163
+ ## Security
164
+
165
+ - Credentials are stored in the OS keychain when available
166
+ - Tokens are never logged or exposed in console output
167
+ - All API communication uses HTTPS
168
+ - Tokens auto-refresh when expired
169
+
170
+ ## License
171
+
172
+ MIT
173
+
174
+ ## Related
175
+
176
+ - [Vibe Assurance](https://vibeassurance.app) - Governance platform
177
+ - [Claude Code](https://claude.ai/code) - AI coding assistant
178
+ - [MCP Protocol](https://modelcontextprotocol.io) - Model Context Protocol
package/bin/vibe.js ADDED
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Load .env file if present (for local development)
4
+ const path = require('path');
5
+ require('dotenv').config({ path: path.join(__dirname, '..', '.env') });
6
+
7
+ const { program } = require('commander');
8
+ const pkg = require('../package.json');
9
+
10
+ // Commands
11
+ const login = require('../src/commands/login');
12
+ const logout = require('../src/commands/logout');
13
+ const mcpServer = require('../src/commands/mcp-server');
14
+ const setupClaude = require('../src/commands/setup-claude');
15
+
16
+ program
17
+ .name('vibe')
18
+ .description('Vibe Assurance CLI - Connect Claude Code to your governance platform')
19
+ .version(pkg.version);
20
+
21
+ program
22
+ .command('login')
23
+ .description('Authenticate with Vibe Assurance')
24
+ .action(login);
25
+
26
+ program
27
+ .command('logout')
28
+ .description('Clear stored credentials')
29
+ .action(logout);
30
+
31
+ program
32
+ .command('mcp-server')
33
+ .description('Start the MCP server for Claude Code')
34
+ .action(mcpServer);
35
+
36
+ program
37
+ .command('setup-claude')
38
+ .description('Configure Claude Code to use the Vibe Assurance MCP server')
39
+ .action(setupClaude);
40
+
41
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@vibe-assurance/cli",
3
+ "version": "1.0.0",
4
+ "description": "Vibe Assurance CLI - Connect Claude Code to your governance platform",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "vibe": "./bin/vibe.js"
8
+ },
9
+ "scripts": {
10
+ "test": "jest",
11
+ "lint": "eslint src/"
12
+ },
13
+ "keywords": [
14
+ "vibe-assurance",
15
+ "governance",
16
+ "mcp",
17
+ "claude-code",
18
+ "compliance",
19
+ "security"
20
+ ],
21
+ "author": "Vibe Assurance",
22
+ "license": "MIT",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/vibeassurance/cli.git"
26
+ },
27
+ "engines": {
28
+ "node": ">=18.0.0"
29
+ },
30
+ "dependencies": {
31
+ "@modelcontextprotocol/sdk": "^1.0.0",
32
+ "axios": "^1.6.0",
33
+ "chalk": "^4.1.2",
34
+ "commander": "^12.0.0",
35
+ "dotenv": "^16.6.1",
36
+ "keytar": "^7.9.0",
37
+ "open": "^8.4.2",
38
+ "ora": "^5.4.1"
39
+ },
40
+ "devDependencies": {
41
+ "eslint": "^8.56.0",
42
+ "jest": "^29.7.0"
43
+ }
44
+ }
@@ -0,0 +1,132 @@
1
+ const axios = require('axios');
2
+ const { getCredentials, storeCredentials, deleteCredentials } = require('../config/credentials');
3
+
4
+ // Default to production URL, can be overridden via environment variable
5
+ const API_BASE_URL = process.env.VIBE_API_URL || 'https://agent-platform-prod.azurewebsites.net';
6
+
7
+ /**
8
+ * Create an authenticated axios instance with token refresh handling
9
+ * @returns {Promise<import('axios').AxiosInstance>}
10
+ */
11
+ async function createClient() {
12
+ const creds = await getCredentials();
13
+ if (!creds || !creds.accessToken) {
14
+ throw new Error('Not authenticated. Run: vibe login');
15
+ }
16
+
17
+ const client = axios.create({
18
+ baseURL: API_BASE_URL,
19
+ headers: {
20
+ 'Authorization': `Bearer ${creds.accessToken}`,
21
+ 'Content-Type': 'application/json',
22
+ 'User-Agent': 'vibe-cli/1.0.0'
23
+ },
24
+ timeout: 30000 // 30 second timeout
25
+ });
26
+
27
+ // Response interceptor for automatic token refresh on 401
28
+ client.interceptors.response.use(
29
+ response => response,
30
+ async error => {
31
+ const originalRequest = error.config;
32
+
33
+ // If 401 and we haven't tried refreshing yet
34
+ if (error.response?.status === 401 && !originalRequest._retry && creds.refreshToken) {
35
+ originalRequest._retry = true;
36
+
37
+ try {
38
+ const newTokens = await refreshToken(creds.refreshToken);
39
+ await storeCredentials(newTokens);
40
+
41
+ // Update the failed request with new token
42
+ originalRequest.headers['Authorization'] = `Bearer ${newTokens.accessToken}`;
43
+ return client(originalRequest);
44
+ } catch (refreshError) {
45
+ // Refresh failed - clear credentials and require re-login
46
+ await deleteCredentials();
47
+ throw new Error('Session expired. Please run: vibe login');
48
+ }
49
+ }
50
+
51
+ // Format error message
52
+ const errorMessage = error.response?.data?.error ||
53
+ error.response?.data?.message ||
54
+ error.message ||
55
+ 'Unknown error';
56
+ throw new Error(errorMessage);
57
+ }
58
+ );
59
+
60
+ return client;
61
+ }
62
+
63
+ /**
64
+ * Refresh access token using refresh token
65
+ * @param {string} refreshTokenValue
66
+ * @returns {Promise<Object>} New token object
67
+ */
68
+ async function refreshToken(refreshTokenValue) {
69
+ const response = await axios.post(`${API_BASE_URL}/api/auth/refresh`, {
70
+ refreshToken: refreshTokenValue
71
+ });
72
+
73
+ return {
74
+ accessToken: response.data.token || response.data.accessToken,
75
+ refreshToken: response.data.refreshToken || refreshTokenValue,
76
+ expiresAt: response.data.expiresAt || new Date(Date.now() + 3600 * 1000).toISOString()
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Make a GET request to the API
82
+ * @param {string} path - API path (e.g., '/api/mcp/roles')
83
+ * @returns {Promise<any>} Response data
84
+ */
85
+ async function get(path) {
86
+ const client = await createClient();
87
+ const response = await client.get(path);
88
+ return response.data;
89
+ }
90
+
91
+ /**
92
+ * Make a POST request to the API
93
+ * @param {string} path - API path
94
+ * @param {Object} data - Request body
95
+ * @returns {Promise<any>} Response data
96
+ */
97
+ async function post(path, data) {
98
+ const client = await createClient();
99
+ const response = await client.post(path, data);
100
+ return response.data;
101
+ }
102
+
103
+ /**
104
+ * Make a PUT request to the API
105
+ * @param {string} path - API path
106
+ * @param {Object} data - Request body
107
+ * @returns {Promise<any>} Response data
108
+ */
109
+ async function put(path, data) {
110
+ const client = await createClient();
111
+ const response = await client.put(path, data);
112
+ return response.data;
113
+ }
114
+
115
+ /**
116
+ * Make a DELETE request to the API
117
+ * @param {string} path - API path
118
+ * @returns {Promise<any>} Response data
119
+ */
120
+ async function del(path) {
121
+ const client = await createClient();
122
+ const response = await client.delete(path);
123
+ return response.data;
124
+ }
125
+
126
+ module.exports = {
127
+ get,
128
+ post,
129
+ put,
130
+ delete: del,
131
+ API_BASE_URL
132
+ };
@@ -0,0 +1,234 @@
1
+ const open = require('open');
2
+ const http = require('http');
3
+ const { URL } = require('url');
4
+ const chalk = require('chalk');
5
+ const ora = require('ora');
6
+ const { storeCredentials, hasValidCredentials } = require('../config/credentials');
7
+ const { API_BASE_URL } = require('../api/client');
8
+
9
+ // Local callback server port (chosen to be unlikely to conflict)
10
+ const CALLBACK_PORT = 38274;
11
+ const CALLBACK_URL = `http://localhost:${CALLBACK_PORT}/callback`;
12
+
13
+ /**
14
+ * Login command - authenticates user via OAuth flow
15
+ */
16
+ async function login() {
17
+ // Check if already logged in
18
+ if (await hasValidCredentials()) {
19
+ console.log(chalk.yellow('You are already logged in.'));
20
+ console.log('Run `vibe logout` to sign out first.');
21
+ return;
22
+ }
23
+
24
+ const spinner = ora('Opening browser for authentication...').start();
25
+
26
+ // Track server for cleanup
27
+ let server = null;
28
+
29
+ // Cleanup function to ensure server is closed
30
+ const cleanup = () => {
31
+ if (server) {
32
+ try {
33
+ server.close();
34
+ server = null;
35
+ } catch (e) {
36
+ // Ignore close errors
37
+ }
38
+ }
39
+ };
40
+
41
+ // Handle process termination signals
42
+ process.on('SIGINT', () => {
43
+ cleanup();
44
+ process.exit(1);
45
+ });
46
+ process.on('SIGTERM', () => {
47
+ cleanup();
48
+ process.exit(1);
49
+ });
50
+
51
+ // Create a promise that will resolve when auth completes or reject on error/timeout
52
+ const authPromise = new Promise((resolve, reject) => {
53
+ // Create local server to receive OAuth callback
54
+ server = http.createServer(async (req, res) => {
55
+ try {
56
+ const url = new URL(req.url, `http://localhost:${CALLBACK_PORT}`);
57
+
58
+ if (url.pathname === '/callback') {
59
+ const accessToken = url.searchParams.get('access_token');
60
+ const refreshToken = url.searchParams.get('refresh_token');
61
+ const expiresIn = url.searchParams.get('expires_in');
62
+ const error = url.searchParams.get('error');
63
+
64
+ if (error) {
65
+ // Authentication failed
66
+ res.writeHead(400, { 'Content-Type': 'text/html' });
67
+ res.end(getErrorHtml(error));
68
+ server.close();
69
+ reject(new Error(error));
70
+ return;
71
+ }
72
+
73
+ if (accessToken) {
74
+ // Calculate expiry time
75
+ const expiresAt = expiresIn
76
+ ? new Date(Date.now() + parseInt(expiresIn) * 1000)
77
+ : new Date(Date.now() + 3600 * 1000); // Default 1 hour
78
+
79
+ // Store credentials securely
80
+ await storeCredentials({
81
+ accessToken,
82
+ refreshToken,
83
+ expiresAt: expiresAt.toISOString()
84
+ });
85
+
86
+ // Send success response to browser
87
+ res.writeHead(200, { 'Content-Type': 'text/html' });
88
+ res.end(getSuccessHtml());
89
+
90
+ server.close();
91
+ resolve();
92
+ } else {
93
+ res.writeHead(400, { 'Content-Type': 'text/html' });
94
+ res.end(getErrorHtml('No access token received'));
95
+ server.close();
96
+ reject(new Error('No access token received'));
97
+ }
98
+ } else {
99
+ // Ignore other requests
100
+ res.writeHead(404);
101
+ res.end();
102
+ }
103
+ } catch (err) {
104
+ res.writeHead(500);
105
+ res.end();
106
+ server.close();
107
+ reject(err);
108
+ }
109
+ });
110
+
111
+ // Handle server errors
112
+ server.on('error', (err) => {
113
+ if (err.code === 'EADDRINUSE') {
114
+ reject(new Error(`Port ${CALLBACK_PORT} is already in use. Please close any other vibe login attempts.`));
115
+ } else {
116
+ reject(err);
117
+ }
118
+ });
119
+
120
+ // Start listening and open browser
121
+ server.listen(CALLBACK_PORT, async () => {
122
+ try {
123
+ const authUrl = `${API_BASE_URL}/api/auth/cli?callback=${encodeURIComponent(CALLBACK_URL)}`;
124
+ await open(authUrl);
125
+ spinner.text = 'Waiting for authentication in browser...';
126
+ } catch (err) {
127
+ server.close();
128
+ reject(new Error(`Failed to open browser: ${err.message}`));
129
+ }
130
+ });
131
+
132
+ // Timeout after 5 minutes
133
+ setTimeout(() => {
134
+ server.close();
135
+ reject(new Error('Authentication timed out after 5 minutes'));
136
+ }, 5 * 60 * 1000);
137
+ });
138
+
139
+ try {
140
+ await authPromise;
141
+ cleanup();
142
+ spinner.succeed('Successfully authenticated!');
143
+ console.log(chalk.green('\nYou are now logged in to Vibe Assurance.'));
144
+ console.log('\nNext steps:');
145
+ console.log(' 1. Run `vibe setup-claude` to configure Claude Code');
146
+ console.log(' 2. Restart Claude Code');
147
+ console.log(' 3. Start using vibe_* tools in your conversations');
148
+ } catch (err) {
149
+ cleanup();
150
+ spinner.fail(`Authentication failed: ${err.message}`);
151
+ process.exit(1);
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Generate success HTML page
157
+ * Matches the main app's branding with Tailwind CSS and emerald colors
158
+ */
159
+ function getSuccessHtml() {
160
+ return `
161
+ <!DOCTYPE html>
162
+ <html lang="en">
163
+ <head>
164
+ <meta charset="UTF-8">
165
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
166
+ <title>Login Successful | Vibe Assurance</title>
167
+ <script src="https://cdn.tailwindcss.com"></script>
168
+ <script>tailwind.config={darkMode:'class',theme:{extend:{colors:{brand:{500:'#10b981',600:'#059669'}}}}}</script>
169
+ <script>(function(){var d=document.documentElement;var t=localStorage.getItem('theme');if(t==='dark'||(!t&&matchMedia('(prefers-color-scheme:dark)').matches))d.classList.add('dark')})();</script>
170
+ </head>
171
+ <body class="min-h-screen flex flex-col items-center justify-center p-4 bg-zinc-50 dark:bg-zinc-950 transition-colors">
172
+ <div class="absolute inset-0 pointer-events-none opacity-40 dark:opacity-10 bg-[radial-gradient(#cbd5e1_1px,transparent_1px)] [background-size:16px_16px]"></div>
173
+
174
+ <div class="mb-8 text-center relative z-10">
175
+ <div class="flex items-center justify-center gap-3 text-2xl font-bold text-zinc-900 dark:text-white tracking-tight">
176
+ <div class="w-10 h-10 bg-emerald-600 rounded-lg flex items-center justify-center text-white text-xl shadow-sm">V</div>
177
+ Vibe Assurance
178
+ </div>
179
+ </div>
180
+
181
+ <div class="max-w-md w-full bg-white dark:bg-zinc-900 p-8 rounded-xl shadow-xl border border-zinc-200 dark:border-zinc-800 text-center relative z-10">
182
+ <div class="w-16 h-16 bg-emerald-100 dark:bg-emerald-900/30 rounded-full flex items-center justify-center mx-auto mb-4">
183
+ <svg class="w-8 h-8 text-emerald-600 dark:text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
184
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
185
+ </svg>
186
+ </div>
187
+ <h1 class="text-xl font-bold text-zinc-900 dark:text-white mb-2">Login Successful!</h1>
188
+ <p class="text-zinc-500 dark:text-zinc-400 text-sm">You can close this window and return to your terminal.</p>
189
+ </div>
190
+ </body>
191
+ </html>`;
192
+ }
193
+
194
+ /**
195
+ * Generate error HTML page
196
+ * Matches the main app's branding with Tailwind CSS
197
+ */
198
+ function getErrorHtml(error) {
199
+ return `
200
+ <!DOCTYPE html>
201
+ <html lang="en">
202
+ <head>
203
+ <meta charset="UTF-8">
204
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
205
+ <title>Login Failed | Vibe Assurance</title>
206
+ <script src="https://cdn.tailwindcss.com"></script>
207
+ <script>tailwind.config={darkMode:'class',theme:{extend:{colors:{brand:{500:'#10b981',600:'#059669'}}}}}</script>
208
+ <script>(function(){var d=document.documentElement;var t=localStorage.getItem('theme');if(t==='dark'||(!t&&matchMedia('(prefers-color-scheme:dark)').matches))d.classList.add('dark')})();</script>
209
+ </head>
210
+ <body class="min-h-screen flex flex-col items-center justify-center p-4 bg-zinc-50 dark:bg-zinc-950 transition-colors">
211
+ <div class="absolute inset-0 pointer-events-none opacity-40 dark:opacity-10 bg-[radial-gradient(#cbd5e1_1px,transparent_1px)] [background-size:16px_16px]"></div>
212
+
213
+ <div class="mb-8 text-center relative z-10">
214
+ <div class="flex items-center justify-center gap-3 text-2xl font-bold text-zinc-900 dark:text-white tracking-tight">
215
+ <div class="w-10 h-10 bg-emerald-600 rounded-lg flex items-center justify-center text-white text-xl shadow-sm">V</div>
216
+ Vibe Assurance
217
+ </div>
218
+ </div>
219
+
220
+ <div class="max-w-md w-full bg-white dark:bg-zinc-900 p-8 rounded-xl shadow-xl border border-zinc-200 dark:border-zinc-800 text-center relative z-10">
221
+ <div class="w-16 h-16 bg-red-100 dark:bg-red-900/30 rounded-full flex items-center justify-center mx-auto mb-4">
222
+ <svg class="w-8 h-8 text-red-600 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
223
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
224
+ </svg>
225
+ </div>
226
+ <h1 class="text-xl font-bold text-zinc-900 dark:text-white mb-2">Login Failed</h1>
227
+ <p class="text-red-600 dark:text-red-400 text-sm mb-2">${error}</p>
228
+ <p class="text-zinc-500 dark:text-zinc-400 text-sm">Please close this window and try again.</p>
229
+ </div>
230
+ </body>
231
+ </html>`;
232
+ }
233
+
234
+ module.exports = login;
@@ -0,0 +1,22 @@
1
+ const chalk = require('chalk');
2
+ const { deleteCredentials, hasValidCredentials } = require('../config/credentials');
3
+
4
+ /**
5
+ * Logout command - clears stored credentials
6
+ */
7
+ async function logout() {
8
+ const wasLoggedIn = await hasValidCredentials();
9
+
10
+ // Delete credentials from all storage locations
11
+ await deleteCredentials();
12
+
13
+ if (wasLoggedIn) {
14
+ console.log(chalk.green('Successfully logged out.'));
15
+ console.log('\nYour local credentials have been cleared.');
16
+ console.log('Your data remains safe in your Vibe Assurance account.');
17
+ } else {
18
+ console.log(chalk.yellow('You were not logged in.'));
19
+ }
20
+ }
21
+
22
+ module.exports = logout;
@@ -0,0 +1,29 @@
1
+ const chalk = require('chalk');
2
+ const { hasValidCredentials } = require('../config/credentials');
3
+ const startServer = require('../mcp/server');
4
+
5
+ /**
6
+ * MCP Server command
7
+ *
8
+ * Starts the MCP server that Claude Code connects to.
9
+ * Requires authentication (run `vibe login` first).
10
+ */
11
+ async function mcpServerCommand() {
12
+ // Check if user is authenticated
13
+ if (!(await hasValidCredentials())) {
14
+ // Use stderr for messages (stdout is for MCP protocol)
15
+ console.error(chalk.red('Not authenticated.'));
16
+ console.error('Please run: vibe login');
17
+ process.exit(1);
18
+ }
19
+
20
+ try {
21
+ // Start the MCP server
22
+ await startServer();
23
+ } catch (error) {
24
+ console.error(chalk.red('Failed to start MCP server:'), error.message);
25
+ process.exit(1);
26
+ }
27
+ }
28
+
29
+ module.exports = mcpServerCommand;
@@ -0,0 +1,112 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const chalk = require('chalk');
5
+
6
+ /**
7
+ * Possible locations for Claude Code MCP configuration
8
+ * Ordered by preference (first existing one will be used)
9
+ */
10
+ const MCP_CONFIG_PATHS = [
11
+ // Standard location
12
+ path.join(os.homedir(), '.claude', 'mcp.json'),
13
+ // macOS Application Support
14
+ path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'mcp.json'),
15
+ // Windows AppData
16
+ path.join(os.homedir(), 'AppData', 'Roaming', 'Claude', 'mcp.json'),
17
+ // Linux config
18
+ path.join(os.homedir(), '.config', 'claude', 'mcp.json')
19
+ ];
20
+
21
+ /**
22
+ * MCP server configuration for Vibe Assurance
23
+ */
24
+ const VIBE_MCP_CONFIG = {
25
+ vibeassurance: {
26
+ command: 'vibe',
27
+ args: ['mcp-server']
28
+ }
29
+ };
30
+
31
+ /**
32
+ * Setup Claude command
33
+ *
34
+ * Configures Claude Code to use the Vibe Assurance MCP server
35
+ * by adding the vibeassurance entry to mcp.json
36
+ */
37
+ async function setupClaude() {
38
+ console.log(chalk.blue('Configuring Claude Code to use Vibe Assurance MCP server...\n'));
39
+
40
+ // Find existing config file or determine default path
41
+ let configPath = null;
42
+ let existingConfig = {};
43
+
44
+ for (const p of MCP_CONFIG_PATHS) {
45
+ if (fs.existsSync(p)) {
46
+ configPath = p;
47
+ try {
48
+ const content = fs.readFileSync(p, 'utf8');
49
+ existingConfig = JSON.parse(content);
50
+ console.log(chalk.gray(`Found existing config at: ${p}`));
51
+ } catch (err) {
52
+ console.log(chalk.yellow(`Found config file but couldn't parse it, will overwrite: ${p}`));
53
+ existingConfig = {};
54
+ }
55
+ break;
56
+ }
57
+ }
58
+
59
+ // If no existing config found, use the default path
60
+ if (!configPath) {
61
+ configPath = MCP_CONFIG_PATHS[0];
62
+ console.log(chalk.gray(`Creating new config at: ${configPath}`));
63
+ }
64
+
65
+ // Check if vibeassurance is already configured
66
+ if (existingConfig.mcpServers?.vibeassurance) {
67
+ console.log(chalk.yellow('Vibe Assurance is already configured in Claude Code.'));
68
+ console.log('Updating configuration...');
69
+ }
70
+
71
+ // Merge configuration
72
+ const newConfig = {
73
+ ...existingConfig,
74
+ mcpServers: {
75
+ ...(existingConfig.mcpServers || {}),
76
+ ...VIBE_MCP_CONFIG
77
+ }
78
+ };
79
+
80
+ // Ensure directory exists
81
+ const configDir = path.dirname(configPath);
82
+ if (!fs.existsSync(configDir)) {
83
+ fs.mkdirSync(configDir, { recursive: true });
84
+ console.log(chalk.gray(`Created directory: ${configDir}`));
85
+ }
86
+
87
+ // Write configuration
88
+ fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 2));
89
+
90
+ // Success output
91
+ console.log();
92
+ console.log(chalk.green('Success!') + ' Claude Code has been configured.\n');
93
+
94
+ console.log('Configuration written to:');
95
+ console.log(chalk.gray(` ${configPath}\n`));
96
+
97
+ console.log('The following MCP server was added:');
98
+ console.log(chalk.gray(' vibeassurance: vibe mcp-server\n'));
99
+
100
+ console.log(chalk.blue('Next steps:'));
101
+ console.log(' 1. Restart Claude Code');
102
+ console.log(' 2. The vibe_* tools will be available automatically');
103
+ console.log(' 3. Try asking: "Get my governance context"\n');
104
+
105
+ console.log(chalk.gray('Available tools after restart:'));
106
+ console.log(chalk.gray(' - vibe_get_role : Get AI analyst role prompts'));
107
+ console.log(chalk.gray(' - vibe_get_context : Get CRs, risks, vulnerabilities'));
108
+ console.log(chalk.gray(' - vibe_store_artifact: Store created documents'));
109
+ console.log(chalk.gray(' - ...and more\n'));
110
+ }
111
+
112
+ module.exports = setupClaude;
@@ -0,0 +1,121 @@
1
+ const keytar = require('keytar');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const os = require('os');
5
+
6
+ const SERVICE_NAME = 'vibe-assurance';
7
+ const ACCOUNT_NAME = 'auth-token';
8
+ const FALLBACK_FILE = path.join(os.homedir(), '.vibe', 'credentials.json');
9
+
10
+ /**
11
+ * Store authentication token
12
+ * Uses OS keychain (preferred) with file fallback
13
+ * @param {Object} credentials - { accessToken, refreshToken, expiresAt }
14
+ * @returns {Promise<{method: string}>} Storage method used
15
+ */
16
+ async function storeCredentials(credentials) {
17
+ try {
18
+ await keytar.setPassword(SERVICE_NAME, ACCOUNT_NAME, JSON.stringify(credentials));
19
+ return { method: 'keychain' };
20
+ } catch (err) {
21
+ // Keychain unavailable (common on Linux without libsecret, or in containers)
22
+ console.warn('Keychain unavailable, using file storage');
23
+ const dir = path.dirname(FALLBACK_FILE);
24
+ if (!fs.existsSync(dir)) {
25
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
26
+ }
27
+ fs.writeFileSync(FALLBACK_FILE, JSON.stringify(credentials), { mode: 0o600 });
28
+ return { method: 'file' };
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Retrieve stored credentials
34
+ * @returns {Promise<Object|null>} Credentials object or null if not found
35
+ */
36
+ async function getCredentials() {
37
+ // Try keychain first
38
+ try {
39
+ const stored = await keytar.getPassword(SERVICE_NAME, ACCOUNT_NAME);
40
+ if (stored) {
41
+ return JSON.parse(stored);
42
+ }
43
+ } catch (err) {
44
+ // Keychain unavailable, try file fallback
45
+ }
46
+
47
+ // Try file fallback
48
+ if (fs.existsSync(FALLBACK_FILE)) {
49
+ try {
50
+ const content = fs.readFileSync(FALLBACK_FILE, 'utf8');
51
+ return JSON.parse(content);
52
+ } catch (err) {
53
+ // File corrupted, return null
54
+ return null;
55
+ }
56
+ }
57
+
58
+ return null;
59
+ }
60
+
61
+ /**
62
+ * Delete stored credentials from all storage locations
63
+ * @returns {Promise<void>}
64
+ */
65
+ async function deleteCredentials() {
66
+ // Delete from keychain
67
+ try {
68
+ await keytar.deletePassword(SERVICE_NAME, ACCOUNT_NAME);
69
+ } catch (err) {
70
+ // Ignore keychain errors
71
+ }
72
+
73
+ // Delete file fallback
74
+ if (fs.existsSync(FALLBACK_FILE)) {
75
+ fs.unlinkSync(FALLBACK_FILE);
76
+ }
77
+
78
+ // Clean up empty .vibe directory
79
+ const vibeDir = path.dirname(FALLBACK_FILE);
80
+ if (fs.existsSync(vibeDir)) {
81
+ try {
82
+ const files = fs.readdirSync(vibeDir);
83
+ if (files.length === 0) {
84
+ fs.rmdirSync(vibeDir);
85
+ }
86
+ } catch (err) {
87
+ // Ignore cleanup errors
88
+ }
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Check if credentials exist and are valid (not expired)
94
+ * @returns {Promise<boolean>}
95
+ */
96
+ async function hasValidCredentials() {
97
+ const creds = await getCredentials();
98
+ if (!creds || !creds.accessToken) {
99
+ return false;
100
+ }
101
+
102
+ // Check if token is expired (with 5 minute buffer)
103
+ if (creds.expiresAt) {
104
+ const expiresAt = new Date(creds.expiresAt);
105
+ const now = new Date();
106
+ const bufferMs = 5 * 60 * 1000; // 5 minutes
107
+
108
+ if (expiresAt.getTime() - bufferMs < now.getTime()) {
109
+ return false;
110
+ }
111
+ }
112
+
113
+ return true;
114
+ }
115
+
116
+ module.exports = {
117
+ storeCredentials,
118
+ getCredentials,
119
+ deleteCredentials,
120
+ hasValidCredentials
121
+ };
@@ -0,0 +1,111 @@
1
+ const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
2
+ const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
3
+ const { ListToolsRequestSchema, CallToolRequestSchema } = require('@modelcontextprotocol/sdk/types.js');
4
+ const tools = require('./tools');
5
+
6
+ /**
7
+ * Start the MCP server
8
+ *
9
+ * The server communicates with Claude Code via stdio transport,
10
+ * exposing the vibe_* tools for governance operations.
11
+ */
12
+ async function startServer() {
13
+ // Create MCP server instance using the low-level API
14
+ const server = new Server(
15
+ {
16
+ name: 'vibe-assurance',
17
+ version: '1.0.0'
18
+ },
19
+ {
20
+ capabilities: {
21
+ tools: {}
22
+ }
23
+ }
24
+ );
25
+
26
+ // Build tool definitions with enhanced descriptions
27
+ const toolDefinitions = tools.map(tool => {
28
+ let description = tool.description;
29
+
30
+ // Add parameter info to description for tools with parameters
31
+ if (tool.inputSchema?.properties && Object.keys(tool.inputSchema.properties).length > 0) {
32
+ const params = Object.entries(tool.inputSchema.properties)
33
+ .map(([name, schema]) => ` - ${name}: ${schema.description || schema.type}`)
34
+ .join('\n');
35
+ description += `\n\nParameters:\n${params}`;
36
+ if (tool.inputSchema.required?.length > 0) {
37
+ description += `\n\nRequired: ${tool.inputSchema.required.join(', ')}`;
38
+ }
39
+ }
40
+
41
+ return {
42
+ name: tool.name,
43
+ description,
44
+ inputSchema: tool.inputSchema
45
+ };
46
+ });
47
+
48
+ // Register handler for listing available tools
49
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
50
+ return { tools: toolDefinitions };
51
+ });
52
+
53
+ // Register handler for tool execution
54
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
55
+ const { name, arguments: args } = request.params;
56
+
57
+ // Find the tool by name
58
+ const tool = tools.find(t => t.name === name);
59
+ if (!tool) {
60
+ return {
61
+ content: [{
62
+ type: 'text',
63
+ text: JSON.stringify({
64
+ error: `Unknown tool: ${name}`,
65
+ availableTools: tools.map(t => t.name)
66
+ }, null, 2)
67
+ }],
68
+ isError: true
69
+ };
70
+ }
71
+
72
+ try {
73
+ // Execute the tool handler with the arguments
74
+ const result = await tool.handler(args || {});
75
+
76
+ return {
77
+ content: [{
78
+ type: 'text',
79
+ text: JSON.stringify(result, null, 2)
80
+ }]
81
+ };
82
+ } catch (error) {
83
+ // Extract meaningful error message
84
+ const message = error.response?.data?.error ||
85
+ error.response?.data?.message ||
86
+ error.message ||
87
+ 'Unknown error occurred';
88
+
89
+ return {
90
+ content: [{
91
+ type: 'text',
92
+ text: JSON.stringify({
93
+ error: message,
94
+ tool: name
95
+ }, null, 2)
96
+ }],
97
+ isError: true
98
+ };
99
+ }
100
+ });
101
+
102
+ // Connect via stdio transport (used by Claude Code)
103
+ const transport = new StdioServerTransport();
104
+ await server.connect(transport);
105
+
106
+ // Log to stderr (stdout is used for MCP communication)
107
+ console.error('[vibe-assurance] MCP server started');
108
+ console.error('[vibe-assurance] Available tools:', tools.map(t => t.name).join(', '));
109
+ }
110
+
111
+ module.exports = startServer;
@@ -0,0 +1,271 @@
1
+ const api = require('../api/client');
2
+
3
+ /**
4
+ * MCP Tool definitions for Vibe Assurance
5
+ *
6
+ * These tools are exposed to Claude Code via the MCP protocol,
7
+ * allowing Claude to interact with the Vibe Assurance governance platform.
8
+ */
9
+ const tools = [
10
+ // ============================================================================
11
+ // PULL TOOLS - Get data from Vibe Assurance
12
+ // ============================================================================
13
+
14
+ {
15
+ name: 'vibe_get_role',
16
+ description: 'Get an AI analyst role prompt from Vibe Assurance. Use this to get detailed instructions for roles like implementation-planner, security-auditor, risk-auditor, etc. The role prompt contains the full system prompt and instructions for acting as that role.',
17
+ inputSchema: {
18
+ type: 'object',
19
+ properties: {
20
+ role: {
21
+ type: 'string',
22
+ description: 'Role ID (e.g., "implementation-planner", "security-auditor", "risk-auditor", "commit-officer", "test-engineer")'
23
+ }
24
+ },
25
+ required: ['role']
26
+ },
27
+ handler: async ({ role }) => {
28
+ const response = await api.get(`/api/mcp/roles/${role}`);
29
+ return {
30
+ role: response.roleId,
31
+ name: response.name,
32
+ description: response.description,
33
+ prompt: response.systemPrompt,
34
+ referencedTemplates: response.referencedTemplates || [],
35
+ expectedOutputs: response.expectedOutputs || []
36
+ };
37
+ }
38
+ },
39
+
40
+ {
41
+ name: 'vibe_list_roles',
42
+ description: 'List all available AI analyst roles in Vibe Assurance. Returns role IDs, names, and descriptions for all governance roles you can use.',
43
+ inputSchema: {
44
+ type: 'object',
45
+ properties: {},
46
+ additionalProperties: false
47
+ },
48
+ handler: async () => {
49
+ return await api.get('/api/mcp/roles');
50
+ }
51
+ },
52
+
53
+ {
54
+ name: 'vibe_get_context',
55
+ description: 'Get your current governance context including existing CRs, open risks, vulnerabilities, and the next available CR ID. Use this before creating new CRs to ensure proper ID sequencing.',
56
+ inputSchema: {
57
+ type: 'object',
58
+ properties: {},
59
+ additionalProperties: false
60
+ },
61
+ handler: async () => {
62
+ return await api.get('/api/mcp/context');
63
+ }
64
+ },
65
+
66
+ {
67
+ name: 'vibe_get_template',
68
+ description: 'Get a document template from Vibe Assurance. Templates include placeholders and structure for governance documents like change requests, risk assessments, and security reports.',
69
+ inputSchema: {
70
+ type: 'object',
71
+ properties: {
72
+ template: {
73
+ type: 'string',
74
+ description: 'Template ID (e.g., "change-request", "risk-assessment", "security-report", "implementation-plan")'
75
+ }
76
+ },
77
+ required: ['template']
78
+ },
79
+ handler: async ({ template }) => {
80
+ return await api.get(`/api/mcp/templates/${template}`);
81
+ }
82
+ },
83
+
84
+ {
85
+ name: 'vibe_list_templates',
86
+ description: 'List all available document templates. Returns template IDs, names, descriptions, and categories.',
87
+ inputSchema: {
88
+ type: 'object',
89
+ properties: {},
90
+ additionalProperties: false
91
+ },
92
+ handler: async () => {
93
+ return await api.get('/api/mcp/templates');
94
+ }
95
+ },
96
+
97
+ // ============================================================================
98
+ // PUSH TOOLS - Store data to Vibe Assurance
99
+ // ============================================================================
100
+
101
+ {
102
+ name: 'vibe_store_artifact',
103
+ description: 'Store a created document (CR, report, risk, vulnerability, etc.) in Vibe Assurance. The artifact will be saved to your account and visible in the web portal.',
104
+ inputSchema: {
105
+ type: 'object',
106
+ properties: {
107
+ type: {
108
+ type: 'string',
109
+ enum: ['CR', 'RISK', 'VULNERABILITY', 'REPORT', 'POLICY'],
110
+ description: 'Type of artifact'
111
+ },
112
+ artifactId: {
113
+ type: 'string',
114
+ description: 'Unique ID for this artifact (e.g., "CR-2025-024", "VUL-059", "RISK-001")'
115
+ },
116
+ title: {
117
+ type: 'string',
118
+ description: 'Title of the artifact'
119
+ },
120
+ status: {
121
+ type: 'string',
122
+ enum: ['Draft', 'Active', 'Completed', 'Closed'],
123
+ description: 'Status of the artifact (default: Draft)'
124
+ },
125
+ content: {
126
+ type: 'string',
127
+ description: 'Main content in markdown format'
128
+ },
129
+ files: {
130
+ type: 'array',
131
+ description: 'Additional files for this artifact (e.g., implementation-plan.md, rollback-plan.md for CRs)',
132
+ items: {
133
+ type: 'object',
134
+ properties: {
135
+ name: { type: 'string', description: 'Filename' },
136
+ content: { type: 'string', description: 'File content' }
137
+ },
138
+ required: ['name', 'content']
139
+ }
140
+ },
141
+ metadata: {
142
+ type: 'object',
143
+ description: 'Additional metadata (e.g., severity, priority, category)',
144
+ additionalProperties: true
145
+ }
146
+ },
147
+ required: ['type', 'artifactId', 'title', 'content']
148
+ },
149
+ handler: async (params) => {
150
+ return await api.post('/api/mcp/artifacts', params);
151
+ }
152
+ },
153
+
154
+ {
155
+ name: 'vibe_update_artifact',
156
+ description: 'Update an existing artifact. You can update the status, content, title, or metadata.',
157
+ inputSchema: {
158
+ type: 'object',
159
+ properties: {
160
+ artifactId: {
161
+ type: 'string',
162
+ description: 'The artifact ID to update (e.g., "CR-2025-024")'
163
+ },
164
+ status: {
165
+ type: 'string',
166
+ enum: ['Draft', 'Active', 'Completed', 'Closed'],
167
+ description: 'New status'
168
+ },
169
+ content: {
170
+ type: 'string',
171
+ description: 'Updated content'
172
+ },
173
+ title: {
174
+ type: 'string',
175
+ description: 'Updated title'
176
+ },
177
+ files: {
178
+ type: 'array',
179
+ description: 'Updated files array',
180
+ items: {
181
+ type: 'object',
182
+ properties: {
183
+ name: { type: 'string' },
184
+ content: { type: 'string' }
185
+ }
186
+ }
187
+ },
188
+ metadata: {
189
+ type: 'object',
190
+ description: 'Updated metadata'
191
+ }
192
+ },
193
+ required: ['artifactId']
194
+ },
195
+ handler: async ({ artifactId, ...updates }) => {
196
+ return await api.put(`/api/mcp/artifacts/${artifactId}`, updates);
197
+ }
198
+ },
199
+
200
+ {
201
+ name: 'vibe_list_artifacts',
202
+ description: 'List your stored artifacts with optional filters. Use this to see what governance documents you have stored.',
203
+ inputSchema: {
204
+ type: 'object',
205
+ properties: {
206
+ type: {
207
+ type: 'string',
208
+ enum: ['CR', 'RISK', 'VULNERABILITY', 'REPORT', 'POLICY'],
209
+ description: 'Filter by artifact type'
210
+ },
211
+ status: {
212
+ type: 'string',
213
+ enum: ['Draft', 'Active', 'Completed', 'Closed'],
214
+ description: 'Filter by status'
215
+ },
216
+ limit: {
217
+ type: 'number',
218
+ description: 'Maximum number of results (default: 50, max: 100)'
219
+ }
220
+ }
221
+ },
222
+ handler: async (params = {}) => {
223
+ const query = new URLSearchParams();
224
+ if (params.type) query.set('type', params.type);
225
+ if (params.status) query.set('status', params.status);
226
+ if (params.limit) query.set('limit', String(Math.min(params.limit, 100)));
227
+
228
+ const queryString = query.toString();
229
+ const path = queryString ? `/api/mcp/artifacts?${queryString}` : '/api/mcp/artifacts';
230
+ return await api.get(path);
231
+ }
232
+ },
233
+
234
+ {
235
+ name: 'vibe_get_artifact',
236
+ description: 'Get a specific artifact by ID. Returns the full content including any additional files.',
237
+ inputSchema: {
238
+ type: 'object',
239
+ properties: {
240
+ artifactId: {
241
+ type: 'string',
242
+ description: 'The artifact ID to retrieve (e.g., "CR-2025-024")'
243
+ }
244
+ },
245
+ required: ['artifactId']
246
+ },
247
+ handler: async ({ artifactId }) => {
248
+ return await api.get(`/api/mcp/artifacts/${artifactId}`);
249
+ }
250
+ },
251
+
252
+ {
253
+ name: 'vibe_delete_artifact',
254
+ description: 'Delete an artifact by ID. This is permanent and cannot be undone.',
255
+ inputSchema: {
256
+ type: 'object',
257
+ properties: {
258
+ artifactId: {
259
+ type: 'string',
260
+ description: 'The artifact ID to delete (e.g., "CR-2025-024")'
261
+ }
262
+ },
263
+ required: ['artifactId']
264
+ },
265
+ handler: async ({ artifactId }) => {
266
+ return await api.delete(`/api/mcp/artifacts/${artifactId}`);
267
+ }
268
+ }
269
+ ];
270
+
271
+ module.exports = tools;