slashvibe-mcp 0.3.22 → 0.3.24
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/bridges/agent-gateway.js +351 -0
- package/bridges/bridge-monitor.js +399 -0
- package/bridges/discord-bot.js +421 -0
- package/bridges/farcaster.js +299 -0
- package/bridges/telegram.js +261 -0
- package/bridges/webhook-health.js +416 -0
- package/bridges/webhook-server.js +461 -0
- package/bridges/whatsapp.js +441 -0
- package/bridges/x-webhook.js +406 -0
- package/debug.js +12 -0
- package/eslint.config.js +54 -0
- package/games/arcade.js +403 -0
- package/games/chess.js +460 -0
- package/games/colorguess.js +344 -0
- package/games/crossword-words.js +175 -0
- package/games/crossword.js +463 -0
- package/games/drawing.js +352 -0
- package/games/gameroulette.js +290 -0
- package/games/gamerouter.js +334 -0
- package/games/gamestatus.js +337 -0
- package/games/guessnumber.js +209 -0
- package/games/hangman.js +330 -0
- package/games/memory.js +360 -0
- package/games/multiplayer-tictactoe.js +406 -0
- package/games/pixelart.js +406 -0
- package/games/quickduel.js +382 -0
- package/games/riddle.js +371 -0
- package/games/rockpaperscissors.js +284 -0
- package/games/snake.js +408 -0
- package/games/storybuilder.js +351 -0
- package/games/tictactoe.js +350 -0
- package/games/twentyquestions.js +379 -0
- package/games/twotruths.js +207 -0
- package/games/werewolf.js +506 -0
- package/games/wordassociation.js +293 -0
- package/games/wordchain.js +158 -0
- package/migrate-v2.js +72 -0
- package/notification-emitter.js +77 -0
- package/package.json +4 -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
|
+
}
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Gateway Bridge — Event push + local state for external agents
|
|
3
|
+
*
|
|
4
|
+
* The /vibe platform API (slashvibe.dev) already provides:
|
|
5
|
+
* - Messaging: POST/GET /api/messages
|
|
6
|
+
* - Presence: POST/GET /api/presence
|
|
7
|
+
* - Board: POST/GET /api/board (ships, ideas, requests)
|
|
8
|
+
* - Discovery: GET /api/discover
|
|
9
|
+
* - Agents: GET /api/agents
|
|
10
|
+
* - Auth: JWT via /api/auth/*
|
|
11
|
+
*
|
|
12
|
+
* This bridge fills the GAPS for external agent gateways (Clawdbot, @seth):
|
|
13
|
+
*
|
|
14
|
+
* 1. EVENT PUSH — Platform is pull-based. This pushes events to agents.
|
|
15
|
+
* 2. LOCAL STATE — Memory, reservations, and session data are local-only.
|
|
16
|
+
* 3. AIRC IDENTITY — Verifies agent identity via Ed25519 signatures.
|
|
17
|
+
* 4. AGENT REGISTRY — Tracks which agents are connected + their capabilities.
|
|
18
|
+
*
|
|
19
|
+
* External agents should use the platform API directly for:
|
|
20
|
+
* DMs, presence, board, discovery, profiles
|
|
21
|
+
* And use THIS bridge for:
|
|
22
|
+
* Event subscriptions, local memory queries, AIRC verification
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const crypto = require('../crypto');
|
|
26
|
+
const config = require('../config');
|
|
27
|
+
const memory = require('../memory');
|
|
28
|
+
const debug = require('../debug');
|
|
29
|
+
|
|
30
|
+
// ============ AGENT REGISTRY ============
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Known agent gateways
|
|
34
|
+
* @type {Map<string, {handle: string, publicKey: string, endpoint: string, capabilities: string[], registeredAt: number}>}
|
|
35
|
+
*/
|
|
36
|
+
const agentRegistry = new Map();
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Event subscriptions — agents subscribe to event types and get HTTP pushes
|
|
40
|
+
* @type {Map<string, {endpoint: string, events: string[], handle: string}>}
|
|
41
|
+
*/
|
|
42
|
+
const eventSubscriptions = new Map();
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Register an external agent gateway with AIRC identity
|
|
46
|
+
*
|
|
47
|
+
* @param {object} params
|
|
48
|
+
* @param {string} params.handle Agent handle (e.g. "seth-agent")
|
|
49
|
+
* @param {string} params.publicKey Base64 Ed25519 public key (AIRC)
|
|
50
|
+
* @param {string} [params.endpoint] HTTP callback URL for event pushes
|
|
51
|
+
* @param {string[]} [params.capabilities] What this agent can do
|
|
52
|
+
* @param {string} [params.signature] AIRC signature proving key ownership
|
|
53
|
+
* @returns {{success: boolean, agentId?: string, error?: string}}
|
|
54
|
+
*/
|
|
55
|
+
function registerAgent({ handle, publicKey, endpoint, capabilities = [], signature }) {
|
|
56
|
+
if (!handle || !publicKey) {
|
|
57
|
+
return { success: false, error: 'handle and publicKey required' };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Verify AIRC signature if provided (proves private key ownership)
|
|
61
|
+
if (signature) {
|
|
62
|
+
const valid = crypto.verify(
|
|
63
|
+
{ handle, publicKey, endpoint, capabilities },
|
|
64
|
+
publicKey
|
|
65
|
+
);
|
|
66
|
+
if (!valid) {
|
|
67
|
+
return { success: false, error: 'Invalid AIRC signature' };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const agentId = `agent_${handle}_${Date.now().toString(36)}`;
|
|
72
|
+
|
|
73
|
+
agentRegistry.set(handle, {
|
|
74
|
+
agentId,
|
|
75
|
+
handle,
|
|
76
|
+
publicKey,
|
|
77
|
+
endpoint: endpoint || null,
|
|
78
|
+
capabilities,
|
|
79
|
+
registeredAt: Date.now()
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
debug(`[agent-gateway] Registered @${handle} (${agentId})`);
|
|
83
|
+
return { success: true, agentId, handle };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Verify an AIRC-signed message from a registered agent
|
|
88
|
+
*
|
|
89
|
+
* @param {object} message Signed message with `from` and `signature` fields
|
|
90
|
+
* @returns {{valid: boolean, handle?: string, verified?: string, error?: string}}
|
|
91
|
+
*/
|
|
92
|
+
function verifyAgentMessage(message) {
|
|
93
|
+
if (!message || !message.from) {
|
|
94
|
+
return { valid: false, error: 'Missing from field' };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const agent = agentRegistry.get(message.from);
|
|
98
|
+
|
|
99
|
+
// AIRC-verified: registered agent with valid signature
|
|
100
|
+
if (agent && message.signature) {
|
|
101
|
+
const valid = crypto.verify(message, agent.publicKey);
|
|
102
|
+
if (!valid) {
|
|
103
|
+
return { valid: false, error: 'AIRC signature verification failed' };
|
|
104
|
+
}
|
|
105
|
+
return { valid: true, handle: message.from, verified: 'airc' };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Registered but unsigned — allow with lower trust
|
|
109
|
+
if (agent && !message.signature) {
|
|
110
|
+
debug(`[agent-gateway] Unsigned request from registered agent @${message.from}`);
|
|
111
|
+
return { valid: true, handle: message.from, verified: 'registered' };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { valid: false, error: `Unknown agent: ${message.from}. Register first via POST /agent/register` };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ============ EVENT PUSH ============
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Subscribe an agent to /vibe events (push model)
|
|
121
|
+
*
|
|
122
|
+
* Event types:
|
|
123
|
+
* - dm: New direct messages for the subscribed handle
|
|
124
|
+
* - mention: @mentions in feed/board
|
|
125
|
+
* - ship: New ships from connections
|
|
126
|
+
* - presence: People coming online/offline
|
|
127
|
+
* - handoff: Task handoff requests
|
|
128
|
+
*
|
|
129
|
+
* @param {string} handle Agent handle
|
|
130
|
+
* @param {string} endpoint HTTP callback URL to receive events
|
|
131
|
+
* @param {string[]} events Event types to subscribe to
|
|
132
|
+
* @returns {{success: boolean, subscribed: string[]}}
|
|
133
|
+
*/
|
|
134
|
+
function subscribe(handle, endpoint, events = ['dm', 'mention', 'ship', 'presence']) {
|
|
135
|
+
if (!endpoint) {
|
|
136
|
+
return { success: false, error: 'endpoint required' };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
eventSubscriptions.set(handle, { endpoint, events, handle });
|
|
140
|
+
debug(`[agent-gateway] @${handle} subscribed to [${events.join(', ')}] → ${endpoint}`);
|
|
141
|
+
return { success: true, subscribed: events };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Unsubscribe an agent from events
|
|
146
|
+
* @param {string} handle Agent handle
|
|
147
|
+
*/
|
|
148
|
+
function unsubscribe(handle) {
|
|
149
|
+
eventSubscriptions.delete(handle);
|
|
150
|
+
debug(`[agent-gateway] @${handle} unsubscribed`);
|
|
151
|
+
return { success: true };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Push an event to all subscribed agents
|
|
156
|
+
* AIRC-signed if we have a keypair (proves event came from /vibe)
|
|
157
|
+
*
|
|
158
|
+
* Called by notify.js and tool handlers when events occur.
|
|
159
|
+
*
|
|
160
|
+
* @param {string} eventType Event type (dm, mention, ship, presence, handoff)
|
|
161
|
+
* @param {object} eventData Event payload
|
|
162
|
+
*/
|
|
163
|
+
async function pushEvent(eventType, eventData) {
|
|
164
|
+
const keypair = config.getKeypair();
|
|
165
|
+
const myHandle = config.getHandle();
|
|
166
|
+
|
|
167
|
+
for (const [handle, sub] of eventSubscriptions) {
|
|
168
|
+
if (!sub.events.includes(eventType)) continue;
|
|
169
|
+
if (!sub.endpoint) continue;
|
|
170
|
+
|
|
171
|
+
const event = {
|
|
172
|
+
v: '0.1',
|
|
173
|
+
type: 'vibe_event',
|
|
174
|
+
event: eventType,
|
|
175
|
+
data: eventData,
|
|
176
|
+
from: myHandle || 'vibe-mcp',
|
|
177
|
+
timestamp: Math.floor(Date.now() / 1000)
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// AIRC sign so receiver can verify this came from /vibe
|
|
181
|
+
if (keypair) {
|
|
182
|
+
event.signature = crypto.sign(event, keypair.privateKey);
|
|
183
|
+
event.publicKey = keypair.publicKey;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
const response = await fetch(sub.endpoint, {
|
|
188
|
+
method: 'POST',
|
|
189
|
+
headers: {
|
|
190
|
+
'Content-Type': 'application/json',
|
|
191
|
+
'X-Vibe-Event': eventType,
|
|
192
|
+
'X-Vibe-Source': 'vibe-mcp'
|
|
193
|
+
},
|
|
194
|
+
body: JSON.stringify(event)
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
if (!response.ok) {
|
|
198
|
+
debug(`[agent-gateway] Push to @${handle} failed: HTTP ${response.status}`);
|
|
199
|
+
}
|
|
200
|
+
} catch (e) {
|
|
201
|
+
debug(`[agent-gateway] Push to @${handle} failed: ${e.message}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ============ LOCAL STATE QUERIES ============
|
|
207
|
+
// These expose data that lives only in the MCP process, not on the platform API
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Query local memory (thread-scoped JSONL files)
|
|
211
|
+
* Platform API doesn't have memory — it's local-first by design
|
|
212
|
+
*/
|
|
213
|
+
function queryMemory(handle, limit = 10, search = null) {
|
|
214
|
+
const memories = memory.recall(handle, limit);
|
|
215
|
+
|
|
216
|
+
if (search && memories.length > 0) {
|
|
217
|
+
return memories.filter(m =>
|
|
218
|
+
m.observation.toLowerCase().includes(search.toLowerCase())
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return memories;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Store a memory observation (local-first)
|
|
227
|
+
*/
|
|
228
|
+
function storeMemory(handle, observation) {
|
|
229
|
+
memory.remember(handle, observation);
|
|
230
|
+
return { success: true, handle, observation };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* List all memory threads
|
|
235
|
+
*/
|
|
236
|
+
function listMemoryThreads() {
|
|
237
|
+
return memory.listThreads();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ============ HTTP HANDLER ============
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* HTTP handler for the agent gateway
|
|
244
|
+
*
|
|
245
|
+
* Routes:
|
|
246
|
+
* POST /agent/register — Register agent with AIRC public key
|
|
247
|
+
* POST /agent/subscribe — Subscribe to event pushes
|
|
248
|
+
* POST /agent/unsubscribe — Unsubscribe from events
|
|
249
|
+
* POST /agent/memory — Query/store local memory
|
|
250
|
+
* GET /agent/status — Gateway health + registered agents
|
|
251
|
+
*
|
|
252
|
+
* For everything else, agents hit the platform API directly:
|
|
253
|
+
* POST https://slashvibe.dev/api/messages — Send DMs
|
|
254
|
+
* GET https://slashvibe.dev/api/presence — Who's online
|
|
255
|
+
* POST https://slashvibe.dev/api/board — Ship/idea/request
|
|
256
|
+
* GET https://slashvibe.dev/api/discover — Find people
|
|
257
|
+
* GET https://slashvibe.dev/api/agents — Agent directory
|
|
258
|
+
*/
|
|
259
|
+
async function handleRequest(req) {
|
|
260
|
+
const { path, method, body } = req;
|
|
261
|
+
|
|
262
|
+
// Health / status
|
|
263
|
+
if (path === '/agent/status' && method === 'GET') {
|
|
264
|
+
const agents = [];
|
|
265
|
+
for (const [, agent] of agentRegistry) {
|
|
266
|
+
agents.push({
|
|
267
|
+
handle: agent.handle,
|
|
268
|
+
capabilities: agent.capabilities,
|
|
269
|
+
registeredAt: agent.registeredAt,
|
|
270
|
+
hasEndpoint: !!agent.endpoint
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
status: 'ok',
|
|
276
|
+
agents,
|
|
277
|
+
subscriptions: eventSubscriptions.size,
|
|
278
|
+
version: '0.1.0',
|
|
279
|
+
platform_api: config.getApiUrl(),
|
|
280
|
+
note: 'For DMs, presence, board, discovery — use the platform API directly'
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (method !== 'POST') {
|
|
285
|
+
return { error: 'Method not allowed', status: 405 };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const data = typeof body === 'string' ? JSON.parse(body) : body;
|
|
289
|
+
|
|
290
|
+
switch (path) {
|
|
291
|
+
case '/agent/register':
|
|
292
|
+
return registerAgent(data);
|
|
293
|
+
|
|
294
|
+
case '/agent/subscribe': {
|
|
295
|
+
const v = verifyAgentMessage(data);
|
|
296
|
+
if (!v.valid) return { success: false, error: v.error, status: 401 };
|
|
297
|
+
return subscribe(v.handle, data.endpoint, data.events);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
case '/agent/unsubscribe': {
|
|
301
|
+
const v = verifyAgentMessage(data);
|
|
302
|
+
if (!v.valid) return { success: false, error: v.error, status: 401 };
|
|
303
|
+
return unsubscribe(v.handle);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
case '/agent/memory': {
|
|
307
|
+
const v = verifyAgentMessage(data);
|
|
308
|
+
if (!v.valid) return { success: false, error: v.error, status: 401 };
|
|
309
|
+
|
|
310
|
+
if (data.action === 'recall') {
|
|
311
|
+
const memories = queryMemory(data.handle, data.limit, data.search);
|
|
312
|
+
return { success: true, memories };
|
|
313
|
+
}
|
|
314
|
+
if (data.action === 'remember') {
|
|
315
|
+
return storeMemory(data.handle, data.observation);
|
|
316
|
+
}
|
|
317
|
+
if (data.action === 'threads') {
|
|
318
|
+
return { success: true, threads: listMemoryThreads() };
|
|
319
|
+
}
|
|
320
|
+
return { success: false, error: 'action must be: recall, remember, or threads' };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
default:
|
|
324
|
+
return { error: 'Not found', status: 404 };
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ============ EXPORTS ============
|
|
329
|
+
|
|
330
|
+
module.exports = {
|
|
331
|
+
// Registration
|
|
332
|
+
registerAgent,
|
|
333
|
+
verifyAgentMessage,
|
|
334
|
+
|
|
335
|
+
// Event push (the main value-add over platform API)
|
|
336
|
+
subscribe,
|
|
337
|
+
unsubscribe,
|
|
338
|
+
pushEvent,
|
|
339
|
+
|
|
340
|
+
// Local state (not on platform)
|
|
341
|
+
queryMemory,
|
|
342
|
+
storeMemory,
|
|
343
|
+
listMemoryThreads,
|
|
344
|
+
|
|
345
|
+
// HTTP handler
|
|
346
|
+
handleRequest,
|
|
347
|
+
|
|
348
|
+
// Registry access
|
|
349
|
+
getAgentRegistry: () => agentRegistry,
|
|
350
|
+
getSubscriptions: () => eventSubscriptions
|
|
351
|
+
};
|