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 +107 -0
- package/auto-update.js +125 -0
- package/debug.js +12 -0
- package/eslint.config.js +54 -0
- package/migrate-v2.js +72 -0
- package/notification-emitter.js +77 -0
- package/package.json +2 -10
- package/post-install.js +141 -0
- package/test-skills-bootstrap.js +20 -0
- package/test-v2-integration.js +385 -0
- package/webhook-runner.js +132 -0
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
package/eslint.config.js
ADDED
|
@@ -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.
|
|
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
|
-
"
|
|
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/",
|
package/post-install.js
ADDED
|
@@ -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 };
|