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 +19 -0
- package/index.js +56 -2
- package/package.json +3 -1
- package/prompts.js +141 -0
- package/tools/handoff.js +239 -0
- package/tools/patterns.js +79 -0
- package/tools/social-inbox.js +180 -0
- package/tools/social-post.js +141 -0
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
|
-
|
|
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.
|
|
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
|
+
};
|
package/tools/handoff.js
ADDED
|
@@ -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 };
|