slashvibe-mcp 0.3.22 → 0.3.23

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/analytics.js ADDED
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Analytics — Retention event tracking from MCP server
3
+ *
4
+ * Logs events to the /api/analytics/event endpoint for measuring
5
+ * user engagement and retention funnel performance.
6
+ *
7
+ * Usage:
8
+ * const analytics = require('./analytics');
9
+ * analytics.track('empty_inbox_action', { action: 'discover', source: 'inbox' });
10
+ */
11
+
12
+ const config = require('./config');
13
+
14
+ const API_URL = process.env.VIBE_API_URL || 'https://www.slashvibe.dev';
15
+
16
+ /**
17
+ * Track an analytics event (fire and forget)
18
+ * @param {string} eventType - Event type (from valid types in api/lib/events.js)
19
+ * @param {object} data - Additional event data
20
+ */
21
+ async function track(eventType, data = {}) {
22
+ const handle = config.getHandle();
23
+ if (!handle) return; // Skip if not initialized
24
+
25
+ try {
26
+ // Fire and forget - don't await or block
27
+ fetch(`${API_URL}/api/analytics/events`, {
28
+ method: 'POST',
29
+ headers: { 'Content-Type': 'application/json' },
30
+ body: JSON.stringify({
31
+ type: eventType,
32
+ handle,
33
+ data: {
34
+ ...data,
35
+ client: 'mcp-server',
36
+ timestamp: Date.now()
37
+ }
38
+ })
39
+ }).catch(() => {}); // Silently ignore errors
40
+ } catch (e) {
41
+ // Analytics should never block or fail user flows
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Track empty inbox interaction
47
+ * @param {string} action - Which action was taken (or 'none' if user closed)
48
+ * @param {object} context - Context about the state (hadOnboardingTask, hadRecentShips, etc.)
49
+ */
50
+ function trackEmptyInbox(action, context = {}) {
51
+ // Track that user reached empty inbox state
52
+ track('empty_inbox_reached', {
53
+ hadRecentThreads: context.recentThreads?.length > 0,
54
+ hadOnboardingTask: !!context.onboardingTask,
55
+ hadRecentShips: context.recentShips?.length > 0
56
+ });
57
+
58
+ // If an action was taken, track it
59
+ if (action && action !== 'none') {
60
+ track('empty_inbox_action', {
61
+ action,
62
+ ...context
63
+ });
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Track lurk mode state change
69
+ * @param {boolean} enabled - Whether lurk mode was enabled or disabled
70
+ */
71
+ function trackLurkMode(enabled) {
72
+ track(enabled ? 'lurk_mode_enabled' : 'lurk_mode_disabled', {});
73
+ }
74
+
75
+ /**
76
+ * Track onboarding task completion
77
+ * @param {string} taskId - The task that was completed
78
+ */
79
+ function trackOnboardingTask(taskId) {
80
+ track('onboarding_task_completed', { taskId });
81
+ }
82
+
83
+ /**
84
+ * Track discovery initiation
85
+ * @param {string} source - Where discovery was initiated from (inbox, start, etc.)
86
+ */
87
+ function trackDiscovery(source) {
88
+ track('discovery_initiated', { source });
89
+ }
90
+
91
+ /**
92
+ * Track session lifecycle
93
+ * @param {string} event - 'started' or 'ended'
94
+ * @param {object} sessionData - Session metrics (duration, actions, etc.)
95
+ */
96
+ function trackSession(event, sessionData = {}) {
97
+ track(event === 'started' ? 'session_started' : 'session_ended', sessionData);
98
+ }
99
+
100
+ module.exports = {
101
+ track,
102
+ trackEmptyInbox,
103
+ trackLurkMode,
104
+ trackOnboardingTask,
105
+ trackDiscovery,
106
+ trackSession
107
+ };
package/auto-update.js ADDED
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Auto-update mechanism for /vibe MCP server
3
+ * Checks for updates and prompts user to update
4
+ */
5
+
6
+ import { exec } from 'child_process';
7
+ import { promisify } from 'util';
8
+ import fs from 'fs/promises';
9
+ import path from 'path';
10
+ import { fileURLToPath } from 'url';
11
+
12
+ const execAsync = promisify(exec);
13
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
+
15
+ export async function checkForUpdates() {
16
+ try {
17
+ // Read local version
18
+ const versionPath = path.join(__dirname, 'version.json');
19
+ const localVersion = JSON.parse(await fs.readFile(versionPath, 'utf-8'));
20
+
21
+ // Check remote version
22
+ const response = await fetch('https://www.slashvibe.dev/api/version', {
23
+ headers: { 'User-Agent': 'vibe-mcp-client' }
24
+ });
25
+
26
+ if (!response.ok) {
27
+ return null; // Silent fail - don't block startup
28
+ }
29
+
30
+ const remoteVersion = await response.json();
31
+
32
+ // Compare versions
33
+ if (compareVersions(remoteVersion.version, localVersion.version) > 0) {
34
+ return {
35
+ current: localVersion.version,
36
+ latest: remoteVersion.version,
37
+ changelog: remoteVersion.changelog,
38
+ features: remoteVersion.features,
39
+ breaking: remoteVersion.breaking
40
+ };
41
+ }
42
+
43
+ return null; // Up to date
44
+ } catch (error) {
45
+ // Silent fail - don't block startup
46
+ return null;
47
+ }
48
+ }
49
+
50
+ export async function performUpdate() {
51
+ try {
52
+ const repoPath = path.join(__dirname, '..');
53
+
54
+ // Check if we're in a git repo
55
+ try {
56
+ await execAsync('git rev-parse --git-dir', { cwd: repoPath });
57
+ } catch {
58
+ throw new Error('Not a git repository. Manual update required.');
59
+ }
60
+
61
+ // Stash any local changes
62
+ await execAsync('git stash', { cwd: repoPath });
63
+
64
+ // Pull latest
65
+ const { stdout, stderr } = await execAsync('git pull origin main', { cwd: repoPath });
66
+
67
+ // Pop stash if we had changes
68
+ try {
69
+ await execAsync('git stash pop', { cwd: repoPath });
70
+ } catch {
71
+ // No stash to pop, that's fine
72
+ }
73
+
74
+ return {
75
+ success: true,
76
+ output: stdout,
77
+ message: 'Update successful! Restart /vibe to apply changes.'
78
+ };
79
+ } catch (error) {
80
+ return {
81
+ success: false,
82
+ error: error.message
83
+ };
84
+ }
85
+ }
86
+
87
+ export function compareVersions(v1, v2) {
88
+ const parts1 = v1.split('.').map(Number);
89
+ const parts2 = v2.split('.').map(Number);
90
+
91
+ for (let i = 0; i < 3; i++) {
92
+ if (parts1[i] > parts2[i]) return 1;
93
+ if (parts1[i] < parts2[i]) return -1;
94
+ }
95
+
96
+ return 0;
97
+ }
98
+
99
+ export function formatUpdateNotification(update) {
100
+ if (!update) return null;
101
+
102
+ let message = `\n${'='.repeat(60)}\n`;
103
+ message += `📦 /vibe UPDATE AVAILABLE\n`;
104
+ message += `${'='.repeat(60)}\n\n`;
105
+ message += `Current: v${update.current}\n`;
106
+ message += `Latest: v${update.latest}${update.breaking ? ' ⚠️ BREAKING' : ''}\n\n`;
107
+ message += `${update.changelog}\n\n`;
108
+
109
+ if (update.features && update.features.length > 0) {
110
+ message += `New features:\n`;
111
+ update.features.forEach(f => {
112
+ message += ` • ${f}\n`;
113
+ });
114
+ message += `\n`;
115
+ }
116
+
117
+ message += `Update now:\n`;
118
+ message += ` vibe update\n`;
119
+ message += `\n`;
120
+ message += `Or manually:\n`;
121
+ message += ` cd ~/.vibe/vibe-repo && git pull origin main\n`;
122
+ message += `${'='.repeat(60)}\n`;
123
+
124
+ return message;
125
+ }
package/debug.js ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Debug logging — conditional on VIBE_DEBUG env var
3
+ *
4
+ * @param {...*} args Arguments to log
5
+ */
6
+ function debug(...args) {
7
+ if (process.env.VIBE_DEBUG === 'true') {
8
+ console.error(...args);
9
+ }
10
+ }
11
+
12
+ module.exports = debug;
@@ -0,0 +1,54 @@
1
+ const js = require('@eslint/js');
2
+
3
+ module.exports = [
4
+ js.configs.recommended,
5
+ {
6
+ languageOptions: {
7
+ ecmaVersion: 2022,
8
+ sourceType: 'commonjs',
9
+ globals: {
10
+ // Node.js globals
11
+ require: 'readonly',
12
+ module: 'readonly',
13
+ exports: 'readonly',
14
+ __dirname: 'readonly',
15
+ __filename: 'readonly',
16
+ process: 'readonly',
17
+ console: 'readonly',
18
+ Buffer: 'readonly',
19
+ setTimeout: 'readonly',
20
+ setInterval: 'readonly',
21
+ clearTimeout: 'readonly',
22
+ clearInterval: 'readonly',
23
+ URL: 'readonly',
24
+ URLSearchParams: 'readonly',
25
+ fetch: 'readonly',
26
+ global: 'readonly',
27
+ AbortController: 'readonly',
28
+ AbortSignal: 'readonly'
29
+ }
30
+ },
31
+ rules: {
32
+ 'no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
33
+ eqeqeq: ['error', 'smart'],
34
+ 'no-var': 'error',
35
+ 'prefer-const': 'warn',
36
+ 'no-throw-literal': 'error',
37
+ 'no-empty': ['warn', { allowEmptyCatch: true }],
38
+ 'no-case-declarations': 'warn',
39
+ 'no-prototype-builtins': 'warn',
40
+ 'no-shadow-restricted-names': 'warn',
41
+ 'no-useless-escape': 'warn'
42
+ }
43
+ },
44
+ // ESM files (use import/export)
45
+ {
46
+ files: ['auto-update.js', 'post-install.js'],
47
+ languageOptions: {
48
+ sourceType: 'module'
49
+ }
50
+ },
51
+ {
52
+ ignores: ['node_modules/', 'coverage/', 'dist/']
53
+ }
54
+ ];
package/migrate-v2.js ADDED
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Migration: Add thread_id column for V2 Postgres integration
4
+ *
5
+ * This migration adds the thread_id column to the messages table
6
+ * and creates the necessary index for V2 API compatibility.
7
+ *
8
+ * Run: node migrate-v2.js
9
+ */
10
+
11
+ const Database = require('better-sqlite3');
12
+ const path = require('path');
13
+ const os = require('os');
14
+
15
+ const DB_PATH = path.join(os.homedir(), '.vibecodings', 'sessions.db');
16
+
17
+ function migrate() {
18
+ console.log('[Migration] Starting V2 schema migration...');
19
+ console.log(`[Migration] Database: ${DB_PATH}`);
20
+
21
+ const db = new Database(DB_PATH);
22
+
23
+ // Check if thread_id column already exists
24
+ const columns = db.prepare('PRAGMA table_info(messages)').all();
25
+ const hasThreadId = columns.some(col => col.name === 'thread_id');
26
+
27
+ if (hasThreadId) {
28
+ console.log('[Migration] ✅ thread_id column already exists. No migration needed.');
29
+ db.close();
30
+ return;
31
+ }
32
+
33
+ console.log('[Migration] Adding thread_id column...');
34
+
35
+ try {
36
+ // Add thread_id column
37
+ db.exec(`ALTER TABLE messages ADD COLUMN thread_id TEXT;`);
38
+
39
+ // Create index for thread_id
40
+ db.exec(`
41
+ CREATE INDEX IF NOT EXISTS idx_messages_thread_id
42
+ ON messages(thread_id);
43
+ `);
44
+
45
+ console.log('[Migration] ✅ Successfully added thread_id column and index');
46
+
47
+ // Verify migration
48
+ const newColumns = db.prepare('PRAGMA table_info(messages)').all();
49
+ const threadIdColumn = newColumns.find(col => col.name === 'thread_id');
50
+
51
+ if (threadIdColumn) {
52
+ console.log('[Migration] ✅ Verification passed');
53
+ console.log(`[Migration] Column details: ${JSON.stringify(threadIdColumn)}`);
54
+ } else {
55
+ console.error('[Migration] ❌ Verification failed - column not found after migration');
56
+ }
57
+ } catch (error) {
58
+ console.error('[Migration] ❌ Migration failed:', error.message);
59
+ throw error;
60
+ } finally {
61
+ db.close();
62
+ }
63
+
64
+ console.log('[Migration] Complete!');
65
+ }
66
+
67
+ // Run migration
68
+ if (require.main === module) {
69
+ migrate();
70
+ }
71
+
72
+ module.exports = { migrate };
@@ -0,0 +1,77 @@
1
+ /**
2
+ * MCP `list_changed` notification emitter
3
+ *
4
+ * Triggers Claude to refresh tool results without reconnection.
5
+ * Implements debouncing to prevent notification spam.
6
+ *
7
+ * This eliminates the need for 30-second polling loops,
8
+ * reducing API calls by ~90% and providing instant updates.
9
+ */
10
+
11
+ class NotificationEmitter {
12
+ constructor(server) {
13
+ this.server = server;
14
+ this.debounceTimers = {};
15
+ }
16
+
17
+ /**
18
+ * Emit list_changed notification with debouncing
19
+ * @param {string} reason - Why notification is being sent (for logging/debugging)
20
+ * @param {number} debounceMs - Debounce window in milliseconds (default: 1000ms)
21
+ */
22
+ emitChange(reason, debounceMs = 1000) {
23
+ // Debounce to prevent notification spam
24
+ // If we get multiple changes of the same type within the window,
25
+ // only emit one notification
26
+ if (this.debounceTimers[reason]) {
27
+ clearTimeout(this.debounceTimers[reason]);
28
+ }
29
+
30
+ this.debounceTimers[reason] = setTimeout(() => {
31
+ try {
32
+ this.server.notification({
33
+ method: 'notifications/list_changed'
34
+ });
35
+ delete this.debounceTimers[reason];
36
+ } catch (e) {
37
+ // Silent fail - notifications are best-effort
38
+ // If notification fails, Claude will continue working normally
39
+ }
40
+ }, debounceMs);
41
+ }
42
+
43
+ /**
44
+ * Emit immediately without debouncing
45
+ * Use for urgent updates like direct mentions
46
+ */
47
+ emitImmediate() {
48
+ try {
49
+ this.server.notification({
50
+ method: 'notifications/list_changed'
51
+ });
52
+ } catch (e) {
53
+ // Silent fail
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Cancel pending notifications for a specific reason
59
+ * Useful when shutting down or cleaning up
60
+ */
61
+ cancel(reason) {
62
+ if (this.debounceTimers[reason]) {
63
+ clearTimeout(this.debounceTimers[reason]);
64
+ delete this.debounceTimers[reason];
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Cancel all pending notifications
70
+ */
71
+ cancelAll() {
72
+ Object.values(this.debounceTimers).forEach(timer => clearTimeout(timer));
73
+ this.debounceTimers = {};
74
+ }
75
+ }
76
+
77
+ module.exports = NotificationEmitter;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slashvibe-mcp",
3
- "version": "0.3.22",
3
+ "version": "0.3.23",
4
4
  "mcpName": "io.github.vibecodinginc/vibe-mcp",
5
5
  "description": "Social MCP server - DMs, presence, discovery, and games for AI-assisted developers. Works with Claude Code, Cursor, VS Code, Windsurf, and any MCP client.",
6
6
  "main": "index.js",
@@ -47,15 +47,7 @@
47
47
  "node": ">=18.0.0"
48
48
  },
49
49
  "files": [
50
- "index.js",
51
- "config.js",
52
- "crypto.js",
53
- "discord.js",
54
- "memory.js",
55
- "notify.js",
56
- "presence.js",
57
- "prompts.js",
58
- "twitter.js",
50
+ "*.js",
59
51
  "version.json",
60
52
  "tools/",
61
53
  "store/",
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Post-install script for slashvibe-mcp
5
+ * Sets up MCP server configuration and CLAUDE.md for Claude Code
6
+ */
7
+
8
+ import fs from 'fs/promises';
9
+ import path from 'path';
10
+ import { homedir } from 'os';
11
+ import { fileURLToPath } from 'url';
12
+
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = path.dirname(__filename);
15
+
16
+ const HOME = homedir();
17
+ const CLAUDE_CONFIG_DIR = path.join(HOME, '.config', 'claude-code');
18
+ const MCP_CONFIG_PATH = path.join(CLAUDE_CONFIG_DIR, 'mcp.json');
19
+ const CLAUDE_MD_DIR = path.join(HOME, '.claude');
20
+ const CLAUDE_MD_PATH = path.join(CLAUDE_MD_DIR, 'CLAUDE.md');
21
+ const VIBE_MARKER = '## /vibe - Terminal-Native Social';
22
+
23
+ async function setup() {
24
+ console.log('\n📦 Setting up /vibe...\n');
25
+
26
+ try {
27
+ // Ensure config directory exists
28
+ await fs.mkdir(CLAUDE_CONFIG_DIR, { recursive: true });
29
+ await fs.mkdir(CLAUDE_MD_DIR, { recursive: true });
30
+
31
+ // Read existing MCP config or create new
32
+ let config = { mcpServers: {} };
33
+ try {
34
+ const existing = await fs.readFile(MCP_CONFIG_PATH, 'utf-8');
35
+ config = JSON.parse(existing);
36
+ if (!config.mcpServers) config.mcpServers = {};
37
+ } catch (error) {
38
+ // File doesn't exist, use default
39
+ }
40
+
41
+ // Use npx to run slashvibe-mcp — works regardless of install location
42
+ config.mcpServers.vibe = {
43
+ command: 'npx',
44
+ args: ['-y', 'slashvibe-mcp']
45
+ };
46
+
47
+ // Write config
48
+ await fs.writeFile(MCP_CONFIG_PATH, JSON.stringify(config, null, 2));
49
+
50
+ // Inject CLAUDE.md template for dashboard mode
51
+ await injectClaudeMd();
52
+
53
+ console.log('✅ /vibe MCP server configured\n');
54
+ console.log('Next steps:');
55
+ console.log(' 1. Restart Claude Code');
56
+ console.log(' 2. Run: vibe init @yourusername\n');
57
+ console.log('📖 Docs: https://slashvibe.dev\n');
58
+ } catch (error) {
59
+ console.error('⚠️ Setup incomplete:', error.message);
60
+ console.error('\nManual setup:');
61
+ console.error(' Add to ~/.config/claude-code/mcp.json:\n');
62
+ console.error(' {');
63
+ console.error(' "mcpServers": {');
64
+ console.error(' "vibe": {');
65
+ console.error(' "command": "npx",');
66
+ console.error(' "args": ["-y", "slashvibe-mcp"]');
67
+ console.error(' }');
68
+ console.error(' }');
69
+ console.error(' }\n');
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Inject /vibe CLAUDE.md template for dashboard mode and hint handling
75
+ * - If CLAUDE.md doesn't exist, create it with template
76
+ * - If exists but no /vibe section, append template
77
+ * - If /vibe section exists, update it with latest template
78
+ */
79
+ async function injectClaudeMd() {
80
+ try {
81
+ // Find the template relative to this script
82
+ // When installed via npm: ../dashboard/VIBE_CLAUDE_MD_TEMPLATE.md
83
+ const templatePath = path.join(__dirname, '..', 'dashboard', 'VIBE_CLAUDE_MD_TEMPLATE.md');
84
+
85
+ let template;
86
+ try {
87
+ template = await fs.readFile(templatePath, 'utf-8');
88
+ } catch (e) {
89
+ // Template not found (might be dev environment), skip injection
90
+ console.log('ℹ️ CLAUDE.md template not found, skipping dashboard setup');
91
+ return;
92
+ }
93
+
94
+ // Check if CLAUDE.md exists
95
+ let existingContent = '';
96
+ try {
97
+ existingContent = await fs.readFile(CLAUDE_MD_PATH, 'utf-8');
98
+ } catch (e) {
99
+ // File doesn't exist, will create new
100
+ }
101
+
102
+ if (existingContent.includes(VIBE_MARKER)) {
103
+ // /vibe section exists - replace it with updated template
104
+ // Find the section and replace it
105
+ const startIndex = existingContent.indexOf(VIBE_MARKER);
106
+
107
+ // Find the next ## header (or end of file)
108
+ const afterMarker = existingContent.substring(startIndex + VIBE_MARKER.length);
109
+ const nextSectionMatch = afterMarker.match(/\n## (?!\/vibe)/);
110
+
111
+ let endIndex;
112
+ if (nextSectionMatch) {
113
+ endIndex = startIndex + VIBE_MARKER.length + nextSectionMatch.index;
114
+ } else {
115
+ endIndex = existingContent.length;
116
+ }
117
+
118
+ // Replace the section
119
+ const beforeSection = existingContent.substring(0, startIndex);
120
+ const afterSection = existingContent.substring(endIndex);
121
+ const newContent = beforeSection + template + afterSection;
122
+
123
+ await fs.writeFile(CLAUDE_MD_PATH, newContent);
124
+ console.log('✅ CLAUDE.md updated with latest /vibe dashboard');
125
+ } else if (existingContent) {
126
+ // CLAUDE.md exists but no /vibe section - append
127
+ const newContent = existingContent + '\n\n' + template;
128
+ await fs.writeFile(CLAUDE_MD_PATH, newContent);
129
+ console.log('✅ /vibe dashboard added to CLAUDE.md');
130
+ } else {
131
+ // No CLAUDE.md - create with template
132
+ await fs.writeFile(CLAUDE_MD_PATH, template);
133
+ console.log('✅ CLAUDE.md created with /vibe dashboard');
134
+ }
135
+ } catch (error) {
136
+ // Non-fatal - log but don't fail installation
137
+ console.log('ℹ️ Could not update CLAUDE.md:', error.message);
138
+ }
139
+ }
140
+
141
+ setup();
@@ -0,0 +1,20 @@
1
+ // Test the skills bootstrap functionality
2
+ const skillsBootstrap = require('./tools/skills-bootstrap.js');
3
+
4
+ async function testBootstrap() {
5
+ try {
6
+ console.log('Testing skills bootstrap...');
7
+
8
+ // Check status first
9
+ const statusResult = await skillsBootstrap.handler({ action: 'status' });
10
+ console.log('Status check:', statusResult);
11
+
12
+ // Seed the marketplace
13
+ const seedResult = await skillsBootstrap.handler({ action: 'seed' });
14
+ console.log('Seed result:', seedResult);
15
+ } catch (error) {
16
+ console.error('Bootstrap test failed:', error);
17
+ }
18
+ }
19
+
20
+ testBootstrap();
@@ -0,0 +1,385 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * V2 Postgres Integration Test Suite
4
+ *
5
+ * Tests the complete message flow:
6
+ * 1. SQLite schema validation
7
+ * 2. Message saving (optimistic UI)
8
+ * 3. thread_id extraction from V2 API
9
+ * 4. Message status updates
10
+ * 5. Thread retrieval
11
+ *
12
+ * Run: node test-v2-integration.js
13
+ */
14
+
15
+ const Database = require('better-sqlite3');
16
+ const path = require('path');
17
+ const os = require('os');
18
+ const sqlite = require('./store/sqlite');
19
+
20
+ const DB_PATH = path.join(os.homedir(), '.vibecodings', 'sessions.db');
21
+
22
+ class V2IntegrationTest {
23
+ constructor() {
24
+ this.db = new Database(DB_PATH);
25
+ this.results = {
26
+ passed: [],
27
+ failed: [],
28
+ warnings: []
29
+ };
30
+ }
31
+
32
+ log(emoji, message) {
33
+ console.log(`${emoji} ${message}`);
34
+ }
35
+
36
+ pass(test, details = '') {
37
+ this.results.passed.push({ test, details });
38
+ this.log('✅', `${test}${details ? ': ' + details : ''}`);
39
+ }
40
+
41
+ fail(test, error) {
42
+ this.results.failed.push({ test, error });
43
+ this.log('❌', `${test}: ${error}`);
44
+ }
45
+
46
+ warn(test, message) {
47
+ this.results.warnings.push({ test, message });
48
+ this.log('⚠️', `${test}: ${message}`);
49
+ }
50
+
51
+ // Test 1: Schema Validation
52
+ testSchema() {
53
+ this.log('🔍', 'Test 1: Validating SQLite schema...');
54
+
55
+ const columns = this.db.prepare('PRAGMA table_info(messages)').all();
56
+ const columnNames = columns.map(c => c.name);
57
+
58
+ const requiredColumns = [
59
+ 'local_id',
60
+ 'server_id',
61
+ 'thread_id', // V2 requirement
62
+ 'from_handle',
63
+ 'to_handle',
64
+ 'content',
65
+ 'created_at',
66
+ 'status',
67
+ 'sent_at',
68
+ 'delivered_at',
69
+ 'read_at',
70
+ 'synced_at',
71
+ 'retry_count'
72
+ ];
73
+
74
+ const missing = requiredColumns.filter(col => !columnNames.includes(col));
75
+
76
+ if (missing.length > 0) {
77
+ this.fail('Schema validation', `Missing columns: ${missing.join(', ')}`);
78
+ return false;
79
+ }
80
+
81
+ this.pass('Schema validation', `All ${requiredColumns.length} columns present`);
82
+
83
+ // Check indexes
84
+ const indexes = this.db.prepare("SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='messages'").all();
85
+ const indexNames = indexes.map(i => i.name);
86
+
87
+ if (!indexNames.includes('idx_messages_thread_id')) {
88
+ this.warn('Index check', 'idx_messages_thread_id missing');
89
+ } else {
90
+ this.pass('Index check', 'thread_id index exists');
91
+ }
92
+
93
+ return true;
94
+ }
95
+
96
+ // Test 2: Message Saving (Optimistic UI)
97
+ testMessageSaving() {
98
+ this.log('🔍', 'Test 2: Testing optimistic message save...');
99
+
100
+ const testMessage = {
101
+ from_handle: 'test_alice',
102
+ to_handle: 'test_bob',
103
+ content: 'Test message for V2 integration',
104
+ status: 'pending'
105
+ };
106
+
107
+ try {
108
+ const local_id = sqlite.saveLocalMessage(testMessage);
109
+
110
+ if (!local_id) {
111
+ this.fail('Message saving', 'No local_id returned');
112
+ return null;
113
+ }
114
+
115
+ // Verify it was saved
116
+ const saved = this.db.prepare('SELECT * FROM messages WHERE local_id = ?').get(local_id);
117
+
118
+ if (!saved) {
119
+ this.fail('Message saving', 'Message not found after save');
120
+ return null;
121
+ }
122
+
123
+ if (saved.status !== 'pending') {
124
+ this.fail('Message saving', `Expected status 'pending', got '${saved.status}'`);
125
+ return null;
126
+ }
127
+
128
+ this.pass('Message saving', `Saved with local_id: ${local_id.slice(0, 12)}...`);
129
+ return local_id;
130
+ } catch (error) {
131
+ this.fail('Message saving', error.message);
132
+ return null;
133
+ }
134
+ }
135
+
136
+ // Test 3: Status Update with thread_id
137
+ testStatusUpdate(local_id) {
138
+ if (!local_id) {
139
+ this.warn('Status update', 'Skipped (no local_id from previous test)');
140
+ return;
141
+ }
142
+
143
+ this.log('🔍', 'Test 3: Testing status update with thread_id...');
144
+
145
+ try {
146
+ // Simulate V2 API response with server_id and thread_id
147
+ const server_id = 'msg_test_' + Date.now();
148
+ const thread_id = 'thread_test_' + Date.now();
149
+
150
+ sqlite.updateMessageStatus(local_id, 'sent', server_id, thread_id);
151
+
152
+ // Verify update
153
+ const updated = this.db.prepare('SELECT * FROM messages WHERE local_id = ?').get(local_id);
154
+
155
+ if (updated.status !== 'sent') {
156
+ this.fail('Status update', `Expected status 'sent', got '${updated.status}'`);
157
+ return;
158
+ }
159
+
160
+ if (updated.server_id !== server_id) {
161
+ this.fail('Status update', `server_id mismatch`);
162
+ return;
163
+ }
164
+
165
+ if (updated.thread_id !== thread_id) {
166
+ this.fail('Status update', `thread_id not saved correctly`);
167
+ return;
168
+ }
169
+
170
+ if (!updated.sent_at) {
171
+ this.warn('Status update', 'sent_at timestamp not set');
172
+ }
173
+
174
+ this.pass('Status update', `Status: ${updated.status}, thread_id: ${thread_id.slice(0, 20)}...`);
175
+ } catch (error) {
176
+ this.fail('Status update', error.message);
177
+ }
178
+ }
179
+
180
+ // Test 4: Thread Retrieval
181
+ testThreadRetrieval() {
182
+ this.log('🔍', 'Test 4: Testing thread retrieval...');
183
+
184
+ try {
185
+ const messages = sqlite.getThreadMessages('test_alice', 'test_bob', 100);
186
+
187
+ if (!Array.isArray(messages)) {
188
+ this.fail('Thread retrieval', 'Did not return an array');
189
+ return;
190
+ }
191
+
192
+ if (messages.length === 0) {
193
+ this.warn('Thread retrieval', 'No messages found (expected from test 2)');
194
+ return;
195
+ }
196
+
197
+ const msg = messages[0];
198
+
199
+ // Check that thread_id is included
200
+ if (msg.thread_id === undefined) {
201
+ this.fail('Thread retrieval', 'Message missing thread_id field');
202
+ return;
203
+ }
204
+
205
+ this.pass('Thread retrieval', `Retrieved ${messages.length} message(s) with thread_id`);
206
+ } catch (error) {
207
+ this.fail('Thread retrieval', error.message);
208
+ }
209
+ }
210
+
211
+ // Test 5: Merge Server Messages (V2 format)
212
+ testMergeServerMessages() {
213
+ this.log('🔍', 'Test 5: Testing server message merge with V2 format...');
214
+
215
+ const v2Messages = [
216
+ {
217
+ id: 'msg_server_001',
218
+ thread_id: 'thread_xyz',
219
+ from: 'server_alice',
220
+ to: 'server_bob',
221
+ body: 'Server message 1',
222
+ created_at: new Date().toISOString()
223
+ },
224
+ {
225
+ id: 'msg_server_002',
226
+ thread_id: 'thread_xyz',
227
+ from: 'server_bob',
228
+ to: 'server_alice',
229
+ body: 'Server message 2',
230
+ created_at: new Date().toISOString()
231
+ }
232
+ ];
233
+
234
+ try {
235
+ const merged = sqlite.mergeServerMessages(v2Messages);
236
+
237
+ if (merged !== v2Messages.length) {
238
+ this.fail('Server message merge', `Expected ${v2Messages.length} merged, got ${merged}`);
239
+ return;
240
+ }
241
+
242
+ // Verify messages were saved with thread_id
243
+ const saved = this.db
244
+ .prepare(
245
+ `
246
+ SELECT * FROM messages
247
+ WHERE server_id IN ('msg_server_001', 'msg_server_002')
248
+ ORDER BY created_at
249
+ `
250
+ )
251
+ .all();
252
+
253
+ if (saved.length !== 2) {
254
+ this.fail('Server message merge', `Expected 2 messages, found ${saved.length}`);
255
+ return;
256
+ }
257
+
258
+ // Check thread_id was preserved
259
+ const threadIds = saved.map(m => m.thread_id);
260
+ if (!threadIds.every(id => id === 'thread_xyz')) {
261
+ this.fail('Server message merge', 'thread_id not preserved correctly');
262
+ return;
263
+ }
264
+
265
+ this.pass('Server message merge', `Merged ${merged} V2 messages with thread_id`);
266
+ } catch (error) {
267
+ this.fail('Server message merge', error.message);
268
+ }
269
+ }
270
+
271
+ // Test 6: Code Review - Check api.js integration
272
+ testCodeIntegration() {
273
+ this.log('🔍', 'Test 6: Code integration check...');
274
+
275
+ const apiCode = require('fs').readFileSync('./store/api.js', 'utf-8');
276
+
277
+ // Check 1: sendMessage saves to SQLite
278
+ if (!apiCode.includes('sqlite.saveLocalMessage')) {
279
+ this.fail('Code integration', 'api.js missing sqlite.saveLocalMessage call');
280
+ return;
281
+ }
282
+
283
+ // Check 2: sendMessage updates status with thread_id
284
+ if (!apiCode.includes('sqlite.updateMessageStatus')) {
285
+ this.fail('Code integration', 'api.js missing sqlite.updateMessageStatus call');
286
+ return;
287
+ }
288
+
289
+ // Check 3: thread_id extraction from V2 response
290
+ if (!apiCode.includes('thread_id')) {
291
+ this.fail('Code integration', 'api.js not extracting thread_id from response');
292
+ return;
293
+ }
294
+
295
+ // Check 4: getThread uses sqlite
296
+ if (!apiCode.includes('sqlite.getThreadMessages')) {
297
+ this.fail('Code integration', 'api.js getThread not using SQLite');
298
+ return;
299
+ }
300
+
301
+ // Check 5: getInbox uses sqlite
302
+ if (!apiCode.includes('sqlite.getInboxThreads')) {
303
+ this.warn('Code integration', 'api.js getInbox might not be using SQLite optimally');
304
+ }
305
+
306
+ this.pass('Code integration', 'All critical V2 integration points found in api.js');
307
+ }
308
+
309
+ // Cleanup test data
310
+ cleanup() {
311
+ this.log('🧹', 'Cleaning up test data...');
312
+
313
+ try {
314
+ this.db.exec(`
315
+ DELETE FROM messages
316
+ WHERE from_handle LIKE 'test_%'
317
+ OR from_handle LIKE 'server_%'
318
+ OR to_handle LIKE 'test_%'
319
+ OR to_handle LIKE 'server_%'
320
+ `);
321
+ this.pass('Cleanup', 'Test data removed');
322
+ } catch (error) {
323
+ this.warn('Cleanup', error.message);
324
+ }
325
+ }
326
+
327
+ // Run all tests
328
+ async runAll() {
329
+ console.log('\n════════════════════════════════════════════════════════');
330
+ console.log(' V2 Postgres Integration Test Suite');
331
+ console.log('════════════════════════════════════════════════════════\n');
332
+
333
+ // Run tests
334
+ const schemaOk = this.testSchema();
335
+
336
+ if (schemaOk) {
337
+ const local_id = this.testMessageSaving();
338
+ this.testStatusUpdate(local_id);
339
+ this.testThreadRetrieval();
340
+ this.testMergeServerMessages();
341
+ }
342
+
343
+ this.testCodeIntegration();
344
+ this.cleanup();
345
+
346
+ // Print summary
347
+ console.log('\n════════════════════════════════════════════════════════');
348
+ console.log(' Test Summary');
349
+ console.log('════════════════════════════════════════════════════════\n');
350
+
351
+ console.log(`✅ Passed: ${this.results.passed.length}`);
352
+ console.log(`❌ Failed: ${this.results.failed.length}`);
353
+ console.log(`⚠️ Warnings: ${this.results.warnings.length}`);
354
+
355
+ if (this.results.failed.length > 0) {
356
+ console.log('\n❌ Failed Tests:');
357
+ this.results.failed.forEach(({ test, error }) => {
358
+ console.log(` - ${test}: ${error}`);
359
+ });
360
+ }
361
+
362
+ if (this.results.warnings.length > 0) {
363
+ console.log('\n⚠️ Warnings:');
364
+ this.results.warnings.forEach(({ test, message }) => {
365
+ console.log(` - ${test}: ${message}`);
366
+ });
367
+ }
368
+
369
+ console.log('\n════════════════════════════════════════════════════════\n');
370
+
371
+ // Close database
372
+ this.db.close();
373
+
374
+ // Exit with appropriate code
375
+ process.exit(this.results.failed.length > 0 ? 1 : 0);
376
+ }
377
+ }
378
+
379
+ // Run tests
380
+ if (require.main === module) {
381
+ const test = new V2IntegrationTest();
382
+ test.runAll();
383
+ }
384
+
385
+ module.exports = V2IntegrationTest;
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * /vibe Webhook Server Runner
5
+ *
6
+ * Standalone webhook server for receiving real-time updates from external platforms.
7
+ * Run this on a public server to bridge external platforms into /vibe.
8
+ *
9
+ * Usage:
10
+ * node webhook-runner.js [--port 3001]
11
+ *
12
+ * Environment Variables:
13
+ * WEBHOOK_PORT=3001
14
+ * WEBHOOK_SECRET=your-webhook-secret
15
+ * TELEGRAM_WEBHOOK_SECRET=telegram-secret-token
16
+ * DISCORD_PUBLIC_KEY=discord-public-key
17
+ */
18
+
19
+ const express = require('express');
20
+ const cors = require('cors');
21
+ const { createWebhookHandler, getConfig, getSetupInstructions } = require('./bridges/webhook-server');
22
+
23
+ const app = express();
24
+
25
+ // Middleware
26
+ app.use(cors());
27
+ app.use(express.json({ limit: '10mb' }));
28
+ app.use(express.raw({ type: 'application/json', limit: '10mb' }));
29
+
30
+ // Health check
31
+ app.get('/health', (req, res) => {
32
+ res.json({
33
+ status: 'ok',
34
+ service: 'vibe-webhook-server',
35
+ timestamp: new Date().toISOString()
36
+ });
37
+ });
38
+
39
+ // Setup info endpoint
40
+ app.get('/setup', (req, res) => {
41
+ const instructions = getSetupInstructions();
42
+ res.json({
43
+ service: '/vibe Webhook Server',
44
+ instructions,
45
+ endpoints: {
46
+ telegram: '/webhook/telegram',
47
+ discord: '/webhook/discord',
48
+ health: '/health'
49
+ }
50
+ });
51
+ });
52
+
53
+ // Webhook endpoints
54
+ app.use('/webhook', createWebhookHandler());
55
+
56
+ // Error handler
57
+ app.use((err, req, res, next) => {
58
+ console.error('Webhook server error:', err);
59
+ res.status(500).json({
60
+ error: 'Internal server error',
61
+ timestamp: new Date().toISOString()
62
+ });
63
+ });
64
+
65
+ // Start server
66
+ function startServer() {
67
+ const config = getConfig();
68
+ const port = config.port;
69
+
70
+ const server = app.listen(port, () => {
71
+ console.log(`🌉 /vibe webhook server running on port ${port}`);
72
+ console.log();
73
+ console.log('📡 Endpoints:');
74
+ console.log(` Health: http://localhost:${port}/health`);
75
+ console.log(` Setup: http://localhost:${port}/setup`);
76
+ console.log(` Telegram: http://localhost:${port}/webhook/telegram`);
77
+ console.log(` Discord: http://localhost:${port}/webhook/discord`);
78
+ console.log();
79
+ console.log('🔧 Configuration:');
80
+ console.log(` Port: ${port}`);
81
+ console.log(` Webhook Secret: ${config.secret ? '✅ Set' : '❌ Not set'}`);
82
+ console.log(` Telegram Secret: ${config.telegramSecret ? '✅ Set' : '❌ Not set'}`);
83
+ console.log(` Discord Public Key: ${config.discordPublicKey ? '✅ Set' : '❌ Not set'}`);
84
+ console.log();
85
+ console.log('Visit /setup endpoint for platform-specific setup instructions.');
86
+ });
87
+
88
+ // Graceful shutdown
89
+ process.on('SIGINT', () => {
90
+ console.log('\n🛑 Shutting down webhook server...');
91
+ server.close(() => {
92
+ console.log('✅ Webhook server stopped');
93
+ process.exit(0);
94
+ });
95
+ });
96
+
97
+ return server;
98
+ }
99
+
100
+ // CLI handling
101
+ if (require.main === module) {
102
+ const args = process.argv.slice(2);
103
+
104
+ if (args.includes('--help') || args.includes('-h')) {
105
+ console.log(`
106
+ /vibe Webhook Server
107
+
108
+ Usage:
109
+ node webhook-runner.js [options]
110
+
111
+ Options:
112
+ --port PORT Webhook server port (default: 3001)
113
+ --help, -h Show this help
114
+
115
+ Environment Variables:
116
+ WEBHOOK_PORT Webhook server port
117
+ WEBHOOK_SECRET Secret for webhook signature verification
118
+ TELEGRAM_WEBHOOK_SECRET Telegram webhook secret token
119
+ DISCORD_PUBLIC_KEY Discord application public key
120
+
121
+ Examples:
122
+ node webhook-runner.js
123
+ node webhook-runner.js --port 8080
124
+ WEBHOOK_PORT=3001 node webhook-runner.js
125
+ `);
126
+ process.exit(0);
127
+ }
128
+
129
+ startServer();
130
+ }
131
+
132
+ module.exports = { app, startServer };