slashvibe-mcp 0.2.0 โ†’ 0.2.2

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/discord.js CHANGED
@@ -116,6 +116,24 @@ async function postAnnouncement(message) {
116
116
  return post(null, { embed });
117
117
  }
118
118
 
119
+ /**
120
+ * Post a conversation highlight
121
+ */
122
+ async function postHighlight(handle, title, summary, threads = []) {
123
+ const threadList = threads.length > 0
124
+ ? '\n\n**Open threads:**\n' + threads.map(t => `โ€ข ${t}`).join('\n')
125
+ : '';
126
+
127
+ const embed = {
128
+ color: 0xF39C12, // Gold/amber for highlights
129
+ title: `๐Ÿ’ฌ ${title}`,
130
+ description: summary + threadList,
131
+ footer: { text: `shared by @${handle} ยท slashvibe.dev` },
132
+ timestamp: new Date().toISOString()
133
+ };
134
+ return post(null, { embed });
135
+ }
136
+
119
137
  /**
120
138
  * Post who's currently online
121
139
  */
@@ -147,5 +165,6 @@ module.exports = {
147
165
  postActivity,
148
166
  postStatus,
149
167
  postAnnouncement,
168
+ postHighlight,
150
169
  postOnlineList
151
170
  };
package/index.js CHANGED
@@ -9,10 +9,45 @@
9
9
  const presence = require('./presence');
10
10
  const config = require('./config');
11
11
  const store = require('./store');
12
+ const prompts = require('./prompts');
12
13
 
13
14
  // Tools that shouldn't show presence footer (would be redundant/noisy)
14
15
  const SKIP_FOOTER_TOOLS = ['vibe_init', 'vibe_doctor', 'vibe_test', 'vibe_update'];
15
16
 
17
+ // Infer user prompt from tool arguments (for pattern logging)
18
+ function inferPromptFromArgs(toolName, args) {
19
+ const action = toolName.replace('vibe_', '');
20
+ const handle = args.handle ? `@${args.handle.replace('@', '')}` : '';
21
+ const message = args.message ? `"${args.message.slice(0, 50)}..."` : '';
22
+ const note = args.note || '';
23
+ const mood = args.mood || '';
24
+ const reaction = args.reaction || '';
25
+
26
+ switch (action) {
27
+ case 'start': return 'start vibing';
28
+ case 'who': return 'who is online';
29
+ case 'ping': return `ping ${handle} ${note}`.trim();
30
+ case 'react': return `react ${reaction} to ${handle}`.trim();
31
+ case 'dm': return `message ${handle} ${message}`.trim();
32
+ case 'inbox': return 'check inbox';
33
+ case 'open': return `open thread with ${handle}`;
34
+ case 'status': return `set status to ${mood}`;
35
+ case 'context': return 'share context';
36
+ case 'summarize': return 'summarize session';
37
+ case 'bye': return 'end session';
38
+ case 'remember': return `remember about ${handle}`;
39
+ case 'recall': return `recall ${handle}`;
40
+ case 'forget': return `forget ${handle}`;
41
+ case 'board': return args.content ? 'post to board' : 'view board';
42
+ case 'invite': return 'generate invite';
43
+ case 'echo': return 'send feedback';
44
+ case 'x_mentions': return 'check x mentions';
45
+ case 'x_reply': return 'reply on x';
46
+ case 'handoff': return `handoff task to ${handle}`;
47
+ default: return `${action} ${handle}`.trim() || null;
48
+ }
49
+ }
50
+
16
51
  // Generate terminal title escape sequence (OSC 0)
17
52
  function getTerminalTitle(onlineCount, unreadCount, lastActivity) {
18
53
  const parts = [];
@@ -127,6 +162,8 @@ const tools = {
127
162
  vibe_summarize: require('./tools/summarize'),
128
163
  vibe_bye: require('./tools/bye'),
129
164
  vibe_game: require('./tools/game'),
165
+ // AIRC Handoff (v1) โ€” context portability
166
+ vibe_handoff: require('./tools/handoff'),
130
167
  // Memory tools (Tier 1 โ€” Collaborative Memory)
131
168
  vibe_remember: require('./tools/remember'),
132
169
  vibe_recall: require('./tools/recall'),
@@ -145,7 +182,12 @@ const tools = {
145
182
  vibe_echo: require('./tools/echo'),
146
183
  // X/Twitter bridge
147
184
  vibe_x_mentions: require('./tools/x-mentions'),
148
- vibe_x_reply: require('./tools/x-reply')
185
+ vibe_x_reply: require('./tools/x-reply'),
186
+ // Unified social inbox (Phase 1a)
187
+ vibe_social_inbox: require('./tools/social-inbox'),
188
+ vibe_social_post: require('./tools/social-post'),
189
+ // Language evolution
190
+ vibe_patterns: require('./tools/patterns')
149
191
  };
150
192
 
151
193
  /**
@@ -196,7 +238,19 @@ class VibeMCPServer {
196
238
  }
197
239
 
198
240
  try {
199
- const result = await tool.handler(params.arguments || {});
241
+ // Log prompt pattern (if _prompt passed) or infer from args
242
+ const args = params.arguments || {};
243
+ const inferredPrompt = args._prompt || inferPromptFromArgs(params.name, args);
244
+ if (inferredPrompt) {
245
+ prompts.log(inferredPrompt, {
246
+ tool: params.name,
247
+ action: params.name.replace('vibe_', ''),
248
+ target: args.handle || args.to || null,
249
+ transform: args.format || args.category || null
250
+ });
251
+ }
252
+
253
+ const result = await tool.handler(args);
200
254
 
201
255
  // Add ambient presence footer (unless tool is in skip list)
202
256
  let footer = '';
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "slashvibe-mcp",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
+ "mcpName": "io.github.brightseth/vibe",
4
5
  "description": "Social layer for Claude Code - DMs, presence, and connection between AI-assisted developers",
5
6
  "main": "index.js",
6
7
  "bin": {
@@ -39,6 +40,7 @@
39
40
  "memory.js",
40
41
  "notify.js",
41
42
  "presence.js",
43
+ "prompts.js",
42
44
  "twitter.js",
43
45
  "version.json",
44
46
  "tools/",
package/prompts.js ADDED
@@ -0,0 +1,141 @@
1
+ /**
2
+ * /vibe Prompt Pattern Logger
3
+ *
4
+ * Captures how people ask for things to identify emergent language constructs.
5
+ * Local-first: ~/.vibe/prompts.jsonl
6
+ * Server: anonymized patterns for aggregate analysis
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const config = require('./config');
12
+
13
+ const PROMPTS_FILE = path.join(config.VIBE_DIR, 'prompts.jsonl');
14
+
15
+ /**
16
+ * Log a prompt and what it resolved to
17
+ */
18
+ function log(prompt, resolution) {
19
+ if (!prompt) return;
20
+
21
+ const entry = {
22
+ id: `pr_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`,
23
+ ts: new Date().toISOString(),
24
+ prompt: prompt.slice(0, 500), // Cap length
25
+ tool: resolution.tool || null,
26
+ action: resolution.action || null,
27
+ target: resolution.target || null, // @handle, channel, etc.
28
+ transform: resolution.transform || null, // emoji, recap, etc.
29
+ };
30
+
31
+ try {
32
+ fs.appendFileSync(PROMPTS_FILE, JSON.stringify(entry) + '\n');
33
+ } catch (e) {
34
+ // Silent fail - logging is best-effort
35
+ }
36
+
37
+ return entry;
38
+ }
39
+
40
+ /**
41
+ * Get recent prompts for pattern analysis
42
+ */
43
+ function getRecent(limit = 50) {
44
+ try {
45
+ if (!fs.existsSync(PROMPTS_FILE)) return [];
46
+
47
+ const lines = fs.readFileSync(PROMPTS_FILE, 'utf8')
48
+ .trim()
49
+ .split('\n')
50
+ .filter(Boolean);
51
+
52
+ return lines
53
+ .slice(-limit)
54
+ .map(line => JSON.parse(line))
55
+ .reverse();
56
+ } catch (e) {
57
+ return [];
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Extract patterns from logged prompts
63
+ * Returns frequency map of normalized patterns
64
+ */
65
+ function extractPatterns() {
66
+ const prompts = getRecent(200);
67
+ const patterns = {};
68
+
69
+ for (const p of prompts) {
70
+ // Normalize: lowercase, replace @handles with @*, replace quoted strings with "*"
71
+ let normalized = p.prompt.toLowerCase()
72
+ .replace(/@\w+/g, '@*')
73
+ .replace(/"[^"]+"/g, '"*"')
74
+ .replace(/'[^']+'/g, "'*'")
75
+ .trim();
76
+
77
+ patterns[normalized] = (patterns[normalized] || 0) + 1;
78
+ }
79
+
80
+ // Sort by frequency
81
+ return Object.entries(patterns)
82
+ .sort((a, b) => b[1] - a[1])
83
+ .slice(0, 20)
84
+ .map(([pattern, count]) => ({ pattern, count }));
85
+ }
86
+
87
+ /**
88
+ * Suggest commands based on frequent patterns
89
+ */
90
+ function suggestConstructs() {
91
+ const patterns = extractPatterns();
92
+ const suggestions = [];
93
+
94
+ for (const { pattern, count } of patterns) {
95
+ if (count < 3) continue; // Need repetition to suggest
96
+
97
+ // Pattern matching for common constructs
98
+ if (pattern.includes('share') && pattern.includes('discord')) {
99
+ suggestions.push({ pattern, construct: 'vibe discord <content>', count });
100
+ }
101
+ if (pattern.includes('emoji') && pattern.includes('poem')) {
102
+ suggestions.push({ pattern, construct: 'vibe poem <content>', count });
103
+ }
104
+ if (pattern.includes('menu') || pattern.includes('options')) {
105
+ suggestions.push({ pattern, construct: 'vibe menu', count });
106
+ }
107
+ if (pattern.includes('recap') || pattern.includes('summary')) {
108
+ suggestions.push({ pattern, construct: 'vibe recap @handle', count });
109
+ }
110
+ if (pattern.includes('everyone') || pattern.includes('broadcast')) {
111
+ suggestions.push({ pattern, construct: 'vibe broadcast <message>', count });
112
+ }
113
+ }
114
+
115
+ return suggestions;
116
+ }
117
+
118
+ /**
119
+ * Anonymize and prepare for server upload
120
+ */
121
+ function getAnonymizedPatterns() {
122
+ const patterns = extractPatterns();
123
+
124
+ return patterns.map(({ pattern, count }) => ({
125
+ pattern,
126
+ frequency: count,
127
+ // Remove any potentially identifying info
128
+ normalized: pattern
129
+ .replace(/\d+/g, 'N')
130
+ .replace(/[a-f0-9]{8,}/gi, 'HASH')
131
+ }));
132
+ }
133
+
134
+ module.exports = {
135
+ log,
136
+ getRecent,
137
+ extractPatterns,
138
+ suggestConstructs,
139
+ getAnonymizedPatterns,
140
+ PROMPTS_FILE
141
+ };
@@ -0,0 +1,239 @@
1
+ /**
2
+ * vibe handoff โ€” Transfer task context to another agent
3
+ *
4
+ * AIRC Handoff v1: The atomic unit of agent work.
5
+ * Enables context portability and non-session-bound tasks.
6
+ */
7
+
8
+ const config = require('../config');
9
+ const store = require('../store');
10
+ const { requireInit, normalizeHandle, warning } = require('./_shared');
11
+ const { actions, formatActions } = require('./_actions');
12
+
13
+ // Handoff schema version
14
+ const HANDOFF_VERSION = '1.0';
15
+
16
+ const definition = {
17
+ name: 'vibe_handoff',
18
+ description: `Hand off a task to another agent with full context. Use when:
19
+ - You're stuck and need another agent to continue
20
+ - Shifting to a different domain/expertise
21
+ - Ending your session but work needs to continue
22
+ - Delegating a subtask
23
+
24
+ The receiving agent gets structured context to resume work immediately.`,
25
+ inputSchema: {
26
+ type: 'object',
27
+ properties: {
28
+ to: {
29
+ type: 'string',
30
+ description: 'Who to hand off to (e.g., @gene_agent)'
31
+ },
32
+ task: {
33
+ type: 'object',
34
+ description: 'The task being handed off',
35
+ properties: {
36
+ title: {
37
+ type: 'string',
38
+ description: 'Brief task title (e.g., "Fix auth token refresh bug")'
39
+ },
40
+ intent: {
41
+ type: 'string',
42
+ description: 'What you were trying to accomplish'
43
+ },
44
+ priority: {
45
+ type: 'string',
46
+ enum: ['low', 'medium', 'high', 'critical'],
47
+ description: 'Task priority'
48
+ }
49
+ },
50
+ required: ['title', 'intent']
51
+ },
52
+ context: {
53
+ type: 'object',
54
+ description: 'Technical context for the task',
55
+ properties: {
56
+ repo: {
57
+ type: 'string',
58
+ description: 'Git repository URL or identifier'
59
+ },
60
+ branch: {
61
+ type: 'string',
62
+ description: 'Current branch'
63
+ },
64
+ files: {
65
+ type: 'array',
66
+ items: {
67
+ type: 'object',
68
+ properties: {
69
+ path: { type: 'string' },
70
+ lines: { type: 'string', description: 'Line range (e.g., "138-155")' },
71
+ note: { type: 'string', description: 'What this file is about' }
72
+ }
73
+ },
74
+ description: 'Relevant files with notes'
75
+ },
76
+ current_state: {
77
+ type: 'string',
78
+ description: 'What has been done so far'
79
+ },
80
+ next_step: {
81
+ type: 'string',
82
+ description: 'The immediate next action needed'
83
+ },
84
+ blockers: {
85
+ type: 'array',
86
+ items: { type: 'string' },
87
+ description: 'Any blockers or open questions'
88
+ }
89
+ },
90
+ required: ['current_state', 'next_step']
91
+ },
92
+ history_summary: {
93
+ type: 'string',
94
+ description: 'Brief summary of investigation/work done (prevents context loss)'
95
+ }
96
+ },
97
+ required: ['to', 'task', 'context']
98
+ }
99
+ };
100
+
101
+ async function handler(args) {
102
+ const initCheck = requireInit();
103
+ if (initCheck) return initCheck;
104
+
105
+ const { to, task, context, history_summary } = args;
106
+ const myHandle = config.getHandle();
107
+ const them = normalizeHandle(to);
108
+
109
+ if (them === myHandle) {
110
+ return { display: 'Cannot hand off to yourself.' };
111
+ }
112
+
113
+ // Validate required fields
114
+ if (!task?.title || !task?.intent) {
115
+ return { display: 'Task requires title and intent.' };
116
+ }
117
+ if (!context?.current_state || !context?.next_step) {
118
+ return { display: 'Context requires current_state and next_step.' };
119
+ }
120
+
121
+ // Generate handoff ID
122
+ const handoff_id = `handoff_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
123
+
124
+ // Build the handoff payload (AIRC Handoff v1 schema)
125
+ const handoffPayload = {
126
+ type: 'handoff',
127
+ version: HANDOFF_VERSION,
128
+ handoff_id,
129
+ timestamp: new Date().toISOString(),
130
+
131
+ task: {
132
+ title: task.title,
133
+ intent: task.intent,
134
+ priority: task.priority || 'medium'
135
+ },
136
+
137
+ context: {
138
+ repo: context.repo || null,
139
+ branch: context.branch || null,
140
+ files: context.files || [],
141
+ current_state: context.current_state,
142
+ next_step: context.next_step,
143
+ blockers: context.blockers || [],
144
+ },
145
+
146
+ history: {
147
+ summary: history_summary || 'No history provided'
148
+ }
149
+ };
150
+
151
+ // Build human-readable message for the recipient
152
+ const humanMessage = formatHandoffMessage(handoffPayload, myHandle);
153
+
154
+ // Send via existing message system with structured payload
155
+ await store.sendMessage(
156
+ myHandle,
157
+ them,
158
+ humanMessage,
159
+ 'handoff',
160
+ handoffPayload
161
+ );
162
+
163
+ // Build response
164
+ const filesCount = context.files?.length || 0;
165
+ let display = `**Handed off to @${them}**\n\n`;
166
+ display += `Task: ${task.title}\n`;
167
+ display += `Priority: ${task.priority || 'medium'}\n`;
168
+ if (context.repo) display += `Repo: ${context.repo}\n`;
169
+ if (filesCount > 0) display += `Files: ${filesCount} tracked\n`;
170
+ display += `\nNext step for @${them}:\n> ${context.next_step}`;
171
+
172
+ if (context.blockers?.length > 0) {
173
+ display += `\n\nBlockers:\n`;
174
+ context.blockers.forEach(b => {
175
+ display += `- ${b}\n`;
176
+ });
177
+ }
178
+
179
+ display += `\n\n_Handoff ID: ${handoff_id}_`;
180
+
181
+ return {
182
+ display,
183
+ hint: 'handoff_sent',
184
+ handoff_id,
185
+ to: them,
186
+ actions: formatActions([
187
+ { label: 'check inbox', command: 'vibe inbox' },
188
+ { label: 'end session', command: 'vibe bye' }
189
+ ])
190
+ };
191
+ }
192
+
193
+ /**
194
+ * Format handoff as human-readable message
195
+ */
196
+ function formatHandoffMessage(payload, from) {
197
+ const { task, context, history } = payload;
198
+
199
+ let msg = `HANDOFF from @${from}\n\n`;
200
+ msg += `TASK: ${task.title}\n`;
201
+ msg += `PRIORITY: ${task.priority}\n`;
202
+ msg += `INTENT: ${task.intent}\n\n`;
203
+
204
+ if (context.repo) {
205
+ msg += `REPO: ${context.repo}`;
206
+ if (context.branch) msg += ` (${context.branch})`;
207
+ msg += '\n';
208
+ }
209
+
210
+ if (context.files?.length > 0) {
211
+ msg += '\nFILES:\n';
212
+ context.files.forEach(f => {
213
+ msg += `- ${f.path}`;
214
+ if (f.lines) msg += ` [${f.lines}]`;
215
+ if (f.note) msg += `: ${f.note}`;
216
+ msg += '\n';
217
+ });
218
+ }
219
+
220
+ msg += `\nCURRENT STATE:\n${context.current_state}\n`;
221
+ msg += `\nNEXT STEP:\n${context.next_step}\n`;
222
+
223
+ if (context.blockers?.length > 0) {
224
+ msg += '\nBLOCKERS:\n';
225
+ context.blockers.forEach(b => {
226
+ msg += `- ${b}\n`;
227
+ });
228
+ }
229
+
230
+ if (history.summary && history.summary !== 'No history provided') {
231
+ msg += `\nHISTORY:\n${history.summary}\n`;
232
+ }
233
+
234
+ msg += '\n---\nReply to accept and continue this work.';
235
+
236
+ return msg;
237
+ }
238
+
239
+ module.exports = { definition, handler };
@@ -0,0 +1,79 @@
1
+ /**
2
+ * vibe patterns โ€” View emerging language constructs
3
+ *
4
+ * Shows frequent prompt patterns and suggests new commands.
5
+ */
6
+
7
+ const prompts = require('../prompts');
8
+
9
+ const definition = {
10
+ name: 'vibe_patterns',
11
+ description: 'View emerging language patterns from how people use /vibe. Shows frequent prompts and suggests new commands.',
12
+ inputSchema: {
13
+ type: 'object',
14
+ properties: {
15
+ limit: {
16
+ type: 'number',
17
+ description: 'Number of recent prompts to analyze (default: 50)'
18
+ },
19
+ raw: {
20
+ type: 'boolean',
21
+ description: 'Show raw prompts instead of patterns'
22
+ }
23
+ }
24
+ }
25
+ };
26
+
27
+ async function handler({ limit = 50, raw = false }) {
28
+ if (raw) {
29
+ // Show raw recent prompts
30
+ const recent = prompts.getRecent(limit);
31
+
32
+ if (recent.length === 0) {
33
+ return {
34
+ display: `## Prompt Log\n\n_No prompts logged yet. Use /vibe and patterns will emerge._\n\nFile: \`${prompts.PROMPTS_FILE}\``
35
+ };
36
+ }
37
+
38
+ let display = `## Recent Prompts (${recent.length})\n\n`;
39
+ display += '| Time | Prompt | Tool |\n';
40
+ display += '|------|--------|------|\n';
41
+
42
+ for (const p of recent.slice(0, 20)) {
43
+ const time = new Date(p.ts).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
44
+ const prompt = p.prompt.slice(0, 40) + (p.prompt.length > 40 ? '...' : '');
45
+ display += `| ${time} | ${prompt} | ${p.tool || '-'} |\n`;
46
+ }
47
+
48
+ display += `\n---\n_${recent.length} total logged ยท ${prompts.PROMPTS_FILE}_`;
49
+ return { display };
50
+ }
51
+
52
+ // Show patterns and suggestions
53
+ const patterns = prompts.extractPatterns();
54
+ const suggestions = prompts.suggestConstructs();
55
+
56
+ let display = '## Emerging Patterns\n\n';
57
+
58
+ if (patterns.length === 0) {
59
+ display += '_Not enough data yet. Keep using /vibe and patterns will emerge._\n';
60
+ } else {
61
+ display += '**Frequent patterns:**\n';
62
+ for (const { pattern, count } of patterns.slice(0, 10)) {
63
+ display += `- \`${pattern}\` (${count}x)\n`;
64
+ }
65
+ }
66
+
67
+ if (suggestions.length > 0) {
68
+ display += '\n**Suggested constructs:**\n';
69
+ for (const { pattern, construct, count } of suggestions) {
70
+ display += `- \`${construct}\` โ† from "${pattern}" (${count}x)\n`;
71
+ }
72
+ }
73
+
74
+ display += `\n---\n_Run \`vibe patterns --raw\` to see individual prompts_`;
75
+
76
+ return { display };
77
+ }
78
+
79
+ module.exports = { definition, handler };
@@ -0,0 +1,180 @@
1
+ /**
2
+ * vibe social-inbox โ€” Unified social inbox across all channels
3
+ *
4
+ * Reads from the local cache (sync-then-read pattern) for instant access.
5
+ * Use --refresh to trigger a sync.
6
+ */
7
+
8
+ const { requireInit, header, divider, formatTimeAgo } = require('./_shared');
9
+
10
+ const API_URL = process.env.VIBE_API_URL || 'https://slashvibe.dev';
11
+
12
+ const definition = {
13
+ name: 'vibe_social_inbox',
14
+ description: 'See messages across all connected social channels (X, Farcaster, etc.)',
15
+ inputSchema: {
16
+ type: 'object',
17
+ properties: {
18
+ channel: {
19
+ type: 'string',
20
+ enum: ['all', 'x', 'farcaster', 'discord', 'telegram', 'whatsapp', 'email'],
21
+ description: 'Filter by channel (default: all)'
22
+ },
23
+ high_signal: {
24
+ type: 'boolean',
25
+ description: 'Show only high-signal messages like mentions/DMs (default: true)'
26
+ },
27
+ limit: {
28
+ type: 'number',
29
+ description: 'Number of messages to show (default: 20, max: 100)'
30
+ },
31
+ refresh: {
32
+ type: 'boolean',
33
+ description: 'Force sync from external APIs (default: false)'
34
+ },
35
+ status: {
36
+ type: 'boolean',
37
+ description: 'Show channel connection status (default: false)'
38
+ }
39
+ }
40
+ }
41
+ };
42
+
43
+ async function handler(args) {
44
+ const initCheck = requireInit();
45
+ if (initCheck) return initCheck;
46
+
47
+ const {
48
+ channel = 'all',
49
+ high_signal = true,
50
+ limit = 20,
51
+ refresh = false,
52
+ status = false
53
+ } = args;
54
+
55
+ try {
56
+ // Build query params
57
+ const params = new URLSearchParams();
58
+ if (channel !== 'all') params.set('channel', channel);
59
+ if (!high_signal) params.set('high_signal', 'false');
60
+ if (limit) params.set('limit', limit.toString());
61
+ if (refresh) params.set('refresh', 'true');
62
+ if (status) params.set('status', 'true');
63
+
64
+ const url = `${API_URL}/api/social?${params}`;
65
+
66
+ const response = await fetch(url);
67
+ const data = await response.json();
68
+
69
+ if (!data.success) {
70
+ return { display: `${header('Social Inbox')}\n\n_Error:_ ${data.error}` };
71
+ }
72
+
73
+ // Status view
74
+ if (status) {
75
+ let display = header('Channel Status');
76
+ display += '\n\n';
77
+
78
+ for (const [ch, info] of Object.entries(data.channels || {})) {
79
+ const icon = info.status?.status === 'connected' ? 'โœ…' : 'โŒ';
80
+ const configured = info.configured ? 'configured' : 'not configured';
81
+ display += `${icon} **${ch}** โ€” ${configured}\n`;
82
+
83
+ if (info.status?.error) {
84
+ display += ` _${info.status.error}_\n`;
85
+ }
86
+
87
+ if (info.capabilities) {
88
+ const caps = [];
89
+ if (info.capabilities.read) caps.push('read');
90
+ if (info.capabilities.write) caps.push('write');
91
+ if (info.capabilities.dm) caps.push('dm');
92
+ display += ` Capabilities: ${caps.join(', ')}\n`;
93
+ }
94
+ display += '\n';
95
+ }
96
+
97
+ return { display };
98
+ }
99
+
100
+ // Inbox view
101
+ const messages = data.messages || [];
102
+
103
+ if (messages.length === 0) {
104
+ let display = header('Social Inbox');
105
+ display += '\n\n';
106
+
107
+ if (data.summary?.total === 0) {
108
+ display += '_No messages synced yet._\n\n';
109
+ display += 'Run `vibe social-inbox --status` to check channel connections.\n';
110
+ display += 'Run `vibe social-inbox --refresh` to trigger a sync.';
111
+ } else {
112
+ display += `_No ${channel === 'all' ? '' : channel + ' '}messages found._`;
113
+ }
114
+
115
+ return { display };
116
+ }
117
+
118
+ // Format messages
119
+ let display = header(`Social Inbox (${messages.length})`);
120
+ display += '\n\n';
121
+
122
+ // Group by channel for summary
123
+ const byChannel = {};
124
+ for (const msg of messages) {
125
+ byChannel[msg.channel] = (byChannel[msg.channel] || 0) + 1;
126
+ }
127
+
128
+ const channelSummary = Object.entries(byChannel)
129
+ .map(([ch, count]) => `${ch}: ${count}`)
130
+ .join(' | ');
131
+ display += `๐Ÿ“ฌ ${channelSummary}\n`;
132
+ display += divider();
133
+ display += '\n';
134
+
135
+ // Show messages
136
+ for (const msg of messages) {
137
+ const channelIcon = getChannelIcon(msg.channel);
138
+ const typeIcon = getTypeIcon(msg.type);
139
+
140
+ display += `${channelIcon} **@${msg.from}** ${typeIcon} โ€” _${msg.timeAgo}_\n`;
141
+ display += `${msg.content}\n`;
142
+ display += `_[${msg.channel}:${msg.id.split(':')[1]?.slice(0, 8)}]_\n\n`;
143
+ }
144
+
145
+ display += divider();
146
+ display += 'Reply: `vibe post "message" --x --farcaster`';
147
+
148
+ return { display };
149
+
150
+ } catch (e) {
151
+ return {
152
+ display: `${header('Social Inbox')}\n\n_Error:_ ${e.message}`
153
+ };
154
+ }
155
+ }
156
+
157
+ function getChannelIcon(channel) {
158
+ const icons = {
159
+ x: '๐•',
160
+ farcaster: '๐ŸŸฃ',
161
+ discord: '๐Ÿ’ฌ',
162
+ telegram: 'โœˆ๏ธ',
163
+ whatsapp: '๐Ÿ’š',
164
+ email: '๐Ÿ“ง'
165
+ };
166
+ return icons[channel] || '๐Ÿ“ฑ';
167
+ }
168
+
169
+ function getTypeIcon(type) {
170
+ const icons = {
171
+ mention: '@',
172
+ reply: 'โ†ฉ๏ธ',
173
+ dm: 'โœ‰๏ธ',
174
+ like: 'โค๏ธ',
175
+ repost: '๐Ÿ”'
176
+ };
177
+ return icons[type] || '';
178
+ }
179
+
180
+ module.exports = { definition, handler };
@@ -0,0 +1,141 @@
1
+ /**
2
+ * vibe social-post โ€” Post to multiple social channels at once
3
+ *
4
+ * Multi-cast posting with dry-run preview support.
5
+ */
6
+
7
+ const { requireInit, header, divider, warning } = require('./_shared');
8
+
9
+ const API_URL = process.env.VIBE_API_URL || 'https://slashvibe.dev';
10
+
11
+ const definition = {
12
+ name: 'vibe_social_post',
13
+ description: 'Post content to one or more social channels (X, Farcaster, etc.)',
14
+ inputSchema: {
15
+ type: 'object',
16
+ properties: {
17
+ content: {
18
+ type: 'string',
19
+ description: 'The content to post'
20
+ },
21
+ channels: {
22
+ type: 'array',
23
+ items: { type: 'string' },
24
+ description: 'Channels to post to (e.g., ["x", "farcaster"])'
25
+ },
26
+ dry_run: {
27
+ type: 'boolean',
28
+ description: 'Preview post without sending (default: false)'
29
+ },
30
+ reply_to: {
31
+ type: 'string',
32
+ description: 'Message ID to reply to (e.g., "x:1234567890")'
33
+ }
34
+ },
35
+ required: ['content', 'channels']
36
+ }
37
+ };
38
+
39
+ async function handler(args) {
40
+ const initCheck = requireInit();
41
+ if (initCheck) return initCheck;
42
+
43
+ const { content, channels, dry_run, reply_to } = args;
44
+
45
+ // Validation
46
+ if (!content || typeof content !== 'string' || content.trim().length === 0) {
47
+ return { display: 'Need content to post.' };
48
+ }
49
+
50
+ if (!channels || !Array.isArray(channels) || channels.length === 0) {
51
+ return { display: 'Need at least one channel. Options: x, farcaster' };
52
+ }
53
+
54
+ const trimmed = content.trim();
55
+
56
+ // Character limit warnings
57
+ const warnings = [];
58
+ if (channels.includes('x') && trimmed.length > 280) {
59
+ warnings.push(`X: Content is ${trimmed.length} chars (max 280). Will be truncated.`);
60
+ }
61
+
62
+ try {
63
+ const response = await fetch(`${API_URL}/api/social`, {
64
+ method: 'POST',
65
+ headers: { 'Content-Type': 'application/json' },
66
+ body: JSON.stringify({
67
+ content: trimmed,
68
+ channels,
69
+ dry_run: dry_run || false,
70
+ reply_to
71
+ })
72
+ });
73
+
74
+ const data = await response.json();
75
+
76
+ if (!data.success && !data.dry_run) {
77
+ return { display: `${header('Post Failed')}\n\n_Error:_ ${data.error}` };
78
+ }
79
+
80
+ // Dry run preview
81
+ if (data.dry_run) {
82
+ let display = header('Post Preview (Dry Run)');
83
+ display += '\n\n';
84
+
85
+ for (const [ch, preview] of Object.entries(data.previews || {})) {
86
+ const icon = preview.configured ? 'โœ…' : 'โŒ';
87
+ const canPost = preview.canWrite ? 'can post' : 'read-only';
88
+
89
+ display += `${icon} **${ch}** โ€” ${canPost}\n`;
90
+
91
+ if (!preview.configured) {
92
+ display += ` _Not configured_\n`;
93
+ } else if (preview.wouldTruncate) {
94
+ display += ` โš ๏ธ Content will be truncated to 280 chars\n`;
95
+ }
96
+
97
+ display += ` "${preview.content.slice(0, 100)}${preview.content.length > 100 ? '...' : ''}"\n\n`;
98
+ }
99
+
100
+ display += divider();
101
+ display += 'Remove `--dry_run` to post for real.';
102
+
103
+ return { display };
104
+ }
105
+
106
+ // Actual post results
107
+ let display = header('Posted');
108
+ display += '\n\n';
109
+
110
+ if (warnings.length > 0) {
111
+ display += warning(warnings.join('\n')) + '\n\n';
112
+ }
113
+
114
+ let anySuccess = false;
115
+ for (const [ch, result] of Object.entries(data.results || {})) {
116
+ if (result.success) {
117
+ anySuccess = true;
118
+ display += `โœ… **${ch}** โ€” Posted!\n`;
119
+ if (result.url) {
120
+ display += ` ๐Ÿ”— ${result.url}\n`;
121
+ }
122
+ } else {
123
+ display += `โŒ **${ch}** โ€” Failed: ${result.error}\n`;
124
+ }
125
+ display += '\n';
126
+ }
127
+
128
+ if (!anySuccess) {
129
+ display += '\n_No posts succeeded. Check channel configuration._';
130
+ }
131
+
132
+ return { display };
133
+
134
+ } catch (e) {
135
+ return {
136
+ display: `${header('Post Error')}\n\n_Error:_ ${e.message}`
137
+ };
138
+ }
139
+ }
140
+
141
+ module.exports = { definition, handler };