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.
- package/README.md +1 -0
- package/analytics.js +107 -0
- package/config.js +174 -3
- package/index.js +163 -34
- package/intelligence/index.js +45 -0
- package/intelligence/infer.js +316 -0
- package/intelligence/interests.js +369 -0
- package/intelligence/patterns.js +651 -0
- package/intelligence/proactive.js +358 -0
- package/intelligence/serendipity.js +306 -0
- package/notification-emitter.js +77 -0
- package/notify.js +141 -18
- package/package.json +14 -6
- package/presence.js +5 -1
- package/protocol/index.js +88 -1
- package/protocol/telegram-commands.js +199 -0
- package/store/api.js +469 -29
- package/store/index.js +7 -7
- package/store/local.js +67 -11
- package/store/profiles.js +435 -0
- package/store/reservations.js +321 -0
- package/store/skills.js +378 -0
- package/tools/_actions.js +491 -22
- package/tools/_connection-queue.js +257 -0
- package/tools/_discovery-enhanced.js +290 -0
- package/tools/_discovery.js +439 -0
- package/tools/_proactive-discovery.js +301 -0
- package/tools/_shared/index.js +64 -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/admin-inbox.js +218 -0
- package/tools/agent-treasury.js +288 -0
- package/tools/agents.js +122 -0
- package/tools/analytics.js +191 -0
- package/tools/approve.js +197 -0
- package/tools/arcade.js +173 -0
- package/tools/artifact-create.js +247 -0
- package/tools/artifact-view.js +174 -0
- package/tools/artifacts-price.js +107 -0
- package/tools/ask-expert.js +160 -0
- package/tools/auto-suggest-connections.js +304 -0
- package/tools/away.js +68 -0
- package/tools/back.js +51 -0
- package/tools/become-expert.js +150 -0
- package/tools/bootstrap-skills.js +231 -0
- package/tools/bridge-dashboard.js +342 -0
- package/tools/bridge-health.js +400 -0
- package/tools/bridge-live.js +384 -0
- package/tools/bridges.js +383 -0
- package/tools/broadcast.js +286 -0
- package/tools/bye.js +4 -0
- package/tools/chat.js +202 -0
- package/tools/collaborative-drawing.js +286 -0
- package/tools/colorguess.js +281 -0
- package/tools/crossword.js +369 -0
- package/tools/discover-insights.js +379 -0
- package/tools/discover-momentum.js +256 -0
- package/tools/discover.js +675 -0
- package/tools/discovery-analytics.js +345 -0
- package/tools/discovery-auto-suggest.js +275 -0
- package/tools/discovery-bootstrap.js +267 -0
- package/tools/discovery-daily.js +375 -0
- package/tools/discovery-dashboard.js +385 -0
- package/tools/discovery-digest.js +314 -0
- package/tools/discovery-hub.js +357 -0
- package/tools/discovery-insights.js +384 -0
- package/tools/discovery-momentum.js +281 -0
- package/tools/discovery-monitor.js +319 -0
- package/tools/discovery-proactive.js +300 -0
- package/tools/dm.js +84 -14
- package/tools/draw.js +317 -0
- package/tools/drawing.js +310 -0
- package/tools/earnings.js +126 -0
- package/tools/echo.js +16 -0
- package/tools/farcaster.js +307 -0
- package/tools/feed.js +215 -0
- package/tools/follow.js +224 -0
- package/tools/friends.js +192 -0
- package/tools/game.js +218 -110
- package/tools/games-catalog.js +376 -0
- package/tools/games.js +313 -0
- package/tools/genesis.js +233 -0
- package/tools/gig-browse.js +206 -0
- package/tools/gig-complete.js +139 -0
- package/tools/guessnumber.js +194 -0
- package/tools/hangman.js +129 -0
- package/tools/help.js +269 -0
- package/tools/idea.js +217 -0
- package/tools/inbox.js +291 -25
- package/tools/init.js +657 -33
- package/tools/insights.js +123 -0
- package/tools/invite.js +142 -21
- package/tools/l2-bridge.js +272 -0
- package/tools/l2-status.js +217 -0
- package/tools/l2.js +206 -0
- package/tools/migrate.js +156 -0
- package/tools/mint.js +377 -0
- package/tools/multiplayer-game.js +275 -0
- package/tools/multiplayer-tictactoe.js +303 -0
- package/tools/mute.js +97 -0
- package/tools/notifications.js +415 -0
- package/tools/observe.js +200 -0
- package/tools/onboarding.js +147 -0
- package/tools/open.js +52 -3
- package/tools/party-game.js +314 -0
- package/tools/plan.js +225 -0
- package/tools/presence-agent.js +167 -0
- package/tools/profile.js +219 -0
- package/tools/proof-of-work.js +139 -0
- package/tools/pulse.js +218 -0
- package/tools/react.js +4 -0
- package/tools/release.js +83 -0
- package/tools/report.js +109 -0
- package/tools/reputation.js +175 -0
- package/tools/request.js +231 -0
- package/tools/reservations.js +116 -0
- package/tools/reserve.js +111 -0
- package/tools/riddle.js +240 -0
- package/tools/run-bootstrap.js +69 -0
- package/tools/schedule.js +367 -0
- package/tools/session.js +420 -0
- package/tools/session_price.js +128 -0
- package/tools/settings.js +200 -0
- package/tools/ship.js +188 -0
- package/tools/shipback.js +326 -0
- package/tools/skills-analytics.js +349 -0
- package/tools/skills-bootstrap.js +301 -0
- package/tools/skills-dashboard.js +268 -0
- package/tools/skills-exchange.js +342 -0
- package/tools/skills.js +380 -0
- package/tools/smart-intro.js +353 -0
- package/tools/social-inbox.js +326 -69
- package/tools/social-post.js +251 -66
- package/tools/social-processor.js +445 -0
- package/tools/solo-game.js +390 -0
- package/tools/start.js +296 -81
- package/tools/status.js +53 -6
- package/tools/storybuilder.js +331 -0
- package/tools/stuck.js +297 -0
- package/tools/subscribe.js +148 -0
- package/tools/subscriptions.js +134 -0
- package/tools/suggest-tags.js +184 -0
- package/tools/tag-suggestions.js +257 -0
- package/tools/telegram-bot.js +183 -0
- package/tools/telegram-setup.js +214 -0
- package/tools/tictactoe.js +155 -0
- package/tools/tip.js +120 -0
- package/tools/token.js +103 -0
- package/tools/twentyquestions.js +143 -0
- package/tools/update.js +1 -1
- package/tools/wallet.js +127 -0
- package/tools/watch.js +157 -0
- package/tools/webhook-test.js +388 -0
- package/tools/who.js +118 -25
- package/tools/withdraw.js +145 -0
- package/tools/wordassociation.js +247 -0
- package/tools/work-summary.js +96 -0
- package/tools/workshop-buddy.js +394 -0
- package/tools/workshop.js +327 -0
- package/version.json +12 -3
- 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
|
+
};
|
package/store/skills.js
ADDED
|
@@ -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
|
+
};
|