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,321 @@
1
+ /**
2
+ * Reservations Store — Local-first file reservations
3
+ *
4
+ * Advisory locks to signal edit intent and reduce conflicts.
5
+ * Stored locally in ~/.vibe/reservations/
6
+ *
7
+ * Key behaviors:
8
+ * - Scope auto-detected from git remote, fallback to cwd
9
+ * - Paths are relative to scope
10
+ * - TTL is client-enforced (check expires_ts on read)
11
+ * - Warn on conflict (overlapping exclusive paths), don't block
12
+ */
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const { execSync } = require('child_process');
17
+ const crypto = require('crypto');
18
+
19
+ const RESERVATIONS_DIR = path.join(process.env.HOME, '.vibe', 'reservations');
20
+ const ACTIVE_FILE = path.join(RESERVATIONS_DIR, 'active.jsonl');
21
+ const HISTORY_FILE = path.join(RESERVATIONS_DIR, 'history.jsonl');
22
+ const INDEX_FILE = path.join(RESERVATIONS_DIR, 'index.json');
23
+
24
+ // Ensure directories exist
25
+ function ensureDir() {
26
+ if (!fs.existsSync(RESERVATIONS_DIR)) {
27
+ fs.mkdirSync(RESERVATIONS_DIR, { recursive: true });
28
+ }
29
+ }
30
+
31
+ // Generate reservation ID: rsv-{nanoid}
32
+ function generateId() {
33
+ return 'rsv-' + crypto.randomBytes(4).toString('hex');
34
+ }
35
+
36
+ // Get git remote URL for scope
37
+ function getGitRemote() {
38
+ try {
39
+ const remote = execSync('git remote get-url origin 2>/dev/null', { encoding: 'utf8' }).trim();
40
+ // Normalize: git@github.com:user/repo.git → github.com/user/repo
41
+ if (remote.startsWith('git@')) {
42
+ return remote.replace('git@', '').replace(':', '/').replace('.git', '');
43
+ }
44
+ // https://github.com/user/repo.git → github.com/user/repo
45
+ if (remote.startsWith('https://')) {
46
+ return remote.replace('https://', '').replace('.git', '');
47
+ }
48
+ return remote;
49
+ } catch (e) {
50
+ return null;
51
+ }
52
+ }
53
+
54
+ // Get current working directory name as fallback scope
55
+ function getLocalScope() {
56
+ try {
57
+ const root = execSync('git rev-parse --show-toplevel 2>/dev/null', { encoding: 'utf8' }).trim();
58
+ return `local:${path.basename(root)}`;
59
+ } catch (e) {
60
+ return `local:${path.basename(process.cwd())}`;
61
+ }
62
+ }
63
+
64
+ // Get scope for reservations
65
+ function getScope() {
66
+ const remote = getGitRemote();
67
+ if (remote) {
68
+ return `repo:${remote}`;
69
+ }
70
+ return getLocalScope();
71
+ }
72
+
73
+ // Read all active reservations
74
+ function readActive() {
75
+ ensureDir();
76
+ if (!fs.existsSync(ACTIVE_FILE)) {
77
+ return [];
78
+ }
79
+ const content = fs.readFileSync(ACTIVE_FILE, 'utf8');
80
+ const reservations = content
81
+ .split('\n')
82
+ .filter(line => line.trim())
83
+ .map(line => {
84
+ try {
85
+ return JSON.parse(line);
86
+ } catch (e) {
87
+ return null;
88
+ }
89
+ })
90
+ .filter(r => r !== null);
91
+
92
+ // Filter out expired
93
+ const now = new Date().toISOString();
94
+ return reservations.filter(r => r.expires_ts > now && r.status === 'active');
95
+ }
96
+
97
+ // Write active reservations (rewrite entire file)
98
+ function writeActive(reservations) {
99
+ ensureDir();
100
+ const content = reservations.map(r => JSON.stringify(r)).join('\n') + (reservations.length ? '\n' : '');
101
+ fs.writeFileSync(ACTIVE_FILE, content);
102
+ }
103
+
104
+ // Append to history
105
+ function appendHistory(reservation) {
106
+ ensureDir();
107
+ fs.appendFileSync(HISTORY_FILE, JSON.stringify(reservation) + '\n');
108
+ }
109
+
110
+ // Update index (for quick lookups)
111
+ function updateIndex(reservations) {
112
+ ensureDir();
113
+ const index = {
114
+ count: reservations.length,
115
+ byPath: {},
116
+ byId: {},
117
+ updatedAt: new Date().toISOString()
118
+ };
119
+
120
+ for (const r of reservations) {
121
+ index.byId[r.reservation_id] = r;
122
+ for (const p of r.paths) {
123
+ if (!index.byPath[p]) {
124
+ index.byPath[p] = [];
125
+ }
126
+ index.byPath[p].push(r.reservation_id);
127
+ }
128
+ }
129
+
130
+ fs.writeFileSync(INDEX_FILE, JSON.stringify(index, null, 2));
131
+ }
132
+
133
+ // Check for conflicts with existing reservations
134
+ function checkConflicts(paths, exclusive = true, scope = null) {
135
+ const active = readActive();
136
+ const targetScope = scope || getScope();
137
+ const conflicts = [];
138
+
139
+ for (const reservation of active) {
140
+ // Only check same scope
141
+ if (reservation.scope !== targetScope) continue;
142
+ // Only check exclusive reservations
143
+ if (!reservation.exclusive) continue;
144
+
145
+ // Check path overlap
146
+ for (const newPath of paths) {
147
+ for (const existingPath of reservation.paths) {
148
+ // Exact match
149
+ if (newPath === existingPath) {
150
+ conflicts.push({
151
+ path: newPath,
152
+ reservation_id: reservation.reservation_id,
153
+ owner: reservation.owner,
154
+ reason: reservation.reason,
155
+ expires_ts: reservation.expires_ts
156
+ });
157
+ }
158
+ // Check if new path is under existing (existing: src/, new: src/auth.ts)
159
+ else if (newPath.startsWith(existingPath + '/')) {
160
+ conflicts.push({
161
+ path: newPath,
162
+ conflictsWith: existingPath,
163
+ reservation_id: reservation.reservation_id,
164
+ owner: reservation.owner,
165
+ reason: reservation.reason,
166
+ expires_ts: reservation.expires_ts
167
+ });
168
+ }
169
+ // Check if existing path is under new (existing: src/auth.ts, new: src/)
170
+ else if (existingPath.startsWith(newPath + '/')) {
171
+ conflicts.push({
172
+ path: existingPath,
173
+ conflictsWith: newPath,
174
+ reservation_id: reservation.reservation_id,
175
+ owner: reservation.owner,
176
+ reason: reservation.reason,
177
+ expires_ts: reservation.expires_ts
178
+ });
179
+ }
180
+ }
181
+ }
182
+ }
183
+
184
+ return conflicts;
185
+ }
186
+
187
+ // Create a new reservation
188
+ function create(owner, paths, options = {}) {
189
+ const {
190
+ ttl_seconds = 3600,
191
+ exclusive = true,
192
+ reason = null,
193
+ thread_id = null
194
+ } = options;
195
+
196
+ const scope = getScope();
197
+ const now = new Date();
198
+ const expires = new Date(now.getTime() + ttl_seconds * 1000);
199
+
200
+ // Check for conflicts
201
+ const conflicts = checkConflicts(paths, exclusive, scope);
202
+
203
+ const reservation = {
204
+ reservation_id: generateId(),
205
+ scope,
206
+ paths,
207
+ exclusive,
208
+ reason,
209
+ thread_id,
210
+ owner,
211
+ status: 'active',
212
+ ttl_seconds,
213
+ issued_ts: now.toISOString(),
214
+ expires_ts: expires.toISOString()
215
+ };
216
+
217
+ // Add to active
218
+ const active = readActive();
219
+ active.push(reservation);
220
+ writeActive(active);
221
+ updateIndex(active);
222
+
223
+ return {
224
+ reservation,
225
+ conflicts: conflicts.length > 0 ? conflicts : null,
226
+ warning: conflicts.length > 0 ? `${conflicts.length} conflicting reservation(s) found` : null
227
+ };
228
+ }
229
+
230
+ // Release a reservation
231
+ function release(reservation_id, owner = null) {
232
+ const active = readActive();
233
+ const index = active.findIndex(r => r.reservation_id === reservation_id);
234
+
235
+ if (index === -1) {
236
+ return { success: false, error: 'not_found', message: 'Reservation not found or already expired' };
237
+ }
238
+
239
+ const reservation = active[index];
240
+
241
+ // Optional owner check
242
+ if (owner && reservation.owner !== owner) {
243
+ return { success: false, error: 'not_owner', message: 'You are not the owner of this reservation' };
244
+ }
245
+
246
+ // Mark as released and move to history
247
+ reservation.status = 'released';
248
+ reservation.released_ts = new Date().toISOString();
249
+ appendHistory(reservation);
250
+
251
+ // Remove from active
252
+ active.splice(index, 1);
253
+ writeActive(active);
254
+ updateIndex(active);
255
+
256
+ return { success: true, reservation };
257
+ }
258
+
259
+ // List reservations
260
+ function list(options = {}) {
261
+ const { active_only = true, path_filter = null, scope_filter = null } = options;
262
+
263
+ let reservations = readActive();
264
+
265
+ // Filter by scope
266
+ if (scope_filter) {
267
+ reservations = reservations.filter(r => r.scope === scope_filter);
268
+ } else {
269
+ // Default: only show current scope
270
+ const currentScope = getScope();
271
+ reservations = reservations.filter(r => r.scope === currentScope);
272
+ }
273
+
274
+ // Filter by path
275
+ if (path_filter) {
276
+ reservations = reservations.filter(r =>
277
+ r.paths.some(p => p.includes(path_filter) || path_filter.includes(p))
278
+ );
279
+ }
280
+
281
+ return reservations;
282
+ }
283
+
284
+ // Get a single reservation by ID
285
+ function get(reservation_id) {
286
+ const active = readActive();
287
+ return active.find(r => r.reservation_id === reservation_id) || null;
288
+ }
289
+
290
+ // Cleanup expired reservations (called periodically)
291
+ function cleanup() {
292
+ const active = readActive();
293
+ const now = new Date().toISOString();
294
+ const expired = active.filter(r => r.expires_ts <= now);
295
+ const remaining = active.filter(r => r.expires_ts > now);
296
+
297
+ // Move expired to history
298
+ for (const r of expired) {
299
+ r.status = 'expired';
300
+ r.expired_ts = now;
301
+ appendHistory(r);
302
+ }
303
+
304
+ if (expired.length > 0) {
305
+ writeActive(remaining);
306
+ updateIndex(remaining);
307
+ }
308
+
309
+ return { expired: expired.length, remaining: remaining.length };
310
+ }
311
+
312
+ module.exports = {
313
+ create,
314
+ release,
315
+ list,
316
+ get,
317
+ getScope,
318
+ checkConflicts,
319
+ cleanup,
320
+ generateId
321
+ };
@@ -0,0 +1,378 @@
1
+ /**
2
+ * Skills Exchange Store — Marketplace for skills trading
3
+ *
4
+ * Manages skill offers and requests:
5
+ * - Users can offer skills they have (with expertise level)
6
+ * - Users can request skills they need (with urgency/project context)
7
+ * - Smart matching between skill offers and requests
8
+ * - Track successful exchanges and feedback
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const config = require('../config');
14
+ const userProfiles = require('./profiles');
15
+
16
+ const SKILLS_FILE = path.join(config.VIBE_DIR, 'skills.json');
17
+
18
+ // Core skills categories for structured matching
19
+ const SKILL_CATEGORIES = {
20
+ 'Engineering': ['frontend', 'backend', 'fullstack', 'mobile', 'devops', 'ai/ml', 'data', 'security'],
21
+ 'Design': ['ui/ux', 'visual', 'branding', 'illustration', 'animation', 'research'],
22
+ 'Business': ['marketing', 'sales', 'strategy', 'finance', 'operations', 'legal'],
23
+ 'Content': ['writing', 'copywriting', 'video', 'photography', 'podcast', 'social'],
24
+ 'Product': ['pm', 'growth', 'analytics', 'user-research', 'strategy'],
25
+ 'Other': ['consulting', 'mentoring', 'networking', 'feedback']
26
+ };
27
+
28
+ // Load skills data from disk
29
+ function loadSkillsData() {
30
+ try {
31
+ if (fs.existsSync(SKILLS_FILE)) {
32
+ const data = fs.readFileSync(SKILLS_FILE, 'utf8');
33
+ return JSON.parse(data);
34
+ }
35
+ } catch (e) {
36
+ console.warn('Failed to load skills data:', e.message);
37
+ }
38
+ return { offers: [], requests: [], exchanges: [] };
39
+ }
40
+
41
+ // Save skills data to disk
42
+ function saveSkillsData(data) {
43
+ try {
44
+ fs.writeFileSync(SKILLS_FILE, JSON.stringify(data, null, 2));
45
+ } catch (e) {
46
+ console.error('Failed to save skills data:', e.message);
47
+ }
48
+ }
49
+
50
+ // Add a skill offer
51
+ async function addSkillOffer(handle, skill, details = {}) {
52
+ const data = loadSkillsData();
53
+ const offer = {
54
+ id: Date.now().toString(),
55
+ handle: handle.toLowerCase().replace('@', ''),
56
+ skill: skill.toLowerCase(),
57
+ category: getCategoryForSkill(skill),
58
+ level: details.level || 'intermediate', // beginner, intermediate, expert
59
+ availability: details.availability || 'flexible', // flexible, limited, busy
60
+ format: details.format || ['chat', 'call'], // chat, call, pair, review
61
+ description: details.description || null,
62
+ examples: details.examples || [], // Links to work examples
63
+ timestamp: Date.now(),
64
+ status: 'active' // active, paused, fulfilled
65
+ };
66
+
67
+ // Remove duplicate offers for same skill
68
+ data.offers = data.offers.filter(o => !(o.handle === offer.handle && o.skill === offer.skill));
69
+ data.offers.unshift(offer);
70
+
71
+ saveSkillsData(data);
72
+ return offer;
73
+ }
74
+
75
+ // Add a skill request
76
+ async function addSkillRequest(handle, skill, details = {}) {
77
+ const data = loadSkillsData();
78
+ const request = {
79
+ id: Date.now().toString(),
80
+ handle: handle.toLowerCase().replace('@', ''),
81
+ skill: skill.toLowerCase(),
82
+ category: getCategoryForSkill(skill),
83
+ urgency: details.urgency || 'medium', // low, medium, high
84
+ context: details.context || null, // What project/goal
85
+ format: details.format || ['chat', 'call'], // Preferred format
86
+ description: details.description || null,
87
+ timestamp: Date.now(),
88
+ status: 'active' // active, paused, fulfilled
89
+ };
90
+
91
+ // Remove duplicate requests for same skill
92
+ data.requests = data.requests.filter(r => !(r.handle === request.handle && r.skill === request.skill));
93
+ data.requests.unshift(request);
94
+
95
+ saveSkillsData(data);
96
+ return request;
97
+ }
98
+
99
+ // Get all active skill offers
100
+ async function getSkillOffers(skillFilter = null) {
101
+ const data = loadSkillsData();
102
+ let offers = data.offers.filter(o => o.status === 'active');
103
+
104
+ if (skillFilter) {
105
+ const filter = skillFilter.toLowerCase();
106
+ offers = offers.filter(o =>
107
+ o.skill.includes(filter) ||
108
+ o.category.toLowerCase().includes(filter) ||
109
+ o.description?.toLowerCase().includes(filter)
110
+ );
111
+ }
112
+
113
+ return offers;
114
+ }
115
+
116
+ // Get all active skill requests
117
+ async function getSkillRequests(skillFilter = null) {
118
+ const data = loadSkillsData();
119
+ let requests = data.requests.filter(r => r.status === 'active');
120
+
121
+ if (skillFilter) {
122
+ const filter = skillFilter.toLowerCase();
123
+ requests = requests.filter(r =>
124
+ r.skill.includes(filter) ||
125
+ r.category.toLowerCase().includes(filter) ||
126
+ r.description?.toLowerCase().includes(filter) ||
127
+ r.context?.toLowerCase().includes(filter)
128
+ );
129
+ }
130
+
131
+ return requests;
132
+ }
133
+
134
+ // Get user's skill offers
135
+ async function getUserOffers(handle) {
136
+ const data = loadSkillsData();
137
+ const key = handle.toLowerCase().replace('@', '');
138
+ return data.offers.filter(o => o.handle === key && o.status === 'active');
139
+ }
140
+
141
+ // Get user's skill requests
142
+ async function getUserRequests(handle) {
143
+ const data = loadSkillsData();
144
+ const key = handle.toLowerCase().replace('@', '');
145
+ return data.requests.filter(r => r.handle === key && r.status === 'active');
146
+ }
147
+
148
+ // Find matches between offers and requests
149
+ async function findSkillMatches(handle) {
150
+ const data = loadSkillsData();
151
+ const key = handle.toLowerCase().replace('@', '');
152
+
153
+ // Get user's requests and find matching offers
154
+ const myRequests = data.requests.filter(r => r.handle === key && r.status === 'active');
155
+ const myOffers = data.offers.filter(o => o.handle === key && o.status === 'active');
156
+
157
+ const matches = {
158
+ forMyRequests: [], // People who can help me
159
+ forMyOffers: [] // People I can help
160
+ };
161
+
162
+ // Find offers that match my requests
163
+ for (const request of myRequests) {
164
+ const matchingOffers = data.offers.filter(o =>
165
+ o.handle !== key &&
166
+ o.status === 'active' &&
167
+ skillsMatch(request.skill, o.skill)
168
+ );
169
+
170
+ for (const offer of matchingOffers) {
171
+ matches.forMyRequests.push({
172
+ type: 'offer',
173
+ myItem: request,
174
+ theirItem: offer,
175
+ score: calculateSkillMatchScore(request, offer)
176
+ });
177
+ }
178
+ }
179
+
180
+ // Find requests that match my offers
181
+ for (const offer of myOffers) {
182
+ const matchingRequests = data.requests.filter(r =>
183
+ r.handle !== key &&
184
+ r.status === 'active' &&
185
+ skillsMatch(offer.skill, r.skill)
186
+ );
187
+
188
+ for (const request of matchingRequests) {
189
+ matches.forMyOffers.push({
190
+ type: 'request',
191
+ myItem: offer,
192
+ theirItem: request,
193
+ score: calculateSkillMatchScore(request, offer)
194
+ });
195
+ }
196
+ }
197
+
198
+ // Sort by match score
199
+ matches.forMyRequests.sort((a, b) => b.score - a.score);
200
+ matches.forMyOffers.sort((a, b) => b.score - a.score);
201
+
202
+ return matches;
203
+ }
204
+
205
+ // Check if two skills match
206
+ function skillsMatch(skill1, skill2) {
207
+ const s1 = skill1.toLowerCase();
208
+ const s2 = skill2.toLowerCase();
209
+
210
+ // Exact match
211
+ if (s1 === s2) return true;
212
+
213
+ // Partial match (one contains the other)
214
+ if (s1.includes(s2) || s2.includes(s1)) return true;
215
+
216
+ // Synonym matching
217
+ const synonyms = {
218
+ 'frontend': ['frontend', 'front-end', 'ui', 'react', 'vue', 'angular'],
219
+ 'backend': ['backend', 'back-end', 'server', 'api', 'node', 'python'],
220
+ 'design': ['design', 'ui/ux', 'ux', 'ui', 'visual'],
221
+ 'marketing': ['marketing', 'growth', 'seo', 'ads'],
222
+ 'ai': ['ai', 'ml', 'machine learning', 'ai/ml'],
223
+ 'mobile': ['mobile', 'ios', 'android', 'react native', 'flutter']
224
+ };
225
+
226
+ for (const [base, variants] of Object.entries(synonyms)) {
227
+ if (variants.includes(s1) && variants.includes(s2)) {
228
+ return true;
229
+ }
230
+ }
231
+
232
+ return false;
233
+ }
234
+
235
+ // Calculate match score between request and offer
236
+ function calculateSkillMatchScore(request, offer) {
237
+ let score = 50; // Base score
238
+
239
+ // Skill similarity
240
+ if (request.skill === offer.skill) {
241
+ score += 30;
242
+ } else if (skillsMatch(request.skill, offer.skill)) {
243
+ score += 20;
244
+ }
245
+
246
+ // Format compatibility
247
+ const formatOverlap = request.format.filter(f => offer.format.includes(f));
248
+ score += formatOverlap.length * 10;
249
+
250
+ // Urgency vs availability
251
+ if (request.urgency === 'high' && offer.availability === 'flexible') {
252
+ score += 15;
253
+ } else if (request.urgency === 'low' && offer.availability === 'busy') {
254
+ score -= 10;
255
+ }
256
+
257
+ // Level appropriateness (expert offers are good for all requests)
258
+ if (offer.level === 'expert') {
259
+ score += 10;
260
+ }
261
+
262
+ return score;
263
+ }
264
+
265
+ // Get skill category for a skill
266
+ function getCategoryForSkill(skill) {
267
+ const skillLower = skill.toLowerCase();
268
+
269
+ for (const [category, skills] of Object.entries(SKILL_CATEGORIES)) {
270
+ for (const s of skills) {
271
+ if (skillLower.includes(s) || s.includes(skillLower)) {
272
+ return category;
273
+ }
274
+ }
275
+ }
276
+
277
+ return 'Other';
278
+ }
279
+
280
+ // Get skills by category
281
+ async function getSkillsByCategory() {
282
+ const [offers, requests] = await Promise.all([getSkillOffers(), getSkillRequests()]);
283
+
284
+ const categories = {};
285
+
286
+ // Group offers by category
287
+ for (const offer of offers) {
288
+ if (!categories[offer.category]) {
289
+ categories[offer.category] = { offers: [], requests: [] };
290
+ }
291
+ categories[offer.category].offers.push(offer);
292
+ }
293
+
294
+ // Group requests by category
295
+ for (const request of requests) {
296
+ if (!categories[request.category]) {
297
+ categories[request.category] = { offers: [], requests: [] };
298
+ }
299
+ categories[request.category].requests.push(request);
300
+ }
301
+
302
+ return categories;
303
+ }
304
+
305
+ // Record successful skill exchange
306
+ async function recordExchange(fromHandle, toHandle, skill, details = {}) {
307
+ const data = loadSkillsData();
308
+ const exchange = {
309
+ id: Date.now().toString(),
310
+ from: fromHandle.toLowerCase().replace('@', ''),
311
+ to: toHandle.toLowerCase().replace('@', ''),
312
+ skill,
313
+ timestamp: Date.now(),
314
+ status: 'completed', // initiated, in-progress, completed, cancelled
315
+ rating: details.rating || null, // 1-5 stars
316
+ feedback: details.feedback || null
317
+ };
318
+
319
+ data.exchanges.push(exchange);
320
+ saveSkillsData(data);
321
+
322
+ return exchange;
323
+ }
324
+
325
+ // Get exchange statistics
326
+ async function getExchangeStats() {
327
+ const data = loadSkillsData();
328
+ const stats = {
329
+ totalOffers: data.offers.length,
330
+ totalRequests: data.requests.length,
331
+ totalExchanges: data.exchanges.length,
332
+ activeOffers: data.offers.filter(o => o.status === 'active').length,
333
+ activeRequests: data.requests.filter(r => r.status === 'active').length,
334
+ topSkills: {}
335
+ };
336
+
337
+ // Count most popular skills
338
+ const skillCount = {};
339
+ [...data.offers, ...data.requests].forEach(item => {
340
+ skillCount[item.skill] = (skillCount[item.skill] || 0) + 1;
341
+ });
342
+
343
+ stats.topSkills = Object.entries(skillCount)
344
+ .sort(([,a], [,b]) => b - a)
345
+ .slice(0, 10)
346
+ .map(([skill, count]) => ({ skill, count }));
347
+
348
+ return stats;
349
+ }
350
+
351
+ // Remove offer or request
352
+ async function removeSkillItem(handle, id, type) {
353
+ const data = loadSkillsData();
354
+ const key = handle.toLowerCase().replace('@', '');
355
+
356
+ if (type === 'offer') {
357
+ data.offers = data.offers.filter(o => !(o.handle === key && o.id === id));
358
+ } else if (type === 'request') {
359
+ data.requests = data.requests.filter(r => !(r.handle === key && r.id === id));
360
+ }
361
+
362
+ saveSkillsData(data);
363
+ }
364
+
365
+ module.exports = {
366
+ SKILL_CATEGORIES,
367
+ addSkillOffer,
368
+ addSkillRequest,
369
+ getSkillOffers,
370
+ getSkillRequests,
371
+ getUserOffers,
372
+ getUserRequests,
373
+ findSkillMatches,
374
+ getSkillsByCategory,
375
+ recordExchange,
376
+ getExchangeStats,
377
+ removeSkillItem
378
+ };