slashvibe-mcp 0.3.20 → 0.3.21
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/README.md +47 -252
- package/analytics.js +107 -0
- package/auth-store.js +148 -0
- package/auto-update.js +130 -0
- package/bridges/bridge-monitor.js +388 -0
- package/bridges/discord-bot.js +431 -0
- package/bridges/farcaster.js +299 -0
- package/bridges/telegram.js +261 -0
- package/bridges/webhook-health.js +420 -0
- package/bridges/webhook-server.js +437 -0
- package/bridges/whatsapp.js +441 -0
- package/bridges/x-webhook.js +423 -0
- package/config.js +27 -15
- package/games/arcade.js +406 -0
- package/games/chess.js +451 -0
- package/games/colorguess.js +343 -0
- package/games/crossword-words.js +171 -0
- package/games/crossword.js +461 -0
- package/games/drawing.js +347 -0
- package/games/gameroulette.js +300 -0
- package/games/gamerouter.js +336 -0
- package/games/gamestatus.js +337 -0
- package/games/guessnumber.js +209 -0
- package/games/hangman.js +279 -0
- package/games/memory.js +338 -0
- package/games/multiplayer-tictactoe.js +389 -0
- package/games/pixelart.js +399 -0
- package/games/quickduel.js +354 -0
- package/games/riddle.js +371 -0
- package/games/rockpaperscissors.js +291 -0
- package/games/snake.js +406 -0
- package/games/storybuilder.js +343 -0
- package/games/tictactoe.js +345 -0
- package/games/twentyquestions.js +286 -0
- package/games/twotruths.js +207 -0
- package/games/werewolf.js +508 -0
- package/games/wordassociation.js +247 -0
- package/games/wordchain.js +135 -0
- package/index.js +116 -159
- package/intelligence/index.js +9 -2
- package/intelligence/interests.js +369 -0
- package/notification-emitter.js +77 -0
- package/notify.js +5 -1
- package/package.json +21 -16
- package/prompts.js +1 -1
- package/protocol/index.js +73 -0
- package/setup.js +480 -0
- package/smart-inbox.js +276 -0
- package/store/api.js +536 -215
- package/store/profiles.js +160 -12
- package/tools/_actions.js +362 -21
- package/tools/_discovery.js +119 -26
- package/tools/_shared/index.js +64 -0
- package/tools/_shared.js +234 -0
- package/tools/_work-context.js +338 -0
- package/tools/_work-context.manual-test.js +199 -0
- package/tools/_work-context.test.js +260 -0
- package/tools/activity.js +220 -0
- package/tools/analytics.js +191 -0
- package/tools/approve.js +197 -0
- package/tools/artifact-create.js +14 -3
- package/tools/artifacts-price.js +107 -0
- package/tools/available.js +120 -0
- package/tools/broadcast.js +325 -0
- package/tools/chat.js +202 -0
- package/tools/collaborative-drawing.js +1 -1
- package/tools/connection-status.js +178 -0
- package/tools/discover.js +350 -34
- package/tools/dm.js +80 -8
- package/tools/earnings.js +126 -0
- package/tools/feed.js +35 -4
- package/tools/follow.js +224 -0
- package/tools/friends.js +207 -0
- package/tools/gig-browse.js +206 -0
- package/tools/gig-complete.js +144 -0
- package/tools/health.js +87 -0
- package/tools/help.js +3 -3
- package/tools/idea.js +9 -2
- package/tools/inbox.js +289 -105
- package/tools/init.js +131 -34
- package/tools/invite.js +15 -4
- package/tools/leaderboard.js +117 -0
- package/tools/lib/git-apply.js +206 -0
- package/tools/lib/git-bundle.js +407 -0
- package/tools/migrate.js +3 -3
- package/tools/multiplayer-game.js +1 -1
- package/tools/onboarding.js +7 -7
- package/tools/open.js +143 -12
- package/tools/party-game.js +1 -1
- package/tools/plan.js +225 -0
- package/tools/proof-of-work.js +144 -0
- package/tools/reply.js +166 -0
- package/tools/report.js +1 -1
- package/tools/request.js +17 -3
- package/tools/schedule.js +367 -0
- package/tools/search-messages.js +123 -0
- package/tools/session.js +467 -0
- package/tools/session_price.js +128 -0
- package/tools/settings.js +90 -2
- package/tools/ship.js +30 -7
- package/tools/smart-check.js +201 -0
- package/tools/start.js +147 -12
- package/tools/status.js +53 -6
- package/tools/streak.js +147 -0
- package/tools/stuck.js +297 -0
- package/tools/subscribe.js +148 -0
- package/tools/subscriptions.js +134 -0
- package/tools/suggest-tags.js +6 -8
- package/tools/tag-suggestions.js +1 -1
- package/tools/tip.js +150 -77
- package/tools/token.js +4 -4
- package/tools/update.js +1 -1
- package/tools/wallet.js +221 -79
- package/tools/watch.js +157 -0
- package/tools/who.js +30 -1
- package/tools/withdraw.js +145 -0
- package/tools/work-summary.js +96 -0
- package/version.json +10 -8
- package/LICENSE +0 -21
- package/store/sqlite.js +0 -347
- /package/tools/{auto-suggest-connections.js → _deprecated/auto-suggest-connections.js} +0 -0
- /package/tools/{away.js → _deprecated/away.js} +0 -0
- /package/tools/{back.js → _deprecated/back.js} +0 -0
- /package/tools/{bootstrap-skills.js → _deprecated/bootstrap-skills.js} +0 -0
- /package/tools/{bridge-dashboard.js → _deprecated/bridge-dashboard.js} +0 -0
- /package/tools/{bridge-health.js → _deprecated/bridge-health.js} +0 -0
- /package/tools/{bridge-live.js → _deprecated/bridge-live.js} +0 -0
- /package/tools/{bridges.js → _deprecated/bridges.js} +0 -0
- /package/tools/{colorguess.js → _deprecated/colorguess.js} +0 -0
- /package/tools/{discover-insights.js → _deprecated/discover-insights.js} +0 -0
- /package/tools/{discover-momentum.js → _deprecated/discover-momentum.js} +0 -0
- /package/tools/{discovery-analytics.js → _deprecated/discovery-analytics.js} +0 -0
- /package/tools/{discovery-auto-suggest.js → _deprecated/discovery-auto-suggest.js} +0 -0
- /package/tools/{discovery-bootstrap.js → _deprecated/discovery-bootstrap.js} +0 -0
- /package/tools/{discovery-daily.js → _deprecated/discovery-daily.js} +0 -0
- /package/tools/{discovery-dashboard.js → _deprecated/discovery-dashboard.js} +0 -0
- /package/tools/{discovery-digest.js → _deprecated/discovery-digest.js} +0 -0
- /package/tools/{discovery-hub.js → _deprecated/discovery-hub.js} +0 -0
- /package/tools/{discovery-insights.js → _deprecated/discovery-insights.js} +0 -0
- /package/tools/{discovery-momentum.js → _deprecated/discovery-momentum.js} +0 -0
- /package/tools/{discovery-monitor.js → _deprecated/discovery-monitor.js} +0 -0
- /package/tools/{discovery-proactive.js → _deprecated/discovery-proactive.js} +0 -0
- /package/tools/{draw.js → _deprecated/draw.js} +0 -0
- /package/tools/{farcaster.js → _deprecated/farcaster.js} +0 -0
- /package/tools/{forget.js → _deprecated/forget.js} +0 -0
- /package/tools/{games-catalog.js → _deprecated/games-catalog.js} +0 -0
- /package/tools/{games.js → _deprecated/games.js} +0 -0
- /package/tools/{guessnumber.js → _deprecated/guessnumber.js} +0 -0
- /package/tools/{hangman.js → _deprecated/hangman.js} +0 -0
- /package/tools/{multiplayer-tictactoe.js → _deprecated/multiplayer-tictactoe.js} +0 -0
- /package/tools/{mute.js → _deprecated/mute.js} +0 -0
- /package/tools/{recall.js → _deprecated/recall.js} +0 -0
- /package/tools/{remember.js → _deprecated/remember.js} +0 -0
- /package/tools/{riddle.js → _deprecated/riddle.js} +0 -0
- /package/tools/{run-bootstrap.js → _deprecated/run-bootstrap.js} +0 -0
- /package/tools/{skills-analytics.js → _deprecated/skills-analytics.js} +0 -0
- /package/tools/{skills-bootstrap.js → _deprecated/skills-bootstrap.js} +0 -0
- /package/tools/{skills-dashboard.js → _deprecated/skills-dashboard.js} +0 -0
- /package/tools/{skills-exchange.js → _deprecated/skills-exchange.js} +0 -0
- /package/tools/{skills.js → _deprecated/skills.js} +0 -0
- /package/tools/{smart-intro.js → _deprecated/smart-intro.js} +0 -0
- /package/tools/{storybuilder.js → _deprecated/storybuilder.js} +0 -0
- /package/tools/{telegram-bot.js → _deprecated/telegram-bot.js} +0 -0
- /package/tools/{telegram-setup.js → _deprecated/telegram-setup.js} +0 -0
- /package/tools/{tictactoe.js → _deprecated/tictactoe.js} +0 -0
- /package/tools/{twentyquestions.js → _deprecated/twentyquestions.js} +0 -0
- /package/tools/{wordassociation.js → _deprecated/wordassociation.js} +0 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for _work-context.js
|
|
3
|
+
*
|
|
4
|
+
* Run with: node --test tools/_work-context.test.js
|
|
5
|
+
* Or: node tools/_work-context.test.js (for quick inline tests)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { test, describe } = require('node:test');
|
|
9
|
+
const assert = require('node:assert');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
const {
|
|
13
|
+
gatherWorkContext,
|
|
14
|
+
gatherWithTimeout,
|
|
15
|
+
getGitInfo,
|
|
16
|
+
getProjectInfo,
|
|
17
|
+
sanitize,
|
|
18
|
+
redact,
|
|
19
|
+
cap
|
|
20
|
+
} = require('./_work-context');
|
|
21
|
+
|
|
22
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
23
|
+
// UNIT TESTS: Utility functions
|
|
24
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
25
|
+
|
|
26
|
+
describe('sanitize()', () => {
|
|
27
|
+
test('removes ANSI escape sequences', () => {
|
|
28
|
+
const input = '\x1B[31mRed text\x1B[0m';
|
|
29
|
+
assert.strictEqual(sanitize(input), 'Red text');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('removes control characters but keeps newlines/tabs', () => {
|
|
33
|
+
const input = 'Hello\x00World\tTab\nNewline';
|
|
34
|
+
assert.strictEqual(sanitize(input), 'HelloWorld\tTab\nNewline');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('handles null/undefined gracefully', () => {
|
|
38
|
+
assert.strictEqual(sanitize(null), null);
|
|
39
|
+
assert.strictEqual(sanitize(undefined), undefined);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('trims whitespace', () => {
|
|
43
|
+
assert.strictEqual(sanitize(' hello '), 'hello');
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('cap()', () => {
|
|
48
|
+
test('returns original if under limit', () => {
|
|
49
|
+
assert.strictEqual(cap('hello', 10), 'hello');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('truncates with ellipsis if over limit', () => {
|
|
53
|
+
assert.strictEqual(cap('hello world', 8), 'hello w…');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('handles exact length', () => {
|
|
57
|
+
assert.strictEqual(cap('hello', 5), 'hello');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('handles null/undefined', () => {
|
|
61
|
+
assert.strictEqual(cap(null, 10), null);
|
|
62
|
+
assert.strictEqual(cap(undefined, 10), undefined);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('redact()', () => {
|
|
67
|
+
test('redacts email addresses', () => {
|
|
68
|
+
const input = 'Contact me at user@example.com';
|
|
69
|
+
assert.strictEqual(redact(input), 'Contact me at [REDACTED]');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('redacts long hex strings (tokens)', () => {
|
|
73
|
+
const input = 'Value: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6';
|
|
74
|
+
assert.strictEqual(redact(input), 'Value: [REDACTED]');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('redacts OpenAI API keys', () => {
|
|
78
|
+
const input = 'Key: sk-1234567890abcdefghij1234567890';
|
|
79
|
+
assert.strictEqual(redact(input), 'Key: [REDACTED]');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('redacts GitHub tokens', () => {
|
|
83
|
+
const input = 'ghp_abcdefghijklmnopqrstuvwxyz1234567890';
|
|
84
|
+
assert.strictEqual(redact(input), '[REDACTED]');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('redacts sensitive words', () => {
|
|
88
|
+
const words = ['password', 'secret', 'token', 'api_key'];
|
|
89
|
+
words.forEach(word => {
|
|
90
|
+
assert.ok(redact(`My ${word} is hidden`).includes('[REDACTED]'));
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('handles null/undefined', () => {
|
|
95
|
+
assert.strictEqual(redact(null), null);
|
|
96
|
+
assert.strictEqual(redact(undefined), undefined);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
101
|
+
// INTEGRATION TESTS: Git info gathering
|
|
102
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
103
|
+
|
|
104
|
+
describe('getGitInfo()', () => {
|
|
105
|
+
test('returns object with expected shape when in git repo', () => {
|
|
106
|
+
const info = getGitInfo();
|
|
107
|
+
|
|
108
|
+
// We're in the vibe-platform repo, so git should work
|
|
109
|
+
if (info) {
|
|
110
|
+
assert.ok(typeof info.branch === 'string', 'branch should be a string');
|
|
111
|
+
assert.ok(Array.isArray(info.recentCommits), 'recentCommits should be an array');
|
|
112
|
+
assert.ok(Array.isArray(info.changedFiles), 'changedFiles should be an array');
|
|
113
|
+
assert.ok(typeof info.hasUncommitted === 'boolean', 'hasUncommitted should be boolean');
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('commit messages are capped and redacted', () => {
|
|
118
|
+
const info = getGitInfo();
|
|
119
|
+
if (info && info.recentCommits.length > 0) {
|
|
120
|
+
const firstCommit = info.recentCommits[0];
|
|
121
|
+
assert.ok(firstCommit.message.length <= 80, 'commit message should be <= 80 chars');
|
|
122
|
+
assert.ok(firstCommit.hash.length <= 7, 'commit hash should be <= 7 chars');
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('changed files use basename only (no full paths)', () => {
|
|
127
|
+
const info = getGitInfo();
|
|
128
|
+
if (info && info.changedFiles.length > 0) {
|
|
129
|
+
info.changedFiles.forEach(file => {
|
|
130
|
+
assert.ok(!file.includes('/'), `file "${file}" should not contain path separators`);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
137
|
+
// INTEGRATION TESTS: Project info gathering
|
|
138
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
139
|
+
|
|
140
|
+
describe('getProjectInfo()', () => {
|
|
141
|
+
test('returns object with expected shape', () => {
|
|
142
|
+
const info = getProjectInfo();
|
|
143
|
+
|
|
144
|
+
assert.ok(typeof info.name === 'string', 'name should be a string');
|
|
145
|
+
assert.ok(typeof info.type === 'string', 'type should be a string');
|
|
146
|
+
assert.ok(typeof info.directory === 'string', 'directory should be a string');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('detects project name from package.json', () => {
|
|
150
|
+
const info = getProjectInfo();
|
|
151
|
+
// We're in mcp-server which has a package.json
|
|
152
|
+
assert.strictEqual(info.name, 'slashvibe-mcp');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('detects project type', () => {
|
|
156
|
+
const info = getProjectInfo();
|
|
157
|
+
// This is a Node.js project
|
|
158
|
+
assert.ok(['node', 'typescript', 'react', 'nextjs'].includes(info.type));
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
163
|
+
// INTEGRATION TESTS: Full context gathering
|
|
164
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
165
|
+
|
|
166
|
+
describe('gatherWorkContext()', () => {
|
|
167
|
+
test('returns complete context object', () => {
|
|
168
|
+
const ctx = gatherWorkContext();
|
|
169
|
+
|
|
170
|
+
assert.ok(ctx.git !== undefined, 'should have git property');
|
|
171
|
+
assert.ok(ctx.project !== undefined, 'should have project property');
|
|
172
|
+
assert.ok(ctx.suggestions !== undefined, 'should have suggestions property');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('suggestions.brief is reasonably short', () => {
|
|
176
|
+
const ctx = gatherWorkContext();
|
|
177
|
+
assert.ok(ctx.suggestions.brief.length <= 200, 'brief should be <= 200 chars');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('suggestions.detailed exists', () => {
|
|
181
|
+
const ctx = gatherWorkContext();
|
|
182
|
+
assert.ok(typeof ctx.suggestions.detailed === 'string');
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe('gatherWithTimeout()', () => {
|
|
187
|
+
test('resolves within timeout', async () => {
|
|
188
|
+
const start = Date.now();
|
|
189
|
+
const ctx = await gatherWithTimeout(5000);
|
|
190
|
+
const elapsed = Date.now() - start;
|
|
191
|
+
|
|
192
|
+
assert.ok(elapsed < 5000, 'should resolve before timeout');
|
|
193
|
+
assert.ok(ctx.suggestions !== undefined, 'should have suggestions');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('provides fallback on very short timeout', async () => {
|
|
197
|
+
// 1ms timeout should trigger fallback
|
|
198
|
+
const ctx = await gatherWithTimeout(1);
|
|
199
|
+
|
|
200
|
+
// Should still have basic structure
|
|
201
|
+
assert.ok(ctx.project !== undefined);
|
|
202
|
+
assert.ok(ctx.suggestions !== undefined);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
207
|
+
// SECURITY TESTS
|
|
208
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
209
|
+
|
|
210
|
+
describe('Security: Shell injection prevention', () => {
|
|
211
|
+
test('branch names with shell metacharacters are safe', () => {
|
|
212
|
+
// This test verifies we're using execFileSync, not execSync
|
|
213
|
+
// If we were vulnerable, a branch like "; rm -rf /" would execute
|
|
214
|
+
// Since we use execFileSync with shell: false, it's treated as literal text
|
|
215
|
+
const info = getGitInfo();
|
|
216
|
+
// If we got here without error, shell injection was prevented
|
|
217
|
+
assert.ok(true, 'No shell injection occurred');
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe('Security: Output sanitization', () => {
|
|
222
|
+
test('ANSI escape sequences in commit messages are stripped', () => {
|
|
223
|
+
const ctx = gatherWorkContext();
|
|
224
|
+
if (ctx.git?.recentCommits?.length > 0) {
|
|
225
|
+
ctx.git.recentCommits.forEach(commit => {
|
|
226
|
+
assert.ok(!commit.message.includes('\x1B'), 'should not contain ANSI escapes');
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
233
|
+
// Run tests when executed directly
|
|
234
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
235
|
+
|
|
236
|
+
if (require.main === module) {
|
|
237
|
+
console.log('Running _work-context.js tests...\n');
|
|
238
|
+
console.log('Use: node --test tools/_work-context.test.js');
|
|
239
|
+
console.log('Or run this file directly for a quick smoke test.\n');
|
|
240
|
+
|
|
241
|
+
// Quick smoke test
|
|
242
|
+
console.log('=== Quick Smoke Test ===\n');
|
|
243
|
+
|
|
244
|
+
const ctx = gatherWorkContext();
|
|
245
|
+
console.log('Project:', ctx.project?.name);
|
|
246
|
+
console.log('Type:', ctx.project?.type);
|
|
247
|
+
console.log('Branch:', ctx.git?.branch || 'N/A');
|
|
248
|
+
console.log('Recent commits:', ctx.git?.recentCommits?.length || 0);
|
|
249
|
+
console.log('Changed files:', ctx.git?.changedFiles?.length || 0);
|
|
250
|
+
console.log('Has uncommitted:', ctx.git?.hasUncommitted);
|
|
251
|
+
console.log('\nBrief summary:', ctx.suggestions?.brief);
|
|
252
|
+
console.log('Detailed summary:', ctx.suggestions?.detailed);
|
|
253
|
+
|
|
254
|
+
console.log('\n=== Sanitization Tests ===\n');
|
|
255
|
+
console.log('sanitize("\\x1B[31mred\\x1B[0m"):', sanitize('\x1B[31mred\x1B[0m'));
|
|
256
|
+
console.log('cap("hello world", 8):', cap('hello world', 8));
|
|
257
|
+
console.log('redact("email: a@b.com"):', redact('email: a@b.com'));
|
|
258
|
+
|
|
259
|
+
console.log('\n✓ All smoke tests passed\n');
|
|
260
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vibe activity — See what's happening on /vibe
|
|
3
|
+
*
|
|
4
|
+
* Real-time activity feed showing:
|
|
5
|
+
* - Ships (what people are shipping)
|
|
6
|
+
* - Joins (who just joined)
|
|
7
|
+
* - Help requests (who needs help)
|
|
8
|
+
* - Online activity (who's active)
|
|
9
|
+
*
|
|
10
|
+
* Creates FOMO, drives engagement
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const config = require('../config');
|
|
14
|
+
const { requireInit } = require('./_shared');
|
|
15
|
+
const { actions, formatActions } = require('./_actions');
|
|
16
|
+
|
|
17
|
+
const definition = {
|
|
18
|
+
name: 'vibe_activity',
|
|
19
|
+
description: 'See what\'s happening on /vibe - ships, joins, help requests, and online activity.',
|
|
20
|
+
inputSchema: {
|
|
21
|
+
type: 'object',
|
|
22
|
+
properties: {
|
|
23
|
+
filter: {
|
|
24
|
+
type: 'string',
|
|
25
|
+
enum: ['all', 'ships', 'joins', 'help', 'online'],
|
|
26
|
+
description: 'Filter by activity type (default: all)'
|
|
27
|
+
},
|
|
28
|
+
limit: {
|
|
29
|
+
type: 'number',
|
|
30
|
+
description: 'Number of activities to show (default: 15)'
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Activity type to emoji mapping
|
|
37
|
+
const TYPE_ICONS = {
|
|
38
|
+
ship: '🚀',
|
|
39
|
+
join: '👋',
|
|
40
|
+
help: '🆘',
|
|
41
|
+
online: '🟢',
|
|
42
|
+
achievement: '🏆',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Format relative time
|
|
46
|
+
function formatTimeAgo(timestamp) {
|
|
47
|
+
const now = Date.now();
|
|
48
|
+
const time = new Date(timestamp).getTime();
|
|
49
|
+
const seconds = Math.floor((now - time) / 1000);
|
|
50
|
+
|
|
51
|
+
if (seconds < 60) return 'just now';
|
|
52
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
|
|
53
|
+
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
|
|
54
|
+
return `${Math.floor(seconds / 86400)}d ago`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function handler(args) {
|
|
58
|
+
const initCheck = requireInit();
|
|
59
|
+
if (initCheck) return initCheck;
|
|
60
|
+
|
|
61
|
+
const { filter = 'all', limit = 15 } = args;
|
|
62
|
+
const apiUrl = config.getApiUrl();
|
|
63
|
+
|
|
64
|
+
// Build query params
|
|
65
|
+
const params = new URLSearchParams();
|
|
66
|
+
params.set('limit', String(limit));
|
|
67
|
+
|
|
68
|
+
if (filter !== 'all') {
|
|
69
|
+
// Map filter to API type
|
|
70
|
+
const typeMap = {
|
|
71
|
+
ships: 'ship',
|
|
72
|
+
joins: 'join',
|
|
73
|
+
help: 'help',
|
|
74
|
+
online: 'online',
|
|
75
|
+
};
|
|
76
|
+
params.set('types', typeMap[filter] || filter);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const response = await fetch(`${apiUrl}/api/feed?${params.toString()}`, {
|
|
81
|
+
headers: { 'User-Agent': 'vibe-mcp-client' }
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
return {
|
|
86
|
+
display: '❌ Failed to fetch activity feed. Try again later.'
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const data = await response.json();
|
|
91
|
+
|
|
92
|
+
if (!data.success || !data.activities || data.activities.length === 0) {
|
|
93
|
+
return {
|
|
94
|
+
display: `## 📡 Activity Feed
|
|
95
|
+
|
|
96
|
+
_No recent activity to show._
|
|
97
|
+
|
|
98
|
+
**Be the first!**
|
|
99
|
+
- Ship something: \`vibe ship "Built a cool feature"\`
|
|
100
|
+
- Ask for help: \`vibe stuck "Need help with auth"\`
|
|
101
|
+
- Say hi: \`vibe dm @someone hello!\``
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let display = `## 📡 Activity Feed\n\n`;
|
|
106
|
+
|
|
107
|
+
// Group by time period for better display
|
|
108
|
+
const { grouped } = data;
|
|
109
|
+
|
|
110
|
+
if (grouped.recent?.length > 0) {
|
|
111
|
+
display += `**🔴 Live (last 30 min)**\n\n`;
|
|
112
|
+
for (const activity of grouped.recent) {
|
|
113
|
+
display += formatActivity(activity);
|
|
114
|
+
}
|
|
115
|
+
display += '\n';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (grouped.today?.length > 0) {
|
|
119
|
+
display += `**📅 Today**\n\n`;
|
|
120
|
+
for (const activity of grouped.today) {
|
|
121
|
+
display += formatActivity(activity);
|
|
122
|
+
}
|
|
123
|
+
display += '\n';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (grouped.earlier?.length > 0) {
|
|
127
|
+
display += `**📆 Earlier**\n\n`;
|
|
128
|
+
for (const activity of grouped.earlier.slice(0, 5)) {
|
|
129
|
+
display += formatActivity(activity);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Quick stats
|
|
134
|
+
const shipCount = data.activities.filter(a => a.type === 'ship').length;
|
|
135
|
+
const onlineCount = data.activities.filter(a => a.type === 'online').length;
|
|
136
|
+
const helpCount = data.activities.filter(a => a.type === 'help').length;
|
|
137
|
+
|
|
138
|
+
display += `---\n`;
|
|
139
|
+
display += `${shipCount} ships · ${onlineCount} active · ${helpCount} need help\n`;
|
|
140
|
+
|
|
141
|
+
// Add filter hint
|
|
142
|
+
display += `\n_Filter: \`vibe activity --filter ships\` or \`help\` or \`joins\`_`;
|
|
143
|
+
|
|
144
|
+
const response_data = { display };
|
|
145
|
+
|
|
146
|
+
// Suggest actions based on what's happening
|
|
147
|
+
const actionList = [];
|
|
148
|
+
|
|
149
|
+
// If there's a help request, suggest helping
|
|
150
|
+
const helpActivity = data.activities.find(a => a.type === 'help');
|
|
151
|
+
if (helpActivity) {
|
|
152
|
+
actionList.push({
|
|
153
|
+
label: `Help @${helpActivity.handle}`,
|
|
154
|
+
action: `vibe stuck offer ${helpActivity.handle}`,
|
|
155
|
+
description: `They need help with: ${helpActivity.content?.slice(0, 40)}...`
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// If someone shipped, suggest reacting
|
|
160
|
+
const shipActivity = data.activities.find(a => a.type === 'ship');
|
|
161
|
+
if (shipActivity) {
|
|
162
|
+
actionList.push({
|
|
163
|
+
label: `React to @${shipActivity.handle}'s ship`,
|
|
164
|
+
action: `vibe react fire to @${shipActivity.handle}`,
|
|
165
|
+
description: `Shipped: ${shipActivity.content?.slice(0, 40)}...`
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// If someone just joined, suggest welcoming
|
|
170
|
+
const joinActivity = data.activities.find(a => a.type === 'join');
|
|
171
|
+
if (joinActivity) {
|
|
172
|
+
actionList.push({
|
|
173
|
+
label: `Welcome @${joinActivity.handle}`,
|
|
174
|
+
action: `vibe dm @${joinActivity.handle} Welcome to /vibe!`,
|
|
175
|
+
description: 'Say hi to the new builder'
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (actionList.length > 0) {
|
|
180
|
+
response_data.actions = formatActions(actionList);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return response_data;
|
|
184
|
+
|
|
185
|
+
} catch (error) {
|
|
186
|
+
console.error('[vibe_activity] Error:', error.message);
|
|
187
|
+
return {
|
|
188
|
+
display: '❌ Failed to fetch activity feed. Check your connection.'
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function formatActivity(activity) {
|
|
194
|
+
const icon = activity.icon || TYPE_ICONS[activity.type] || '•';
|
|
195
|
+
const time = formatTimeAgo(activity.timestamp);
|
|
196
|
+
|
|
197
|
+
switch (activity.type) {
|
|
198
|
+
case 'ship':
|
|
199
|
+
return `${icon} **@${activity.handle}** shipped: ${activity.content || 'something'}\n _${time}_\n\n`;
|
|
200
|
+
|
|
201
|
+
case 'join':
|
|
202
|
+
const name = activity.github_name ? ` (${activity.github_name})` : '';
|
|
203
|
+
return `${icon} **@${activity.handle}**${name} joined /vibe\n _${time}_\n\n`;
|
|
204
|
+
|
|
205
|
+
case 'help':
|
|
206
|
+
const urgency = activity.urgency === 'high' ? ' 🔴' : '';
|
|
207
|
+
return `${icon} **@${activity.handle}** needs help${urgency}: ${activity.content || 'something'}\n _${time}_\n\n`;
|
|
208
|
+
|
|
209
|
+
case 'online':
|
|
210
|
+
return `${icon} **@${activity.handle}** is building: ${activity.content || 'something'}\n _${time}_\n\n`;
|
|
211
|
+
|
|
212
|
+
case 'achievement':
|
|
213
|
+
return `${icon} **@${activity.handle}** earned: ${activity.content}\n _${time}_\n\n`;
|
|
214
|
+
|
|
215
|
+
default:
|
|
216
|
+
return `• **@${activity.handle}**: ${activity.content || ''}\n _${time}_\n\n`;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
module.exports = { definition, handler };
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vibe analytics — Real-time launch metrics for terminal
|
|
3
|
+
*
|
|
4
|
+
* Shows live platform metrics in a terminal-friendly format.
|
|
5
|
+
* Perfect for monitoring during Watch Me Code launch.
|
|
6
|
+
*
|
|
7
|
+
* Commands:
|
|
8
|
+
* - analytics (default) — Launch dashboard with all metrics
|
|
9
|
+
* - analytics presence — Who's online right now
|
|
10
|
+
* - analytics sessions — Live streaming stats
|
|
11
|
+
* - analytics health — System health status
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const config = require('../config');
|
|
15
|
+
|
|
16
|
+
const API_BASE = 'https://www.slashvibe.dev/api';
|
|
17
|
+
|
|
18
|
+
const definition = {
|
|
19
|
+
name: 'vibe_analytics',
|
|
20
|
+
description: 'Real-time launch metrics. Shows presence, sessions, health. Perfect for launch monitoring.',
|
|
21
|
+
inputSchema: {
|
|
22
|
+
type: 'object',
|
|
23
|
+
properties: {
|
|
24
|
+
view: {
|
|
25
|
+
type: 'string',
|
|
26
|
+
enum: ['dashboard', 'presence', 'sessions', 'health'],
|
|
27
|
+
description: 'Which view to show (default: dashboard)'
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Fetch launch metrics from API
|
|
34
|
+
async function fetchLaunchMetrics() {
|
|
35
|
+
try {
|
|
36
|
+
const res = await fetch(`${API_BASE}/analytics/launch`);
|
|
37
|
+
const data = await res.json();
|
|
38
|
+
return data.success ? data : null;
|
|
39
|
+
} catch (e) {
|
|
40
|
+
console.error('[analytics] Fetch error:', e.message);
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Format the full dashboard view
|
|
46
|
+
function formatDashboard(data) {
|
|
47
|
+
const { presence, sessions, growth, viral, health, countdown } = data;
|
|
48
|
+
|
|
49
|
+
const lines = [
|
|
50
|
+
'',
|
|
51
|
+
'╔══════════════════════════════════════════════════════════════╗',
|
|
52
|
+
`║ 🚀 LAUNCH COMMAND CENTER ${countdown.padStart(20)} ║`,
|
|
53
|
+
'╠══════════════════════════════════════════════════════════════╣',
|
|
54
|
+
'║ ║',
|
|
55
|
+
'║ PRESENCE LIVE SESSIONS ║',
|
|
56
|
+
`║ 🟢 Online: ${String(presence.online).padEnd(6)} 📺 Live Now: ${String(sessions.live_now).padEnd(5)} ║`,
|
|
57
|
+
`║ 🟡 Away: ${String(presence.away).padEnd(6)} 👀 Viewers: ${String(sessions.viewers_now).padEnd(5)} ║`,
|
|
58
|
+
`║ Total: ${String(presence.total_active).padEnd(6)} 🔥 Reactions: ${String(sessions.reactions_now || 0).padEnd(5)} ║`,
|
|
59
|
+
'║ ║',
|
|
60
|
+
'╠══════════════════════════════════════════════════════════════╣',
|
|
61
|
+
'║ GROWTH TODAY VIRAL ║',
|
|
62
|
+
`║ 📝 Registrations: ${String(growth.registrations_today).padEnd(4)} 📨 Invites: ${String(viral.invites_sent_today).padEnd(5)} ║`,
|
|
63
|
+
`║ 💬 Messages: ${String(growth.messages_today).padEnd(4)} 🔗 Shares: ${String(viral.share_link_clicks_today).padEnd(5)} ║`,
|
|
64
|
+
`║ 🚀 Ships: ${String(growth.ships_today).padEnd(4)} 🖼️ OG Views: ${String(viral.og_image_requests_today).padEnd(5)} ║`,
|
|
65
|
+
'║ ║',
|
|
66
|
+
'╠══════════════════════════════════════════════════════════════╣',
|
|
67
|
+
'║ HEALTH ║',
|
|
68
|
+
`║ Status: ${health.status === 'healthy' ? '✅ Healthy' : health.status === 'degraded' ? '⚠️ Degraded' : '❌ Unhealthy'} Latency: ${String(health.api_latency_ms).padEnd(4)}ms ║`,
|
|
69
|
+
`║ KV: ${health.kv_healthy ? '✅' : '❌'} Postgres: ${health.db_healthy ? '✅' : '❌'} ║`,
|
|
70
|
+
'║ ║',
|
|
71
|
+
'╚══════════════════════════════════════════════════════════════╝',
|
|
72
|
+
'',
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
return lines.join('\n');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Format presence view
|
|
79
|
+
function formatPresence(data) {
|
|
80
|
+
const { presence } = data;
|
|
81
|
+
|
|
82
|
+
return `
|
|
83
|
+
**Presence** (real-time)
|
|
84
|
+
|
|
85
|
+
🟢 **${presence.online}** online now
|
|
86
|
+
🟡 **${presence.away}** away
|
|
87
|
+
📊 **${presence.total_active}** total active
|
|
88
|
+
|
|
89
|
+
_Updated: ${new Date().toLocaleTimeString()}_
|
|
90
|
+
`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Format sessions view
|
|
94
|
+
function formatSessions(data) {
|
|
95
|
+
const { sessions } = data;
|
|
96
|
+
|
|
97
|
+
if (sessions.live_now === 0) {
|
|
98
|
+
return `
|
|
99
|
+
**Live Sessions**
|
|
100
|
+
|
|
101
|
+
No live sessions right now.
|
|
102
|
+
|
|
103
|
+
📺 Sessions today: ${sessions.started_today}
|
|
104
|
+
🔗 Shares today: ${sessions.shares_today}
|
|
105
|
+
✂️ Clips today: ${sessions.clips_today}
|
|
106
|
+
|
|
107
|
+
_Start streaming with \`vibe broadcast start "title"\`_
|
|
108
|
+
`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let display = `
|
|
112
|
+
**Live Sessions** 📺
|
|
113
|
+
|
|
114
|
+
🔴 **${sessions.live_now}** live now
|
|
115
|
+
👀 **${sessions.viewers_now}** watching
|
|
116
|
+
🔥 **${sessions.reactions_now || 0}** reactions
|
|
117
|
+
`;
|
|
118
|
+
|
|
119
|
+
// Show top streams if available
|
|
120
|
+
if (sessions.live_streams && sessions.live_streams.length > 0) {
|
|
121
|
+
display += '\n**Active Streams:**\n';
|
|
122
|
+
for (const stream of sessions.live_streams) {
|
|
123
|
+
display += `• @${stream.handle}: ${stream.viewers} viewers, ${stream.durationMins}m\n`;
|
|
124
|
+
display += ` ${stream.watchUrl}\n`;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
display += `
|
|
129
|
+
Today:
|
|
130
|
+
- ${sessions.started_today} sessions started
|
|
131
|
+
- ${sessions.shares_today} shares
|
|
132
|
+
- ${sessions.clips_today} clips
|
|
133
|
+
|
|
134
|
+
_Watch live at slashvibe.dev/live_
|
|
135
|
+
`;
|
|
136
|
+
|
|
137
|
+
return display;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Format health view
|
|
141
|
+
function formatHealth(data) {
|
|
142
|
+
const { health } = data;
|
|
143
|
+
|
|
144
|
+
const statusEmoji = health.status === 'healthy' ? '✅' : health.status === 'degraded' ? '⚠️' : '❌';
|
|
145
|
+
|
|
146
|
+
return `
|
|
147
|
+
**System Health** ${statusEmoji}
|
|
148
|
+
|
|
149
|
+
Status: **${health.status}**
|
|
150
|
+
Latency: ${health.api_latency_ms}ms
|
|
151
|
+
|
|
152
|
+
Services:
|
|
153
|
+
- KV Storage: ${health.kv_healthy ? '✅ Healthy' : '❌ Down'}
|
|
154
|
+
- PostgreSQL: ${health.db_healthy ? '✅ Healthy' : '❌ Down'}
|
|
155
|
+
|
|
156
|
+
_Check slashvibe.dev/mission-control for full dashboard_
|
|
157
|
+
`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function handler(args) {
|
|
161
|
+
const view = args.view || 'dashboard';
|
|
162
|
+
|
|
163
|
+
const data = await fetchLaunchMetrics();
|
|
164
|
+
|
|
165
|
+
if (!data) {
|
|
166
|
+
return {
|
|
167
|
+
display: '❌ Could not fetch analytics. Check your connection.'
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let display;
|
|
172
|
+
|
|
173
|
+
switch (view) {
|
|
174
|
+
case 'presence':
|
|
175
|
+
display = formatPresence(data);
|
|
176
|
+
break;
|
|
177
|
+
case 'sessions':
|
|
178
|
+
display = formatSessions(data);
|
|
179
|
+
break;
|
|
180
|
+
case 'health':
|
|
181
|
+
display = formatHealth(data);
|
|
182
|
+
break;
|
|
183
|
+
case 'dashboard':
|
|
184
|
+
default:
|
|
185
|
+
display = formatDashboard(data);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return { display };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
module.exports = { definition, handler };
|