slashvibe-mcp 0.2.1 → 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/index.js +3 -0
- package/package.json +2 -1
- package/prompts.js +141 -0
- package/tools/handoff.js +239 -0
package/index.js
CHANGED
|
@@ -43,6 +43,7 @@ function inferPromptFromArgs(toolName, args) {
|
|
|
43
43
|
case 'echo': return 'send feedback';
|
|
44
44
|
case 'x_mentions': return 'check x mentions';
|
|
45
45
|
case 'x_reply': return 'reply on x';
|
|
46
|
+
case 'handoff': return `handoff task to ${handle}`;
|
|
46
47
|
default: return `${action} ${handle}`.trim() || null;
|
|
47
48
|
}
|
|
48
49
|
}
|
|
@@ -161,6 +162,8 @@ const tools = {
|
|
|
161
162
|
vibe_summarize: require('./tools/summarize'),
|
|
162
163
|
vibe_bye: require('./tools/bye'),
|
|
163
164
|
vibe_game: require('./tools/game'),
|
|
165
|
+
// AIRC Handoff (v1) — context portability
|
|
166
|
+
vibe_handoff: require('./tools/handoff'),
|
|
164
167
|
// Memory tools (Tier 1 — Collaborative Memory)
|
|
165
168
|
vibe_remember: require('./tools/remember'),
|
|
166
169
|
vibe_recall: require('./tools/recall'),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slashvibe-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"mcpName": "io.github.brightseth/vibe",
|
|
5
5
|
"description": "Social layer for Claude Code - DMs, presence, and connection between AI-assisted developers",
|
|
6
6
|
"main": "index.js",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"memory.js",
|
|
41
41
|
"notify.js",
|
|
42
42
|
"presence.js",
|
|
43
|
+
"prompts.js",
|
|
43
44
|
"twitter.js",
|
|
44
45
|
"version.json",
|
|
45
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 };
|