slashvibe-mcp 0.2.2 → 0.2.4

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.
Files changed (162) hide show
  1. package/README.md +1 -0
  2. package/analytics.js +107 -0
  3. package/config.js +174 -3
  4. package/index.js +163 -34
  5. package/intelligence/index.js +45 -0
  6. package/intelligence/infer.js +316 -0
  7. package/intelligence/interests.js +369 -0
  8. package/intelligence/patterns.js +651 -0
  9. package/intelligence/proactive.js +358 -0
  10. package/intelligence/serendipity.js +306 -0
  11. package/notification-emitter.js +77 -0
  12. package/notify.js +141 -18
  13. package/package.json +14 -6
  14. package/presence.js +5 -1
  15. package/protocol/index.js +88 -1
  16. package/protocol/telegram-commands.js +199 -0
  17. package/store/api.js +469 -29
  18. package/store/index.js +7 -7
  19. package/store/local.js +67 -11
  20. package/store/profiles.js +435 -0
  21. package/store/reservations.js +321 -0
  22. package/store/skills.js +378 -0
  23. package/tools/_actions.js +491 -22
  24. package/tools/_connection-queue.js +257 -0
  25. package/tools/_discovery-enhanced.js +290 -0
  26. package/tools/_discovery.js +439 -0
  27. package/tools/_proactive-discovery.js +301 -0
  28. package/tools/_shared/index.js +64 -0
  29. package/tools/_work-context.js +338 -0
  30. package/tools/_work-context.manual-test.js +199 -0
  31. package/tools/_work-context.test.js +260 -0
  32. package/tools/admin-inbox.js +218 -0
  33. package/tools/agent-treasury.js +288 -0
  34. package/tools/agents.js +122 -0
  35. package/tools/analytics.js +191 -0
  36. package/tools/approve.js +197 -0
  37. package/tools/arcade.js +173 -0
  38. package/tools/artifact-create.js +247 -0
  39. package/tools/artifact-view.js +174 -0
  40. package/tools/artifacts-price.js +107 -0
  41. package/tools/ask-expert.js +160 -0
  42. package/tools/auto-suggest-connections.js +304 -0
  43. package/tools/away.js +68 -0
  44. package/tools/back.js +51 -0
  45. package/tools/become-expert.js +150 -0
  46. package/tools/bootstrap-skills.js +231 -0
  47. package/tools/bridge-dashboard.js +342 -0
  48. package/tools/bridge-health.js +400 -0
  49. package/tools/bridge-live.js +384 -0
  50. package/tools/bridges.js +383 -0
  51. package/tools/broadcast.js +286 -0
  52. package/tools/bye.js +4 -0
  53. package/tools/chat.js +202 -0
  54. package/tools/collaborative-drawing.js +286 -0
  55. package/tools/colorguess.js +281 -0
  56. package/tools/crossword.js +369 -0
  57. package/tools/discover-insights.js +379 -0
  58. package/tools/discover-momentum.js +256 -0
  59. package/tools/discover.js +675 -0
  60. package/tools/discovery-analytics.js +345 -0
  61. package/tools/discovery-auto-suggest.js +275 -0
  62. package/tools/discovery-bootstrap.js +267 -0
  63. package/tools/discovery-daily.js +375 -0
  64. package/tools/discovery-dashboard.js +385 -0
  65. package/tools/discovery-digest.js +314 -0
  66. package/tools/discovery-hub.js +357 -0
  67. package/tools/discovery-insights.js +384 -0
  68. package/tools/discovery-momentum.js +281 -0
  69. package/tools/discovery-monitor.js +319 -0
  70. package/tools/discovery-proactive.js +300 -0
  71. package/tools/dm.js +84 -14
  72. package/tools/draw.js +317 -0
  73. package/tools/drawing.js +310 -0
  74. package/tools/earnings.js +126 -0
  75. package/tools/echo.js +16 -0
  76. package/tools/farcaster.js +307 -0
  77. package/tools/feed.js +215 -0
  78. package/tools/follow.js +224 -0
  79. package/tools/friends.js +192 -0
  80. package/tools/game.js +218 -110
  81. package/tools/games-catalog.js +376 -0
  82. package/tools/games.js +313 -0
  83. package/tools/genesis.js +233 -0
  84. package/tools/gig-browse.js +206 -0
  85. package/tools/gig-complete.js +139 -0
  86. package/tools/guessnumber.js +194 -0
  87. package/tools/hangman.js +129 -0
  88. package/tools/help.js +269 -0
  89. package/tools/idea.js +217 -0
  90. package/tools/inbox.js +291 -25
  91. package/tools/init.js +657 -33
  92. package/tools/insights.js +123 -0
  93. package/tools/invite.js +142 -21
  94. package/tools/l2-bridge.js +272 -0
  95. package/tools/l2-status.js +217 -0
  96. package/tools/l2.js +206 -0
  97. package/tools/migrate.js +156 -0
  98. package/tools/mint.js +377 -0
  99. package/tools/multiplayer-game.js +275 -0
  100. package/tools/multiplayer-tictactoe.js +303 -0
  101. package/tools/mute.js +97 -0
  102. package/tools/notifications.js +415 -0
  103. package/tools/observe.js +200 -0
  104. package/tools/onboarding.js +147 -0
  105. package/tools/open.js +52 -3
  106. package/tools/party-game.js +314 -0
  107. package/tools/plan.js +225 -0
  108. package/tools/presence-agent.js +167 -0
  109. package/tools/profile.js +219 -0
  110. package/tools/proof-of-work.js +139 -0
  111. package/tools/pulse.js +218 -0
  112. package/tools/react.js +4 -0
  113. package/tools/release.js +83 -0
  114. package/tools/report.js +109 -0
  115. package/tools/reputation.js +175 -0
  116. package/tools/request.js +231 -0
  117. package/tools/reservations.js +116 -0
  118. package/tools/reserve.js +111 -0
  119. package/tools/riddle.js +240 -0
  120. package/tools/run-bootstrap.js +69 -0
  121. package/tools/schedule.js +367 -0
  122. package/tools/session.js +420 -0
  123. package/tools/session_price.js +128 -0
  124. package/tools/settings.js +200 -0
  125. package/tools/ship.js +188 -0
  126. package/tools/shipback.js +326 -0
  127. package/tools/skills-analytics.js +349 -0
  128. package/tools/skills-bootstrap.js +301 -0
  129. package/tools/skills-dashboard.js +268 -0
  130. package/tools/skills-exchange.js +342 -0
  131. package/tools/skills.js +380 -0
  132. package/tools/smart-intro.js +353 -0
  133. package/tools/social-inbox.js +326 -69
  134. package/tools/social-post.js +251 -66
  135. package/tools/social-processor.js +445 -0
  136. package/tools/solo-game.js +390 -0
  137. package/tools/start.js +296 -81
  138. package/tools/status.js +53 -6
  139. package/tools/storybuilder.js +331 -0
  140. package/tools/stuck.js +297 -0
  141. package/tools/subscribe.js +148 -0
  142. package/tools/subscriptions.js +134 -0
  143. package/tools/suggest-tags.js +184 -0
  144. package/tools/tag-suggestions.js +257 -0
  145. package/tools/telegram-bot.js +183 -0
  146. package/tools/telegram-setup.js +214 -0
  147. package/tools/tictactoe.js +155 -0
  148. package/tools/tip.js +120 -0
  149. package/tools/token.js +103 -0
  150. package/tools/twentyquestions.js +143 -0
  151. package/tools/update.js +1 -1
  152. package/tools/wallet.js +127 -0
  153. package/tools/watch.js +157 -0
  154. package/tools/webhook-test.js +388 -0
  155. package/tools/who.js +118 -25
  156. package/tools/withdraw.js +145 -0
  157. package/tools/wordassociation.js +247 -0
  158. package/tools/work-summary.js +96 -0
  159. package/tools/workshop-buddy.js +394 -0
  160. package/tools/workshop.js +327 -0
  161. package/version.json +12 -3
  162. package/tools/board.js +0 -130
@@ -0,0 +1,175 @@
1
+ /**
2
+ * vibe_reputation - Check your reputation score and tier
3
+ *
4
+ * View your standing in the /vibe social hierarchy
5
+ *
6
+ * Examples:
7
+ * - "vibe reputation"
8
+ * - "check my tier"
9
+ * - "show my badges"
10
+ * - "vibe rep leaderboard"
11
+ */
12
+
13
+ const fetch = require('node-fetch');
14
+
15
+ module.exports = {
16
+ name: 'vibe_reputation',
17
+ description: 'Check your reputation score, tier, badges, and ranking. Shows economic, social, expert, and creator scores.',
18
+
19
+ inputSchema: {
20
+ type: 'object',
21
+ properties: {
22
+ action: {
23
+ type: 'string',
24
+ enum: ['score', 'leaderboard'],
25
+ description: 'Action: score (your stats) or leaderboard (top users)',
26
+ default: 'score'
27
+ },
28
+ dimension: {
29
+ type: 'string',
30
+ enum: ['overall', 'economic', 'social', 'expert', 'creator'],
31
+ description: 'Leaderboard dimension (only for leaderboard action)',
32
+ default: 'overall'
33
+ },
34
+ limit: {
35
+ type: 'number',
36
+ description: 'Number of users to show in leaderboard (default 10)',
37
+ default: 10
38
+ }
39
+ }
40
+ },
41
+
42
+ async execute({ action = 'score', dimension = 'overall', limit = 10 }, context) {
43
+ try {
44
+ const handle = context.handle;
45
+
46
+ if (!handle) {
47
+ return {
48
+ success: false,
49
+ error: 'Not authenticated. Use vibe init first.'
50
+ };
51
+ }
52
+
53
+ const apiUrl = process.env.VIBE_API_URL || 'https://www.slashvibe.dev';
54
+
55
+ if (action === 'score') {
56
+ const response = await fetch(
57
+ `${apiUrl}/api/reputation/score?handle=${handle}`,
58
+ {
59
+ headers: {
60
+ 'Authorization': `Bearer ${context.token}`
61
+ }
62
+ }
63
+ );
64
+
65
+ const result = await response.json();
66
+
67
+ if (!response.ok) {
68
+ return {
69
+ success: false,
70
+ error: result.error || 'Failed to fetch reputation',
71
+ message: result.message
72
+ };
73
+ }
74
+
75
+ const tierIcons = {
76
+ genesis: '🌱',
77
+ bronze: '🥉',
78
+ silver: '🥈',
79
+ gold: '🥇',
80
+ platinum: '💎',
81
+ diamond: '👑'
82
+ };
83
+
84
+ const progressBar = '█'.repeat(Math.floor(result.progress_to_next * 20)) +
85
+ '░'.repeat(20 - Math.floor(result.progress_to_next * 20));
86
+
87
+ const badgeList = result.badges?.slice(0, 5).map(b =>
88
+ ` ${b.icon || '🏆'} ${b.name} (${b.rarity})`
89
+ ) || [];
90
+
91
+ return {
92
+ success: true,
93
+ reputation: result,
94
+ formatted: `
95
+ ⭐ Reputation: ${handle}
96
+
97
+ Tier: ${tierIcons[result.tier] || ''} ${result.tier.toUpperCase()}
98
+ Overall Score: ${result.overall_score}
99
+ Global Rank: #${result.rank} (top ${result.percentile.toFixed(1)}%)
100
+
101
+ Dimension Scores:
102
+ 💰 Economic: ${result.scores.economic}
103
+ 👥 Social: ${result.scores.social}
104
+ 🎓 Expert: ${result.scores.expert}
105
+ 🎨 Creator: ${result.scores.creator}
106
+
107
+ Progress to ${result.next_tier?.toUpperCase() || 'MAX'}:
108
+ [${progressBar}] ${(result.progress_to_next * 100).toFixed(1)}%
109
+ ${result.points_needed > 0 ? `${result.points_needed} points needed` : 'Max tier reached!'}
110
+
111
+ Badges (${result.badges?.length || 0}):
112
+ ${badgeList.length > 0 ? badgeList.join('\n') : ' No badges yet'}
113
+
114
+ Tier Unlocks:
115
+ ${JSON.stringify(result.tier_unlocks, null, 2).replace(/[{}"]/g, '').trim()}
116
+ `.trim()
117
+ };
118
+
119
+ } else if (action === 'leaderboard') {
120
+ const response = await fetch(
121
+ `${apiUrl}/api/reputation/leaderboard?dimension=${dimension}&limit=${Math.min(limit, 50)}`,
122
+ {
123
+ headers: {
124
+ 'Authorization': `Bearer ${context.token}`
125
+ }
126
+ }
127
+ );
128
+
129
+ const result = await response.json();
130
+
131
+ if (!response.ok) {
132
+ return {
133
+ success: false,
134
+ error: 'Failed to fetch leaderboard'
135
+ };
136
+ }
137
+
138
+ const tierIcons = {
139
+ genesis: '🌱',
140
+ bronze: '🥉',
141
+ silver: '🥈',
142
+ gold: '🥇',
143
+ platinum: '💎',
144
+ diamond: '👑'
145
+ };
146
+
147
+ const leaderboardLines = result.leaderboard?.map(user => {
148
+ const tier = tierIcons[user.tier] || '';
149
+ const scoreField = dimension === 'overall' ? user.overall_score : user.score;
150
+ return ` ${user.rank}. ${user.handle.padEnd(15)} ${scoreField.toString().padStart(5)} ${tier} ${user.tier}`;
151
+ }) || [];
152
+
153
+ return {
154
+ success: true,
155
+ leaderboard: result.leaderboard,
156
+ formatted: `
157
+ 🏆 Reputation Leaderboard (${dimension})
158
+
159
+ ${leaderboardLines.join('\n')}
160
+
161
+ Dimension: ${dimension}
162
+ Total users: ${result.leaderboard?.length || 0}
163
+ `.trim()
164
+ };
165
+ }
166
+
167
+ } catch (error) {
168
+ return {
169
+ success: false,
170
+ error: 'Failed to fetch reputation',
171
+ details: error.message
172
+ };
173
+ }
174
+ }
175
+ };
@@ -0,0 +1,231 @@
1
+ /**
2
+ * vibe request — Post build requests / wishes
3
+ *
4
+ * "I wish X existed" → others can claim and build
5
+ *
6
+ * Post: vibe request "I wish there was a tool for..."
7
+ * Claim: vibe request claim [id]
8
+ * Browse: vibe requests
9
+ */
10
+
11
+ const config = require('../config');
12
+ const { requireInit, header, emptyState, formatTimeAgo, divider } = require('./_shared');
13
+
14
+ const definition = {
15
+ name: 'vibe_request',
16
+ description: 'Post a build request — something you wish existed. Others can claim and build it.',
17
+ inputSchema: {
18
+ type: 'object',
19
+ properties: {
20
+ content: {
21
+ type: 'string',
22
+ description: 'What you wish existed (leave empty to browse requests)'
23
+ },
24
+ claim: {
25
+ type: 'string',
26
+ description: 'ID of request to claim (e.g., "req_abc123")'
27
+ },
28
+ bounty: {
29
+ type: 'string',
30
+ description: 'What you\'re offering (e.g., "feedback", "shoutout", "collab")'
31
+ },
32
+ tags: {
33
+ type: 'array',
34
+ items: { type: 'string' },
35
+ description: 'Tags for the request'
36
+ },
37
+ show: {
38
+ type: 'string',
39
+ enum: ['open', 'claimed', 'all'],
40
+ description: 'Filter by status (default: open)'
41
+ },
42
+ limit: {
43
+ type: 'number',
44
+ description: 'Number to show (default: 10)'
45
+ }
46
+ }
47
+ }
48
+ };
49
+
50
+ const STATUS_EMOJI = {
51
+ 'open': '🔓',
52
+ 'claimed': '🔨',
53
+ 'shipped': '✅'
54
+ };
55
+
56
+ async function handler(args) {
57
+ const initCheck = requireInit();
58
+ if (initCheck) return initCheck;
59
+
60
+ const apiUrl = config.getApiUrl();
61
+ const myHandle = config.getHandle();
62
+
63
+ // Claim a request
64
+ if (args.claim) {
65
+ // Post a claim as a special entry
66
+ try {
67
+ // Include auth token for API authorization
68
+ const authToken = config.getAuthToken();
69
+ const headers = { 'Content-Type': 'application/json' };
70
+ if (authToken) {
71
+ headers['Authorization'] = `Bearer ${authToken}`;
72
+ }
73
+
74
+ const response = await fetch(`${apiUrl}/api/board`, {
75
+ method: 'POST',
76
+ headers,
77
+ body: JSON.stringify({
78
+ author: myHandle,
79
+ category: 'claim',
80
+ content: `🔨 Claimed request: ${args.claim}`,
81
+ tags: [`claim:${args.claim}`]
82
+ })
83
+ });
84
+
85
+ const data = await response.json();
86
+
87
+ if (!data.success) {
88
+ return { display: `⚠️ Failed to claim: ${data.error}` };
89
+ }
90
+
91
+ return {
92
+ display: `🔨 claimed \`${args.claim}\``
93
+ };
94
+
95
+ } catch (error) {
96
+ return { display: `⚠️ Failed to claim: ${error.message}` };
97
+ }
98
+ }
99
+
100
+ // Post a request
101
+ if (args.content) {
102
+ const content = args.content.trim();
103
+
104
+ if (content.length > 500) {
105
+ return { display: '⚠️ Keep requests concise (max 500 chars)' };
106
+ }
107
+
108
+ // Build request entry
109
+ const bountyText = args.bounty ? ` [bounty: ${args.bounty}]` : '';
110
+ const fullContent = `${content}${bountyText}`;
111
+
112
+ try {
113
+ // Include auth token for API authorization
114
+ const authToken = config.getAuthToken();
115
+ const headers = { 'Content-Type': 'application/json' };
116
+ if (authToken) {
117
+ headers['Authorization'] = `Bearer ${authToken}`;
118
+ }
119
+
120
+ const response = await fetch(`${apiUrl}/api/board`, {
121
+ method: 'POST',
122
+ headers,
123
+ body: JSON.stringify({
124
+ author: myHandle,
125
+ category: 'request',
126
+ content: fullContent,
127
+ tags: args.tags || []
128
+ })
129
+ });
130
+
131
+ const data = await response.json();
132
+
133
+ if (!data.success) {
134
+ return { display: `⚠️ Failed to post request: ${data.error}` };
135
+ }
136
+
137
+ let display = `🔓 posted\n\n"${content}"`;
138
+
139
+ if (args.bounty) {
140
+ display += `\n_${args.bounty}_`;
141
+ }
142
+
143
+ return { display };
144
+
145
+ } catch (error) {
146
+ return { display: `⚠️ Failed to post: ${error.message}` };
147
+ }
148
+ }
149
+
150
+ // Browse requests
151
+ try {
152
+ const limit = Math.min(args.limit || 10, 30);
153
+ let url = `${apiUrl}/api/board?limit=${limit * 2}&category=request`;
154
+
155
+ // Also get claims to show status
156
+ const claimsUrl = `${apiUrl}/api/board?limit=50&category=claim`;
157
+
158
+ const [requestsRes, claimsRes] = await Promise.all([
159
+ fetch(url),
160
+ fetch(claimsUrl)
161
+ ]);
162
+
163
+ const requests = (await requestsRes.json()).entries || [];
164
+ const claims = (await claimsRes.json()).entries || [];
165
+
166
+ // Build claimed set
167
+ const claimedIds = new Set();
168
+ const claimers = new Map();
169
+ claims.forEach(c => {
170
+ const match = c.content.match(/Claimed request: (\S+)/);
171
+ if (match) {
172
+ claimedIds.add(match[1]);
173
+ claimers.set(match[1], c.author);
174
+ }
175
+ });
176
+
177
+ // Filter by status
178
+ let filtered = requests;
179
+ if (args.show === 'open') {
180
+ filtered = requests.filter(r => !claimedIds.has(r.id));
181
+ } else if (args.show === 'claimed') {
182
+ filtered = requests.filter(r => claimedIds.has(r.id));
183
+ }
184
+
185
+ if (filtered.length === 0) {
186
+ return {
187
+ display: `${header('Build Requests')}\n\n${emptyState('No requests yet...', 'Post one: "I wish [X] existed"')}`
188
+ };
189
+ }
190
+
191
+ let display = header('Build Requests');
192
+ if (args.show && args.show !== 'all') display += ` (${args.show})`;
193
+ display += '\n\n';
194
+
195
+ filtered.slice(0, limit).forEach(entry => {
196
+ const isClaimed = claimedIds.has(entry.id);
197
+ const status = isClaimed ? 'claimed' : 'open';
198
+ const emoji = STATUS_EMOJI[status];
199
+ const timeAgo = formatTimeAgo(entry.timestamp);
200
+ const shortId = entry.id.slice(-8);
201
+
202
+ // Parse bounty from content
203
+ const bountyMatch = entry.content.match(/\[bounty: ([^\]]+)\]/);
204
+ const bounty = bountyMatch ? bountyMatch[1] : null;
205
+ const cleanContent = entry.content.replace(/\[bounty: [^\]]+\]/, '').trim();
206
+
207
+ display += `${emoji} **@${entry.author}** _${timeAgo}_\n`;
208
+ display += ` "${cleanContent}"\n`;
209
+
210
+ if (bounty) {
211
+ display += ` 💎 _Bounty: ${bounty}_\n`;
212
+ }
213
+
214
+ if (isClaimed) {
215
+ const claimer = claimers.get(entry.id);
216
+ display += ` _Claimed by @${claimer}_\n`;
217
+ } else {
218
+ display += ` \`claim ${shortId}\`\n`;
219
+ }
220
+
221
+ display += '\n';
222
+ });
223
+
224
+ return { display };
225
+
226
+ } catch (error) {
227
+ return { display: `⚠️ Failed to load requests: ${error.message}` };
228
+ }
229
+ }
230
+
231
+ module.exports = { definition, handler };
@@ -0,0 +1,116 @@
1
+ /**
2
+ * vibe reservations — List active file reservations
3
+ *
4
+ * Shows who has reserved what files in the current scope.
5
+ */
6
+
7
+ const config = require('../config');
8
+ const reservations = require('../store/reservations');
9
+ const { requireInit, formatTimeAgo, emptyState } = require('./_shared');
10
+
11
+ const definition = {
12
+ name: 'vibe_reservations',
13
+ description: 'List active file reservations in the current project.',
14
+ inputSchema: {
15
+ type: 'object',
16
+ properties: {
17
+ active_only: {
18
+ type: 'boolean',
19
+ description: 'Only show active reservations (default: true)'
20
+ },
21
+ path_filter: {
22
+ type: 'string',
23
+ description: 'Filter by path (e.g., "src/auth")'
24
+ }
25
+ }
26
+ }
27
+ };
28
+
29
+ // Format remaining time
30
+ function formatRemaining(expires_ts) {
31
+ const now = new Date();
32
+ const expires = new Date(expires_ts);
33
+ const ms = expires - now;
34
+
35
+ if (ms <= 0) return 'expired';
36
+
37
+ const minutes = Math.floor(ms / 60000);
38
+ if (minutes < 60) return `${minutes}m left`;
39
+
40
+ const hours = Math.floor(minutes / 60);
41
+ if (hours < 24) return `${hours}h ${minutes % 60}m left`;
42
+
43
+ return `${Math.floor(hours / 24)}d left`;
44
+ }
45
+
46
+ async function handler(args) {
47
+ const initCheck = requireInit();
48
+ if (initCheck) return initCheck;
49
+
50
+ const handle = config.getHandle();
51
+ const { active_only = true, path_filter } = args;
52
+
53
+ // Cleanup expired first
54
+ reservations.cleanup();
55
+
56
+ // Get reservations
57
+ const list = reservations.list({ active_only, path_filter });
58
+ const scope = reservations.getScope();
59
+
60
+ if (list.length === 0) {
61
+ let display = `## Reservations\n\n`;
62
+ display += `**Scope:** ${scope}\n\n`;
63
+ display += emptyState(
64
+ 'No active reservations in this project.',
65
+ '`vibe reserve --paths ["file.ts"]` to claim files'
66
+ );
67
+ return { display };
68
+ }
69
+
70
+ let display = `## Reservations\n\n`;
71
+ display += `**Scope:** ${scope}\n\n`;
72
+
73
+ // Group by owner
74
+ const byOwner = {};
75
+ for (const r of list) {
76
+ if (!byOwner[r.owner]) {
77
+ byOwner[r.owner] = [];
78
+ }
79
+ byOwner[r.owner].push(r);
80
+ }
81
+
82
+ // Show own reservations first
83
+ const owners = Object.keys(byOwner).sort((a, b) => {
84
+ if (a === handle) return -1;
85
+ if (b === handle) return 1;
86
+ return a.localeCompare(b);
87
+ });
88
+
89
+ for (const owner of owners) {
90
+ const isMe = owner === handle;
91
+ display += `### @${owner}${isMe ? ' (you)' : ''}\n\n`;
92
+
93
+ for (const r of byOwner[owner]) {
94
+ const remaining = formatRemaining(r.expires_ts);
95
+ const exclusive = r.exclusive ? '🔒' : '📂';
96
+
97
+ display += `${exclusive} **${r.paths.map(p => `\`${p}\``).join(', ')}**\n`;
98
+ display += ` _${remaining}_`;
99
+ if (r.reason) {
100
+ display += ` — ${r.reason}`;
101
+ }
102
+ display += `\n`;
103
+ if (isMe) {
104
+ display += ` \`vibe release ${r.reservation_id}\` to release\n`;
105
+ }
106
+ display += `\n`;
107
+ }
108
+ }
109
+
110
+ display += `---\n`;
111
+ display += `${list.length} active reservation(s)`;
112
+
113
+ return { display };
114
+ }
115
+
116
+ module.exports = { definition, handler };
@@ -0,0 +1,111 @@
1
+ /**
2
+ * vibe reserve — Claim files you're working on
3
+ *
4
+ * Advisory lock to signal edit intent.
5
+ * Warns on conflicts, doesn't block.
6
+ */
7
+
8
+ const config = require('../config');
9
+ const reservations = require('../store/reservations');
10
+ const { requireInit, formatDuration, warning, success } = require('./_shared');
11
+
12
+ const definition = {
13
+ name: 'vibe_reserve',
14
+ description: 'Reserve files you\'re working on. Advisory lock — warns on conflicts, doesn\'t block.',
15
+ inputSchema: {
16
+ type: 'object',
17
+ properties: {
18
+ paths: {
19
+ type: 'array',
20
+ items: { type: 'string' },
21
+ description: 'Files or directories to reserve (relative paths, e.g., ["src/auth.ts", "src/api/"])'
22
+ },
23
+ ttl_seconds: {
24
+ type: 'number',
25
+ description: 'How long to hold reservation (default: 3600 = 1 hour)'
26
+ },
27
+ exclusive: {
28
+ type: 'boolean',
29
+ description: 'Warn others if they try to reserve same files (default: true)'
30
+ },
31
+ reason: {
32
+ type: 'string',
33
+ description: 'What you\'re doing (e.g., "Fix auth race condition")'
34
+ },
35
+ thread_id: {
36
+ type: 'string',
37
+ description: 'Link to a thread/task ID (optional)'
38
+ }
39
+ },
40
+ required: ['paths']
41
+ }
42
+ };
43
+
44
+ async function handler(args) {
45
+ const initCheck = requireInit();
46
+ if (initCheck) return initCheck;
47
+
48
+ const handle = config.getHandle();
49
+ const { paths, ttl_seconds = 3600, exclusive = true, reason, thread_id } = args;
50
+
51
+ if (!paths || paths.length === 0) {
52
+ return {
53
+ display: `## Reserve Files
54
+
55
+ Claim files you're working on to avoid conflicts.
56
+
57
+ **Usage:**
58
+ \`\`\`
59
+ vibe reserve --paths ["src/auth.ts"]
60
+ vibe reserve --paths ["src/api/"] --reason "Refactoring API layer"
61
+ vibe reserve --paths ["*.ts"] --ttl_seconds 7200
62
+ \`\`\`
63
+
64
+ **Options:**
65
+ - \`paths\` — Files or directories (required)
66
+ - \`reason\` — What you're doing
67
+ - \`ttl_seconds\` — Duration (default: 1 hour)
68
+ - \`exclusive\` — Warn on conflicts (default: true)
69
+ - \`thread_id\` — Link to task/thread`
70
+ };
71
+ }
72
+
73
+ // Create reservation
74
+ const result = reservations.create(handle, paths, {
75
+ ttl_seconds,
76
+ exclusive,
77
+ reason,
78
+ thread_id
79
+ });
80
+
81
+ const { reservation, conflicts } = result;
82
+ const duration = formatDuration(ttl_seconds * 1000);
83
+
84
+ let display = `## Reservation Created\n\n`;
85
+ display += `**ID:** \`${reservation.reservation_id}\`\n`;
86
+ display += `**Scope:** ${reservation.scope}\n`;
87
+ display += `**Files:** ${paths.map(p => `\`${p}\``).join(', ')}\n`;
88
+ display += `**Expires:** ${duration}\n`;
89
+ if (reason) {
90
+ display += `**Reason:** ${reason}\n`;
91
+ }
92
+
93
+ // Show conflicts as warnings
94
+ if (conflicts && conflicts.length > 0) {
95
+ display += `\n---\n\n`;
96
+ display += warning(`${conflicts.length} conflict(s) detected:\n\n`);
97
+ for (const c of conflicts) {
98
+ display += `- \`${c.path}\` reserved by **@${c.owner}**`;
99
+ if (c.reason) display += ` (${c.reason})`;
100
+ display += `\n`;
101
+ }
102
+ display += `\n_These are advisory warnings — you can still proceed._`;
103
+ }
104
+
105
+ display += `\n\n---\n`;
106
+ display += `\`vibe reservations\` to see all | \`vibe release ${reservation.reservation_id}\` when done`;
107
+
108
+ return { display };
109
+ }
110
+
111
+ module.exports = { definition, handler };