slashvibe-mcp 0.2.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,69 @@
1
+ # vibe-mcp
2
+
3
+ Social layer for Claude Code. DMs, presence, and connection between AI-assisted developers.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ # Install globally
9
+ npm install -g vibe-mcp
10
+
11
+ # Or add to Claude Code MCP config
12
+ claude mcp add vibe-mcp
13
+ ```
14
+
15
+ ## Manual Setup
16
+
17
+ Add to `~/.claude.json`:
18
+
19
+ ```json
20
+ {
21
+ "mcpServers": {
22
+ "vibe": {
23
+ "command": "npx",
24
+ "args": ["vibe-mcp"],
25
+ "env": {
26
+ "VIBE_API_URL": "https://www.slashvibe.dev"
27
+ }
28
+ }
29
+ }
30
+ }
31
+ ```
32
+
33
+ ## Features
34
+
35
+ - **Presence** - See who's online building with Claude Code
36
+ - **DMs** - Direct messages between developers
37
+ - **Memory** - Remember context about connections
38
+ - **Status** - Share what you're working on
39
+ - **Games** - Play tic-tac-toe while coding
40
+
41
+ ## Commands
42
+
43
+ Once installed, use these in Claude Code:
44
+
45
+ | Command | Description |
46
+ |---------|-------------|
47
+ | `vibe` | Check inbox and see who's online |
48
+ | `vibe who` | List online users |
49
+ | `vibe dm @handle "message"` | Send a DM |
50
+ | `vibe status shipping` | Set your status |
51
+ | `vibe remember @handle "note"` | Save a memory |
52
+ | `vibe recall @handle` | Recall memories |
53
+
54
+ ## API
55
+
56
+ The MCP server connects to `slashvibe.dev` for:
57
+ - User presence and discovery
58
+ - Message routing
59
+ - Identity verification
60
+
61
+ ## Related
62
+
63
+ - [slashvibe.dev](https://slashvibe.dev) - Web presence
64
+ - [Spirit Protocol](https://spiritprotocol.io) - Parent ecosystem
65
+ - [AIRC](https://airc.chat) - Agent identity protocol
66
+
67
+ ## License
68
+
69
+ MIT
package/config.js ADDED
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Config — User identity and paths
3
+ *
4
+ * UNIFIED: Uses ~/.vibecodings/config.json as primary source
5
+ * Falls back to ~/.vibe/config.json for backward compat
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+
11
+ const VIBE_DIR = path.join(process.env.HOME, '.vibe');
12
+ const VIBECODINGS_DIR = path.join(process.env.HOME, '.vibecodings');
13
+ const PRIMARY_CONFIG = path.join(VIBECODINGS_DIR, 'config.json'); // Primary
14
+ const FALLBACK_CONFIG = path.join(VIBE_DIR, 'config.json'); // Fallback
15
+ const CONFIG_FILE = PRIMARY_CONFIG;
16
+
17
+ function ensureDir() {
18
+ if (!fs.existsSync(VIBECODINGS_DIR)) {
19
+ fs.mkdirSync(VIBECODINGS_DIR, { recursive: true });
20
+ }
21
+ }
22
+
23
+ function load() {
24
+ ensureDir();
25
+ // Try primary config first
26
+ try {
27
+ if (fs.existsSync(PRIMARY_CONFIG)) {
28
+ const data = JSON.parse(fs.readFileSync(PRIMARY_CONFIG, 'utf8'));
29
+ // Normalize: support both 'handle' and 'username' field names
30
+ return {
31
+ ...data, // Pass through all fields (including x_credentials, etc.)
32
+ handle: data.handle || data.username || null,
33
+ one_liner: data.one_liner || data.workingOn || null,
34
+ visible: data.visible !== false,
35
+ // AIRC keypair (persisted across sessions)
36
+ publicKey: data.publicKey || null,
37
+ privateKey: data.privateKey || null
38
+ };
39
+ }
40
+ } catch (e) {}
41
+ // Fallback to legacy config (returns full object)
42
+ try {
43
+ if (fs.existsSync(FALLBACK_CONFIG)) {
44
+ return JSON.parse(fs.readFileSync(FALLBACK_CONFIG, 'utf8'));
45
+ }
46
+ } catch (e) {}
47
+ return { handle: null, one_liner: null, visible: true, publicKey: null, privateKey: null };
48
+ }
49
+
50
+ function save(config) {
51
+ ensureDir();
52
+ // Load existing to preserve fields we're not updating
53
+ let existing = {};
54
+ try {
55
+ if (fs.existsSync(PRIMARY_CONFIG)) {
56
+ existing = JSON.parse(fs.readFileSync(PRIMARY_CONFIG, 'utf8'));
57
+ }
58
+ } catch (e) {}
59
+
60
+ // Save to primary config in vibecodings format
61
+ const data = {
62
+ username: config.handle || config.username || existing.username,
63
+ workingOn: config.one_liner || config.workingOn || existing.workingOn,
64
+ createdAt: config.createdAt || existing.createdAt || new Date().toISOString().split('T')[0],
65
+ // AIRC keypair (persisted across sessions)
66
+ publicKey: config.publicKey || existing.publicKey || null,
67
+ privateKey: config.privateKey || existing.privateKey || null,
68
+ // Guided mode (AskUserQuestion menus)
69
+ guided_mode: config.guided_mode !== undefined ? config.guided_mode : existing.guided_mode
70
+ };
71
+ fs.writeFileSync(PRIMARY_CONFIG, JSON.stringify(data, null, 2));
72
+ }
73
+
74
+ function getHandle() {
75
+ // Prefer session-specific handle over shared config
76
+ const sessionHandle = getSessionHandle();
77
+ if (sessionHandle) return sessionHandle;
78
+ // Fall back to shared config
79
+ const config = load();
80
+ return config.handle || null;
81
+ }
82
+
83
+ function getOneLiner() {
84
+ // Prefer session-specific one_liner over shared config
85
+ const sessionOneLiner = getSessionOneLiner();
86
+ if (sessionOneLiner) return sessionOneLiner;
87
+ // Fall back to shared config
88
+ const config = load();
89
+ return config.one_liner || null;
90
+ }
91
+
92
+ function isInitialized() {
93
+ // Check session first, then shared config
94
+ const sessionHandle = getSessionHandle();
95
+ if (sessionHandle) return true;
96
+ const config = load();
97
+ return config.handle && config.handle.length > 0;
98
+ }
99
+
100
+ // Session management - unique ID per Claude Code instance
101
+ // Now stores full identity (handle + one_liner), not just sessionId
102
+ const SESSION_FILE = path.join(VIBECODINGS_DIR, `.session_${process.pid}`);
103
+
104
+ function generateSessionId() {
105
+ return 'sess_' + Date.now().toString(36) + Math.random().toString(36).substring(2, 10);
106
+ }
107
+
108
+ function getSessionData() {
109
+ try {
110
+ if (fs.existsSync(SESSION_FILE)) {
111
+ const content = fs.readFileSync(SESSION_FILE, 'utf8').trim();
112
+ // Support old format (just sessionId string) and new format (JSON)
113
+ if (content.startsWith('{')) {
114
+ return JSON.parse(content);
115
+ }
116
+ // Old format: just the sessionId
117
+ return { sessionId: content, handle: null, one_liner: null };
118
+ }
119
+ } catch (e) {}
120
+ return null;
121
+ }
122
+
123
+ function saveSessionData(data) {
124
+ ensureDir();
125
+ fs.writeFileSync(SESSION_FILE, JSON.stringify(data, null, 2));
126
+ }
127
+
128
+ function getSessionId() {
129
+ const data = getSessionData();
130
+ if (data?.sessionId) {
131
+ return data.sessionId;
132
+ }
133
+ // Generate new session
134
+ const sessionId = generateSessionId();
135
+ saveSessionData({ sessionId, handle: null, one_liner: null });
136
+ return sessionId;
137
+ }
138
+
139
+ function getSessionHandle() {
140
+ const data = getSessionData();
141
+ return data?.handle || null;
142
+ }
143
+
144
+ function getSessionOneLiner() {
145
+ const data = getSessionData();
146
+ return data?.one_liner || null;
147
+ }
148
+
149
+ function setSessionIdentity(handle, one_liner, keypair = null) {
150
+ const sessionId = getSessionId();
151
+ const existingData = getSessionData() || {};
152
+ saveSessionData({
153
+ sessionId,
154
+ handle,
155
+ one_liner,
156
+ // Preserve token if already set (from server registration)
157
+ token: existingData.token || null,
158
+ // AIRC keypair (generated on init)
159
+ publicKey: keypair?.publicKey || existingData.publicKey || null,
160
+ privateKey: keypair?.privateKey || existingData.privateKey || null
161
+ });
162
+ }
163
+
164
+ function getKeypair() {
165
+ // First check session data
166
+ const sessionData = getSessionData();
167
+ if (sessionData?.publicKey && sessionData?.privateKey) {
168
+ return {
169
+ publicKey: sessionData.publicKey,
170
+ privateKey: sessionData.privateKey
171
+ };
172
+ }
173
+ // Fall back to shared config (keypairs persist across MCP invocations)
174
+ const config = load();
175
+ if (config?.publicKey && config?.privateKey) {
176
+ return {
177
+ publicKey: config.publicKey,
178
+ privateKey: config.privateKey
179
+ };
180
+ }
181
+ return null;
182
+ }
183
+
184
+ function hasKeypair() {
185
+ return getKeypair() !== null;
186
+ }
187
+
188
+ function saveKeypair(keypair) {
189
+ // Save to shared config so it persists across MCP process invocations
190
+ const config = load();
191
+ config.publicKey = keypair.publicKey;
192
+ config.privateKey = keypair.privateKey;
193
+ save(config);
194
+ }
195
+
196
+ function setAuthToken(token, sessionId = null) {
197
+ const data = getSessionData() || {};
198
+ saveSessionData({
199
+ ...data,
200
+ sessionId: sessionId || data.sessionId || generateSessionId(),
201
+ token
202
+ });
203
+ }
204
+
205
+ function getAuthToken() {
206
+ const data = getSessionData();
207
+ return data?.token || null;
208
+ }
209
+
210
+ function clearSession() {
211
+ try {
212
+ if (fs.existsSync(SESSION_FILE)) {
213
+ fs.unlinkSync(SESSION_FILE);
214
+ }
215
+ } catch (e) {}
216
+ }
217
+
218
+ // Guided mode — show AskUserQuestion menus (default: true for new users)
219
+ function getGuidedMode() {
220
+ const config = load();
221
+ // Default to true (guided mode on) if not set
222
+ return config.guided_mode !== false;
223
+ }
224
+
225
+ function setGuidedMode(enabled) {
226
+ const config = load();
227
+ config.guided_mode = enabled;
228
+ save(config);
229
+ }
230
+
231
+ module.exports = {
232
+ VIBE_DIR,
233
+ CONFIG_FILE,
234
+ load,
235
+ save,
236
+ getHandle,
237
+ getOneLiner,
238
+ isInitialized,
239
+ getSessionId,
240
+ getSessionHandle,
241
+ getSessionOneLiner,
242
+ setSessionIdentity,
243
+ setAuthToken,
244
+ getAuthToken,
245
+ getKeypair,
246
+ hasKeypair,
247
+ saveKeypair,
248
+ clearSession,
249
+ generateSessionId,
250
+ getGuidedMode,
251
+ setGuidedMode
252
+ };
package/crypto.js ADDED
@@ -0,0 +1,200 @@
1
+ /**
2
+ * AIRC Crypto — Ed25519 keypair generation and message signing
3
+ *
4
+ * Implements AIRC v0.1 signing specification:
5
+ * - Ed25519 keypairs (Node.js crypto)
6
+ * - Canonical JSON serialization
7
+ * - Base64 signature encoding
8
+ */
9
+
10
+ const crypto = require('crypto');
11
+
12
+ /**
13
+ * Generate a new Ed25519 keypair
14
+ * @returns {{ publicKey: string, privateKey: string }} Base64-encoded keys
15
+ */
16
+ function generateKeypair() {
17
+ const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');
18
+
19
+ return {
20
+ publicKey: publicKey.export({ type: 'spki', format: 'der' }).toString('base64'),
21
+ privateKey: privateKey.export({ type: 'pkcs8', format: 'der' }).toString('base64')
22
+ };
23
+ }
24
+
25
+ /**
26
+ * Serialize object to canonical JSON per AIRC spec
27
+ * - Keys sorted alphabetically (recursive)
28
+ * - No whitespace
29
+ * - UTF-8 encoding
30
+ *
31
+ * @param {object} obj Object to serialize
32
+ * @returns {string} Canonical JSON string
33
+ */
34
+ function canonicalJSON(obj) {
35
+ if (obj === null || obj === undefined) return 'null';
36
+ if (typeof obj !== 'object') return JSON.stringify(obj);
37
+ if (Array.isArray(obj)) {
38
+ return '[' + obj.map(canonicalJSON).join(',') + ']';
39
+ }
40
+
41
+ // Sort keys alphabetically and recurse
42
+ const sortedKeys = Object.keys(obj).sort();
43
+ const pairs = sortedKeys
44
+ .filter(k => obj[k] !== undefined) // Exclude undefined values
45
+ .map(k => `${JSON.stringify(k)}:${canonicalJSON(obj[k])}`);
46
+ return '{' + pairs.join(',') + '}';
47
+ }
48
+
49
+ /**
50
+ * Sign an object with Ed25519 private key
51
+ *
52
+ * Per AIRC spec:
53
+ * 1. Clone object
54
+ * 2. Remove 'signature' field if present
55
+ * 3. Serialize to canonical JSON
56
+ * 4. Sign UTF-8 bytes
57
+ *
58
+ * @param {object} obj Object to sign
59
+ * @param {string} privateKeyBase64 Base64-encoded private key (PKCS8 DER)
60
+ * @returns {string} Base64-encoded signature
61
+ */
62
+ function sign(obj, privateKeyBase64) {
63
+ // Clone and remove signature field
64
+ const toSign = { ...obj };
65
+ delete toSign.signature;
66
+
67
+ // Get canonical JSON
68
+ const canonical = canonicalJSON(toSign);
69
+
70
+ // Import private key
71
+ const privateKey = crypto.createPrivateKey({
72
+ key: Buffer.from(privateKeyBase64, 'base64'),
73
+ format: 'der',
74
+ type: 'pkcs8'
75
+ });
76
+
77
+ // Sign
78
+ const signature = crypto.sign(null, Buffer.from(canonical, 'utf8'), privateKey);
79
+ return signature.toString('base64');
80
+ }
81
+
82
+ /**
83
+ * Verify signature on an object
84
+ *
85
+ * @param {object} obj Object with signature field
86
+ * @param {string} publicKeyBase64 Base64-encoded public key (SPKI DER)
87
+ * @returns {boolean} True if signature is valid
88
+ */
89
+ function verify(obj, publicKeyBase64) {
90
+ if (!obj.signature) return false;
91
+
92
+ // Clone and remove signature
93
+ const toVerify = { ...obj };
94
+ const signature = toVerify.signature;
95
+ delete toVerify.signature;
96
+
97
+ // Get canonical JSON
98
+ const canonical = canonicalJSON(toVerify);
99
+
100
+ try {
101
+ // Import public key
102
+ const publicKey = crypto.createPublicKey({
103
+ key: Buffer.from(publicKeyBase64, 'base64'),
104
+ format: 'der',
105
+ type: 'spki'
106
+ });
107
+
108
+ // Verify
109
+ return crypto.verify(
110
+ null,
111
+ Buffer.from(canonical, 'utf8'),
112
+ publicKey,
113
+ Buffer.from(signature, 'base64')
114
+ );
115
+ } catch (e) {
116
+ console.error('[crypto] Verification failed:', e.message);
117
+ return false;
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Generate a random nonce (16+ chars)
123
+ * @returns {string} Hex-encoded nonce
124
+ */
125
+ function generateNonce() {
126
+ return crypto.randomBytes(16).toString('hex');
127
+ }
128
+
129
+ /**
130
+ * Generate AIRC-compliant message ID
131
+ * @returns {string} Message ID (msg_ prefix + random)
132
+ */
133
+ function generateMessageId() {
134
+ return 'msg_' + crypto.randomBytes(12).toString('hex');
135
+ }
136
+
137
+ /**
138
+ * Create a signed AIRC message
139
+ *
140
+ * @param {object} params Message parameters
141
+ * @param {string} params.from Sender handle
142
+ * @param {string} params.to Recipient handle
143
+ * @param {string} [params.body] Message body
144
+ * @param {object} [params.payload] Message payload
145
+ * @param {string} privateKeyBase64 Sender's private key
146
+ * @returns {object} Complete signed message
147
+ */
148
+ function createSignedMessage({ from, to, body, payload }, privateKeyBase64) {
149
+ const message = {
150
+ v: '0.1',
151
+ id: generateMessageId(),
152
+ from,
153
+ to,
154
+ timestamp: Math.floor(Date.now() / 1000),
155
+ nonce: generateNonce()
156
+ };
157
+
158
+ if (body) message.body = body;
159
+ if (payload) message.payload = payload;
160
+
161
+ // Sign
162
+ message.signature = sign(message, privateKeyBase64);
163
+
164
+ return message;
165
+ }
166
+
167
+ /**
168
+ * Create a signed heartbeat
169
+ *
170
+ * @param {string} handle User handle
171
+ * @param {string} status Status (online/idle/busy/offline)
172
+ * @param {string} [context] Activity context
173
+ * @param {string} privateKeyBase64 User's private key
174
+ * @returns {object} Signed heartbeat
175
+ */
176
+ function createSignedHeartbeat(handle, status, context, privateKeyBase64) {
177
+ const heartbeat = {
178
+ handle,
179
+ status,
180
+ timestamp: Math.floor(Date.now() / 1000),
181
+ nonce: generateNonce()
182
+ };
183
+
184
+ if (context) heartbeat.context = context;
185
+
186
+ heartbeat.signature = sign(heartbeat, privateKeyBase64);
187
+
188
+ return heartbeat;
189
+ }
190
+
191
+ module.exports = {
192
+ generateKeypair,
193
+ canonicalJSON,
194
+ sign,
195
+ verify,
196
+ generateNonce,
197
+ generateMessageId,
198
+ createSignedMessage,
199
+ createSignedHeartbeat
200
+ };
package/discord.js ADDED
@@ -0,0 +1,151 @@
1
+ /**
2
+ * /vibe Discord Webhook Integration
3
+ *
4
+ * Posts /vibe activity to a Discord channel via webhook.
5
+ * One-way: /vibe → Discord (outbound only)
6
+ */
7
+
8
+ const config = require('./config');
9
+
10
+ /**
11
+ * Get Discord webhook URL from config
12
+ */
13
+ function getWebhookUrl() {
14
+ const cfg = config.load();
15
+ return cfg.discord_webhook_url || null;
16
+ }
17
+
18
+ /**
19
+ * Check if Discord integration is configured
20
+ */
21
+ function isConfigured() {
22
+ return !!getWebhookUrl();
23
+ }
24
+
25
+ /**
26
+ * Post a message to Discord via webhook
27
+ */
28
+ async function post(content, options = {}) {
29
+ const webhookUrl = getWebhookUrl();
30
+ if (!webhookUrl) return false;
31
+
32
+ try {
33
+ const body = {
34
+ content,
35
+ username: options.username || '/vibe',
36
+ avatar_url: options.avatar || 'https://slashvibe.dev/vibe-icon.png'
37
+ };
38
+
39
+ // Support embeds for richer messages
40
+ if (options.embed) {
41
+ body.embeds = [options.embed];
42
+ delete body.content;
43
+ }
44
+
45
+ const response = await fetch(webhookUrl, {
46
+ method: 'POST',
47
+ headers: { 'Content-Type': 'application/json' },
48
+ body: JSON.stringify(body)
49
+ });
50
+
51
+ return response.ok;
52
+ } catch (e) {
53
+ // Silent fail - Discord is best-effort
54
+ return false;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Post when someone joins /vibe
60
+ */
61
+ async function postJoin(handle, oneLiner) {
62
+ const embed = {
63
+ color: 0x6B8FFF, // Spirit blue
64
+ title: `@${handle} joined /vibe`,
65
+ description: oneLiner || 'Building something',
66
+ footer: { text: 'slashvibe.dev' },
67
+ timestamp: new Date().toISOString()
68
+ };
69
+ return post(null, { embed });
70
+ }
71
+
72
+ /**
73
+ * Post when someone sends a message (anonymized)
74
+ */
75
+ async function postActivity(handle, action) {
76
+ const embed = {
77
+ color: 0x2ECC71, // Green
78
+ description: `**@${handle}** ${action}`,
79
+ timestamp: new Date().toISOString()
80
+ };
81
+ return post(null, { embed });
82
+ }
83
+
84
+ /**
85
+ * Post when someone changes status
86
+ */
87
+ async function postStatus(handle, mood, note) {
88
+ const moodEmoji = {
89
+ 'shipping': '🔥',
90
+ 'debugging': '🐛',
91
+ 'deep': '🧠',
92
+ 'afk': '☕',
93
+ 'celebrating': '🎉',
94
+ 'pairing': '👯'
95
+ };
96
+
97
+ const emoji = moodEmoji[mood] || '●';
98
+ const embed = {
99
+ color: 0x9B59B6, // Purple
100
+ description: `${emoji} **@${handle}** is ${mood}${note ? `: "${note}"` : ''}`,
101
+ timestamp: new Date().toISOString()
102
+ };
103
+ return post(null, { embed });
104
+ }
105
+
106
+ /**
107
+ * Post a system announcement
108
+ */
109
+ async function postAnnouncement(message) {
110
+ const embed = {
111
+ color: 0x6B8FFF,
112
+ title: '/vibe',
113
+ description: message,
114
+ timestamp: new Date().toISOString()
115
+ };
116
+ return post(null, { embed });
117
+ }
118
+
119
+ /**
120
+ * Post who's currently online
121
+ */
122
+ async function postOnlineList(users) {
123
+ if (users.length === 0) {
124
+ return post('_Room is quiet..._');
125
+ }
126
+
127
+ const list = users.map(u => {
128
+ const mood = u.mood ? ` ${u.mood}` : '';
129
+ return `• **@${u.handle}**${mood} — ${u.one_liner || 'building'}`;
130
+ }).join('\n');
131
+
132
+ const embed = {
133
+ color: 0x6B8FFF,
134
+ title: `${users.length} online in /vibe`,
135
+ description: list,
136
+ footer: { text: 'slashvibe.dev' },
137
+ timestamp: new Date().toISOString()
138
+ };
139
+ return post(null, { embed });
140
+ }
141
+
142
+ module.exports = {
143
+ isConfigured,
144
+ getWebhookUrl,
145
+ post,
146
+ postJoin,
147
+ postActivity,
148
+ postStatus,
149
+ postAnnouncement,
150
+ postOnlineList
151
+ };