slashvibe-mcp 0.3.21 → 0.3.23
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/LICENSE +21 -0
- package/README.md +280 -47
- package/auto-update.js +10 -15
- package/config.js +36 -31
- package/crypto.js +1 -6
- package/debug.js +12 -0
- package/discord.js +19 -19
- package/eslint.config.js +54 -0
- package/index.js +217 -207
- package/intelligence/index.js +2 -9
- package/intelligence/infer.js +10 -16
- package/intelligence/patterns.js +23 -18
- package/intelligence/proactive.js +16 -15
- package/intelligence/serendipity.js +57 -20
- package/memory.js +13 -8
- package/migrate-v2.js +72 -0
- package/notification-emitter.js +2 -2
- package/notify.js +39 -14
- package/package.json +28 -29
- package/post-install.js +141 -0
- package/presence.js +2 -2
- package/prompts.js +5 -9
- package/protocol/index.js +123 -87
- package/protocol/telegram-commands.js +36 -37
- package/store/api.js +358 -529
- package/store/local.js +9 -10
- package/store/profiles.js +48 -192
- package/store/reservations.js +2 -9
- package/store/skills.js +69 -71
- package/store/sqlite.js +355 -0
- package/test-skills-bootstrap.js +20 -0
- package/test-v2-integration.js +385 -0
- package/tools/_actions.js +48 -387
- package/tools/_connection-queue.js +45 -56
- package/tools/_discovery-enhanced.js +52 -57
- package/tools/_discovery.js +87 -185
- package/tools/{l2-status.js → _experimental/l2-status.js} +68 -70
- package/tools/{shipback.js → _experimental/shipback.js} +4 -3
- package/tools/_proactive-discovery.js +60 -73
- package/tools/_shared/index.js +41 -64
- package/tools/admin-inbox.js +10 -15
- package/tools/agents.js +1 -1
- package/tools/artifact-create.js +13 -23
- package/tools/artifact-view.js +4 -4
- package/tools/{_deprecated/back.js → back.js} +1 -1
- package/tools/bye.js +3 -5
- package/tools/consent.js +2 -2
- package/tools/context.js +9 -10
- package/tools/crossword.js +3 -2
- package/tools/discover.js +94 -356
- package/tools/dm.js +27 -86
- package/tools/doctor.js +12 -41
- package/tools/drawing.js +34 -20
- package/tools/echo.js +11 -11
- package/tools/feed.js +30 -58
- package/tools/follow.js +64 -187
- package/tools/{_deprecated/forget.js → forget.js} +4 -7
- package/tools/game.js +144 -48
- package/tools/handoff.js +6 -8
- package/tools/help.js +3 -3
- package/tools/idea.js +15 -27
- package/tools/inbox.js +121 -293
- package/tools/init.js +54 -151
- package/tools/invite.js +8 -21
- package/tools/migrate.js +27 -24
- package/tools/multiplayer-game.js +50 -40
- package/tools/{_deprecated/mute.js → mute.js} +4 -3
- package/tools/notifications.js +58 -48
- package/tools/observe.js +12 -15
- package/tools/onboarding.js +8 -11
- package/tools/open.js +13 -144
- package/tools/party-game.js +23 -12
- package/tools/patterns.js +2 -1
- package/tools/ping.js +5 -7
- package/tools/react.js +28 -30
- package/tools/{_deprecated/recall.js → recall.js} +5 -10
- package/tools/release.js +4 -2
- package/tools/{_deprecated/remember.js → remember.js} +4 -6
- package/tools/report.js +2 -2
- package/tools/request.js +6 -26
- package/tools/reserve.js +1 -1
- package/tools/session-fork.js +97 -0
- package/tools/session-save.js +109 -0
- package/tools/settings.js +30 -99
- package/tools/ship.js +74 -56
- package/tools/{_deprecated/skills-exchange.js → skills-exchange.js} +38 -39
- package/tools/social-inbox.js +22 -28
- package/tools/social-post.js +24 -27
- package/tools/solo-game.js +54 -46
- package/tools/start.js +14 -148
- package/tools/status.js +21 -68
- package/tools/submit.js +4 -2
- package/tools/suggest-tags.js +36 -33
- package/tools/summarize.js +19 -16
- package/tools/tag-suggestions.js +72 -73
- package/tools/test.js +1 -1
- package/tools/{_deprecated/tictactoe.js → tictactoe.js} +26 -26
- package/tools/token.js +4 -4
- package/tools/update.js +1 -2
- package/tools/watch.js +132 -112
- package/tools/who.js +20 -40
- package/tools/{_deprecated/wordassociation.js → wordassociation.js} +23 -20
- package/tools/workshop-buddy.js +52 -53
- package/tools/x-mentions.js +0 -1
- package/tools/x-reply.js +0 -1
- package/twitter.js +14 -20
- package/version.json +8 -10
- package/webhook-runner.js +132 -0
- package/auth-store.js +0 -148
- package/bridges/bridge-monitor.js +0 -388
- package/bridges/discord-bot.js +0 -431
- package/bridges/farcaster.js +0 -299
- package/bridges/telegram.js +0 -261
- package/bridges/webhook-health.js +0 -420
- package/bridges/webhook-server.js +0 -437
- package/bridges/whatsapp.js +0 -441
- package/bridges/x-webhook.js +0 -423
- package/games/arcade.js +0 -406
- package/games/chess.js +0 -451
- package/games/colorguess.js +0 -343
- package/games/crossword-words.js +0 -171
- package/games/crossword.js +0 -461
- package/games/drawing.js +0 -347
- package/games/gameroulette.js +0 -300
- package/games/gamerouter.js +0 -336
- package/games/gamestatus.js +0 -337
- package/games/guessnumber.js +0 -209
- package/games/hangman.js +0 -279
- package/games/memory.js +0 -338
- package/games/multiplayer-tictactoe.js +0 -389
- package/games/pixelart.js +0 -399
- package/games/quickduel.js +0 -354
- package/games/riddle.js +0 -371
- package/games/rockpaperscissors.js +0 -291
- package/games/snake.js +0 -406
- package/games/storybuilder.js +0 -343
- package/games/tictactoe.js +0 -345
- package/games/twentyquestions.js +0 -286
- package/games/twotruths.js +0 -207
- package/games/werewolf.js +0 -508
- package/games/wordassociation.js +0 -247
- package/games/wordchain.js +0 -135
- package/intelligence/interests.js +0 -369
- package/setup.js +0 -480
- package/smart-inbox.js +0 -276
- package/tools/_deprecated/auto-suggest-connections.js +0 -304
- package/tools/_deprecated/bootstrap-skills.js +0 -231
- package/tools/_deprecated/bridge-dashboard.js +0 -342
- package/tools/_deprecated/bridge-health.js +0 -400
- package/tools/_deprecated/bridge-live.js +0 -384
- package/tools/_deprecated/bridges.js +0 -383
- package/tools/_deprecated/colorguess.js +0 -281
- package/tools/_deprecated/discover-insights.js +0 -379
- package/tools/_deprecated/discover-momentum.js +0 -256
- package/tools/_deprecated/discovery-analytics.js +0 -345
- package/tools/_deprecated/discovery-auto-suggest.js +0 -275
- package/tools/_deprecated/discovery-bootstrap.js +0 -267
- package/tools/_deprecated/discovery-daily.js +0 -375
- package/tools/_deprecated/discovery-dashboard.js +0 -385
- package/tools/_deprecated/discovery-digest.js +0 -314
- package/tools/_deprecated/discovery-hub.js +0 -357
- package/tools/_deprecated/discovery-insights.js +0 -384
- package/tools/_deprecated/discovery-momentum.js +0 -281
- package/tools/_deprecated/discovery-monitor.js +0 -319
- package/tools/_deprecated/discovery-proactive.js +0 -300
- package/tools/_deprecated/draw.js +0 -317
- package/tools/_deprecated/farcaster.js +0 -307
- package/tools/_deprecated/games-catalog.js +0 -376
- package/tools/_deprecated/games.js +0 -313
- package/tools/_deprecated/guessnumber.js +0 -194
- package/tools/_deprecated/hangman.js +0 -129
- package/tools/_deprecated/multiplayer-tictactoe.js +0 -303
- package/tools/_deprecated/riddle.js +0 -240
- package/tools/_deprecated/run-bootstrap.js +0 -69
- package/tools/_deprecated/skills-analytics.js +0 -349
- package/tools/_deprecated/skills-bootstrap.js +0 -301
- package/tools/_deprecated/skills-dashboard.js +0 -268
- package/tools/_deprecated/skills.js +0 -380
- package/tools/_deprecated/smart-intro.js +0 -353
- package/tools/_deprecated/storybuilder.js +0 -331
- package/tools/_deprecated/telegram-bot.js +0 -183
- package/tools/_deprecated/telegram-setup.js +0 -214
- package/tools/_deprecated/twentyquestions.js +0 -143
- package/tools/_shared.js +0 -234
- package/tools/_work-context.js +0 -338
- package/tools/_work-context.manual-test.js +0 -199
- package/tools/_work-context.test.js +0 -260
- package/tools/activity.js +0 -220
- package/tools/agent-treasury.js +0 -288
- package/tools/analytics.js +0 -191
- package/tools/approve.js +0 -197
- package/tools/arcade.js +0 -173
- package/tools/artifacts-price.js +0 -107
- package/tools/ask-expert.js +0 -160
- package/tools/available.js +0 -120
- package/tools/become-expert.js +0 -150
- package/tools/broadcast.js +0 -325
- package/tools/chat.js +0 -202
- package/tools/collaborative-drawing.js +0 -286
- package/tools/connection-status.js +0 -178
- package/tools/earnings.js +0 -126
- package/tools/friends.js +0 -207
- package/tools/genesis.js +0 -233
- package/tools/gig-browse.js +0 -206
- package/tools/gig-complete.js +0 -144
- package/tools/health.js +0 -87
- package/tools/leaderboard.js +0 -117
- package/tools/lib/git-apply.js +0 -206
- package/tools/lib/git-bundle.js +0 -407
- package/tools/mint.js +0 -377
- package/tools/plan.js +0 -225
- package/tools/profile.js +0 -219
- package/tools/proof-of-work.js +0 -144
- package/tools/pulse.js +0 -218
- package/tools/reply.js +0 -166
- package/tools/reputation.js +0 -175
- package/tools/schedule.js +0 -367
- package/tools/search-messages.js +0 -123
- package/tools/session.js +0 -467
- package/tools/session_price.js +0 -128
- package/tools/smart-check.js +0 -201
- package/tools/social-processor.js +0 -445
- package/tools/streak.js +0 -147
- package/tools/stuck.js +0 -297
- package/tools/subscribe.js +0 -148
- package/tools/subscriptions.js +0 -134
- package/tools/tip.js +0 -193
- package/tools/wallet.js +0 -269
- package/tools/webhook-test.js +0 -388
- package/tools/withdraw.js +0 -145
- package/tools/work-summary.js +0 -96
- package/tools/workshop.js +0 -327
- /package/tools/{l2-bridge.js → _experimental/l2-bridge.js} +0 -0
- /package/tools/{l2.js → _experimental/l2.js} +0 -0
- /package/tools/{_deprecated/away.js → away.js} +0 -0
package/store/skills.js
CHANGED
|
@@ -17,12 +17,12 @@ const SKILLS_FILE = path.join(config.VIBE_DIR, 'skills.json');
|
|
|
17
17
|
|
|
18
18
|
// Core skills categories for structured matching
|
|
19
19
|
const SKILL_CATEGORIES = {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
26
|
};
|
|
27
27
|
|
|
28
28
|
// Load skills data from disk
|
|
@@ -63,11 +63,11 @@ async function addSkillOffer(handle, skill, details = {}) {
|
|
|
63
63
|
timestamp: Date.now(),
|
|
64
64
|
status: 'active' // active, paused, fulfilled
|
|
65
65
|
};
|
|
66
|
-
|
|
66
|
+
|
|
67
67
|
// Remove duplicate offers for same skill
|
|
68
68
|
data.offers = data.offers.filter(o => !(o.handle === offer.handle && o.skill === offer.skill));
|
|
69
69
|
data.offers.unshift(offer);
|
|
70
|
-
|
|
70
|
+
|
|
71
71
|
saveSkillsData(data);
|
|
72
72
|
return offer;
|
|
73
73
|
}
|
|
@@ -87,11 +87,11 @@ async function addSkillRequest(handle, skill, details = {}) {
|
|
|
87
87
|
timestamp: Date.now(),
|
|
88
88
|
status: 'active' // active, paused, fulfilled
|
|
89
89
|
};
|
|
90
|
-
|
|
90
|
+
|
|
91
91
|
// Remove duplicate requests for same skill
|
|
92
92
|
data.requests = data.requests.filter(r => !(r.handle === request.handle && r.skill === request.skill));
|
|
93
93
|
data.requests.unshift(request);
|
|
94
|
-
|
|
94
|
+
|
|
95
95
|
saveSkillsData(data);
|
|
96
96
|
return request;
|
|
97
97
|
}
|
|
@@ -100,16 +100,17 @@ async function addSkillRequest(handle, skill, details = {}) {
|
|
|
100
100
|
async function getSkillOffers(skillFilter = null) {
|
|
101
101
|
const data = loadSkillsData();
|
|
102
102
|
let offers = data.offers.filter(o => o.status === 'active');
|
|
103
|
-
|
|
103
|
+
|
|
104
104
|
if (skillFilter) {
|
|
105
105
|
const filter = skillFilter.toLowerCase();
|
|
106
|
-
offers = offers.filter(
|
|
107
|
-
o
|
|
108
|
-
|
|
109
|
-
|
|
106
|
+
offers = offers.filter(
|
|
107
|
+
o =>
|
|
108
|
+
o.skill.includes(filter) ||
|
|
109
|
+
o.category.toLowerCase().includes(filter) ||
|
|
110
|
+
o.description?.toLowerCase().includes(filter)
|
|
110
111
|
);
|
|
111
112
|
}
|
|
112
|
-
|
|
113
|
+
|
|
113
114
|
return offers;
|
|
114
115
|
}
|
|
115
116
|
|
|
@@ -117,17 +118,18 @@ async function getSkillOffers(skillFilter = null) {
|
|
|
117
118
|
async function getSkillRequests(skillFilter = null) {
|
|
118
119
|
const data = loadSkillsData();
|
|
119
120
|
let requests = data.requests.filter(r => r.status === 'active');
|
|
120
|
-
|
|
121
|
+
|
|
121
122
|
if (skillFilter) {
|
|
122
123
|
const filter = skillFilter.toLowerCase();
|
|
123
|
-
requests = requests.filter(
|
|
124
|
-
r
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
124
|
+
requests = requests.filter(
|
|
125
|
+
r =>
|
|
126
|
+
r.skill.includes(filter) ||
|
|
127
|
+
r.category.toLowerCase().includes(filter) ||
|
|
128
|
+
r.description?.toLowerCase().includes(filter) ||
|
|
129
|
+
r.context?.toLowerCase().includes(filter)
|
|
128
130
|
);
|
|
129
131
|
}
|
|
130
|
-
|
|
132
|
+
|
|
131
133
|
return requests;
|
|
132
134
|
}
|
|
133
135
|
|
|
@@ -149,24 +151,22 @@ async function getUserRequests(handle) {
|
|
|
149
151
|
async function findSkillMatches(handle) {
|
|
150
152
|
const data = loadSkillsData();
|
|
151
153
|
const key = handle.toLowerCase().replace('@', '');
|
|
152
|
-
|
|
154
|
+
|
|
153
155
|
// Get user's requests and find matching offers
|
|
154
156
|
const myRequests = data.requests.filter(r => r.handle === key && r.status === 'active');
|
|
155
157
|
const myOffers = data.offers.filter(o => o.handle === key && o.status === 'active');
|
|
156
|
-
|
|
158
|
+
|
|
157
159
|
const matches = {
|
|
158
160
|
forMyRequests: [], // People who can help me
|
|
159
|
-
forMyOffers: []
|
|
161
|
+
forMyOffers: [] // People I can help
|
|
160
162
|
};
|
|
161
|
-
|
|
163
|
+
|
|
162
164
|
// Find offers that match my requests
|
|
163
165
|
for (const request of myRequests) {
|
|
164
|
-
const matchingOffers = data.offers.filter(
|
|
165
|
-
o.handle !== key &&
|
|
166
|
-
o.status === 'active' &&
|
|
167
|
-
skillsMatch(request.skill, o.skill)
|
|
166
|
+
const matchingOffers = data.offers.filter(
|
|
167
|
+
o => o.handle !== key && o.status === 'active' && skillsMatch(request.skill, o.skill)
|
|
168
168
|
);
|
|
169
|
-
|
|
169
|
+
|
|
170
170
|
for (const offer of matchingOffers) {
|
|
171
171
|
matches.forMyRequests.push({
|
|
172
172
|
type: 'offer',
|
|
@@ -176,15 +176,13 @@ async function findSkillMatches(handle) {
|
|
|
176
176
|
});
|
|
177
177
|
}
|
|
178
178
|
}
|
|
179
|
-
|
|
179
|
+
|
|
180
180
|
// Find requests that match my offers
|
|
181
181
|
for (const offer of myOffers) {
|
|
182
|
-
const matchingRequests = data.requests.filter(
|
|
183
|
-
r.handle !== key &&
|
|
184
|
-
r.status === 'active' &&
|
|
185
|
-
skillsMatch(offer.skill, r.skill)
|
|
182
|
+
const matchingRequests = data.requests.filter(
|
|
183
|
+
r => r.handle !== key && r.status === 'active' && skillsMatch(offer.skill, r.skill)
|
|
186
184
|
);
|
|
187
|
-
|
|
185
|
+
|
|
188
186
|
for (const request of matchingRequests) {
|
|
189
187
|
matches.forMyOffers.push({
|
|
190
188
|
type: 'request',
|
|
@@ -194,11 +192,11 @@ async function findSkillMatches(handle) {
|
|
|
194
192
|
});
|
|
195
193
|
}
|
|
196
194
|
}
|
|
197
|
-
|
|
195
|
+
|
|
198
196
|
// Sort by match score
|
|
199
197
|
matches.forMyRequests.sort((a, b) => b.score - a.score);
|
|
200
198
|
matches.forMyOffers.sort((a, b) => b.score - a.score);
|
|
201
|
-
|
|
199
|
+
|
|
202
200
|
return matches;
|
|
203
201
|
}
|
|
204
202
|
|
|
@@ -206,66 +204,66 @@ async function findSkillMatches(handle) {
|
|
|
206
204
|
function skillsMatch(skill1, skill2) {
|
|
207
205
|
const s1 = skill1.toLowerCase();
|
|
208
206
|
const s2 = skill2.toLowerCase();
|
|
209
|
-
|
|
207
|
+
|
|
210
208
|
// Exact match
|
|
211
209
|
if (s1 === s2) return true;
|
|
212
|
-
|
|
210
|
+
|
|
213
211
|
// Partial match (one contains the other)
|
|
214
212
|
if (s1.includes(s2) || s2.includes(s1)) return true;
|
|
215
|
-
|
|
213
|
+
|
|
216
214
|
// Synonym matching
|
|
217
215
|
const synonyms = {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
216
|
+
frontend: ['frontend', 'front-end', 'ui', 'react', 'vue', 'angular'],
|
|
217
|
+
backend: ['backend', 'back-end', 'server', 'api', 'node', 'python'],
|
|
218
|
+
design: ['design', 'ui/ux', 'ux', 'ui', 'visual'],
|
|
219
|
+
marketing: ['marketing', 'growth', 'seo', 'ads'],
|
|
220
|
+
ai: ['ai', 'ml', 'machine learning', 'ai/ml'],
|
|
221
|
+
mobile: ['mobile', 'ios', 'android', 'react native', 'flutter']
|
|
224
222
|
};
|
|
225
|
-
|
|
223
|
+
|
|
226
224
|
for (const [base, variants] of Object.entries(synonyms)) {
|
|
227
225
|
if (variants.includes(s1) && variants.includes(s2)) {
|
|
228
226
|
return true;
|
|
229
227
|
}
|
|
230
228
|
}
|
|
231
|
-
|
|
229
|
+
|
|
232
230
|
return false;
|
|
233
231
|
}
|
|
234
232
|
|
|
235
233
|
// Calculate match score between request and offer
|
|
236
234
|
function calculateSkillMatchScore(request, offer) {
|
|
237
235
|
let score = 50; // Base score
|
|
238
|
-
|
|
236
|
+
|
|
239
237
|
// Skill similarity
|
|
240
238
|
if (request.skill === offer.skill) {
|
|
241
239
|
score += 30;
|
|
242
240
|
} else if (skillsMatch(request.skill, offer.skill)) {
|
|
243
241
|
score += 20;
|
|
244
242
|
}
|
|
245
|
-
|
|
243
|
+
|
|
246
244
|
// Format compatibility
|
|
247
245
|
const formatOverlap = request.format.filter(f => offer.format.includes(f));
|
|
248
246
|
score += formatOverlap.length * 10;
|
|
249
|
-
|
|
247
|
+
|
|
250
248
|
// Urgency vs availability
|
|
251
249
|
if (request.urgency === 'high' && offer.availability === 'flexible') {
|
|
252
250
|
score += 15;
|
|
253
251
|
} else if (request.urgency === 'low' && offer.availability === 'busy') {
|
|
254
252
|
score -= 10;
|
|
255
253
|
}
|
|
256
|
-
|
|
254
|
+
|
|
257
255
|
// Level appropriateness (expert offers are good for all requests)
|
|
258
256
|
if (offer.level === 'expert') {
|
|
259
257
|
score += 10;
|
|
260
258
|
}
|
|
261
|
-
|
|
259
|
+
|
|
262
260
|
return score;
|
|
263
261
|
}
|
|
264
262
|
|
|
265
263
|
// Get skill category for a skill
|
|
266
264
|
function getCategoryForSkill(skill) {
|
|
267
265
|
const skillLower = skill.toLowerCase();
|
|
268
|
-
|
|
266
|
+
|
|
269
267
|
for (const [category, skills] of Object.entries(SKILL_CATEGORIES)) {
|
|
270
268
|
for (const s of skills) {
|
|
271
269
|
if (skillLower.includes(s) || s.includes(skillLower)) {
|
|
@@ -273,16 +271,16 @@ function getCategoryForSkill(skill) {
|
|
|
273
271
|
}
|
|
274
272
|
}
|
|
275
273
|
}
|
|
276
|
-
|
|
274
|
+
|
|
277
275
|
return 'Other';
|
|
278
276
|
}
|
|
279
277
|
|
|
280
278
|
// Get skills by category
|
|
281
279
|
async function getSkillsByCategory() {
|
|
282
280
|
const [offers, requests] = await Promise.all([getSkillOffers(), getSkillRequests()]);
|
|
283
|
-
|
|
281
|
+
|
|
284
282
|
const categories = {};
|
|
285
|
-
|
|
283
|
+
|
|
286
284
|
// Group offers by category
|
|
287
285
|
for (const offer of offers) {
|
|
288
286
|
if (!categories[offer.category]) {
|
|
@@ -290,7 +288,7 @@ async function getSkillsByCategory() {
|
|
|
290
288
|
}
|
|
291
289
|
categories[offer.category].offers.push(offer);
|
|
292
290
|
}
|
|
293
|
-
|
|
291
|
+
|
|
294
292
|
// Group requests by category
|
|
295
293
|
for (const request of requests) {
|
|
296
294
|
if (!categories[request.category]) {
|
|
@@ -298,7 +296,7 @@ async function getSkillsByCategory() {
|
|
|
298
296
|
}
|
|
299
297
|
categories[request.category].requests.push(request);
|
|
300
298
|
}
|
|
301
|
-
|
|
299
|
+
|
|
302
300
|
return categories;
|
|
303
301
|
}
|
|
304
302
|
|
|
@@ -315,10 +313,10 @@ async function recordExchange(fromHandle, toHandle, skill, details = {}) {
|
|
|
315
313
|
rating: details.rating || null, // 1-5 stars
|
|
316
314
|
feedback: details.feedback || null
|
|
317
315
|
};
|
|
318
|
-
|
|
316
|
+
|
|
319
317
|
data.exchanges.push(exchange);
|
|
320
318
|
saveSkillsData(data);
|
|
321
|
-
|
|
319
|
+
|
|
322
320
|
return exchange;
|
|
323
321
|
}
|
|
324
322
|
|
|
@@ -333,18 +331,18 @@ async function getExchangeStats() {
|
|
|
333
331
|
activeRequests: data.requests.filter(r => r.status === 'active').length,
|
|
334
332
|
topSkills: {}
|
|
335
333
|
};
|
|
336
|
-
|
|
334
|
+
|
|
337
335
|
// Count most popular skills
|
|
338
336
|
const skillCount = {};
|
|
339
337
|
[...data.offers, ...data.requests].forEach(item => {
|
|
340
338
|
skillCount[item.skill] = (skillCount[item.skill] || 0) + 1;
|
|
341
339
|
});
|
|
342
|
-
|
|
340
|
+
|
|
343
341
|
stats.topSkills = Object.entries(skillCount)
|
|
344
|
-
.sort(([,a], [,b]) => b - a)
|
|
342
|
+
.sort(([, a], [, b]) => b - a)
|
|
345
343
|
.slice(0, 10)
|
|
346
344
|
.map(([skill, count]) => ({ skill, count }));
|
|
347
|
-
|
|
345
|
+
|
|
348
346
|
return stats;
|
|
349
347
|
}
|
|
350
348
|
|
|
@@ -352,13 +350,13 @@ async function getExchangeStats() {
|
|
|
352
350
|
async function removeSkillItem(handle, id, type) {
|
|
353
351
|
const data = loadSkillsData();
|
|
354
352
|
const key = handle.toLowerCase().replace('@', '');
|
|
355
|
-
|
|
353
|
+
|
|
356
354
|
if (type === 'offer') {
|
|
357
355
|
data.offers = data.offers.filter(o => !(o.handle === key && o.id === id));
|
|
358
356
|
} else if (type === 'request') {
|
|
359
357
|
data.requests = data.requests.filter(r => !(r.handle === key && r.id === id));
|
|
360
358
|
}
|
|
361
|
-
|
|
359
|
+
|
|
362
360
|
saveSkillsData(data);
|
|
363
361
|
}
|
|
364
362
|
|
|
@@ -375,4 +373,4 @@ module.exports = {
|
|
|
375
373
|
recordExchange,
|
|
376
374
|
getExchangeStats,
|
|
377
375
|
removeSkillItem
|
|
378
|
-
};
|
|
376
|
+
};
|
package/store/sqlite.js
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite Message Store - Local persistence for v2 messaging
|
|
3
|
+
*
|
|
4
|
+
* Shares database with Vibe Terminal app at ~/.vibecodings/sessions.db
|
|
5
|
+
* Schema matches src-tauri/src/db.rs exactly (LocalMessage struct)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const Database = require('better-sqlite3');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const { randomUUID } = require('crypto');
|
|
13
|
+
|
|
14
|
+
const DB_PATH = path.join(os.homedir(), '.vibecodings', 'sessions.db');
|
|
15
|
+
|
|
16
|
+
class MessageStore {
|
|
17
|
+
constructor() {
|
|
18
|
+
// Ensure directory exists
|
|
19
|
+
const dir = path.dirname(DB_PATH);
|
|
20
|
+
if (!fs.existsSync(dir)) {
|
|
21
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
this.db = new Database(DB_PATH);
|
|
25
|
+
|
|
26
|
+
// Enable WAL mode for better concurrency with Tauri app
|
|
27
|
+
this.db.pragma('journal_mode = WAL');
|
|
28
|
+
this.db.pragma('busy_timeout = 5000');
|
|
29
|
+
this.db.pragma('synchronous = NORMAL');
|
|
30
|
+
|
|
31
|
+
// Ensure messages table exists (should already exist from Tauri, but just in case)
|
|
32
|
+
this.ensureSchema();
|
|
33
|
+
|
|
34
|
+
// Prepare statements for performance
|
|
35
|
+
this.prepareStatements();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
ensureSchema() {
|
|
39
|
+
// This matches the Tauri schema + V2 Postgres fields
|
|
40
|
+
this.db.exec(`
|
|
41
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
42
|
+
local_id TEXT PRIMARY KEY,
|
|
43
|
+
server_id TEXT,
|
|
44
|
+
thread_id TEXT,
|
|
45
|
+
from_handle TEXT NOT NULL,
|
|
46
|
+
to_handle TEXT NOT NULL,
|
|
47
|
+
content TEXT NOT NULL,
|
|
48
|
+
created_at TEXT NOT NULL,
|
|
49
|
+
status TEXT NOT NULL CHECK(status IN ('pending', 'sent', 'delivered', 'read', 'failed')),
|
|
50
|
+
sent_at TEXT,
|
|
51
|
+
delivered_at TEXT,
|
|
52
|
+
read_at TEXT,
|
|
53
|
+
synced_at TEXT,
|
|
54
|
+
retry_count INTEGER DEFAULT 0
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
CREATE INDEX IF NOT EXISTS idx_messages_thread
|
|
58
|
+
ON messages(from_handle, to_handle, created_at);
|
|
59
|
+
|
|
60
|
+
CREATE INDEX IF NOT EXISTS idx_messages_thread_id
|
|
61
|
+
ON messages(thread_id);
|
|
62
|
+
|
|
63
|
+
CREATE INDEX IF NOT EXISTS idx_messages_server_id
|
|
64
|
+
ON messages(server_id);
|
|
65
|
+
|
|
66
|
+
CREATE INDEX IF NOT EXISTS idx_messages_status
|
|
67
|
+
ON messages(status);
|
|
68
|
+
|
|
69
|
+
CREATE INDEX IF NOT EXISTS idx_messages_synced
|
|
70
|
+
ON messages(synced_at);
|
|
71
|
+
`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
prepareStatements() {
|
|
75
|
+
this.stmts = {
|
|
76
|
+
insert: this.db.prepare(`
|
|
77
|
+
INSERT OR REPLACE INTO messages
|
|
78
|
+
(local_id, server_id, thread_id, from_handle, to_handle, content, created_at, status,
|
|
79
|
+
sent_at, delivered_at, read_at, synced_at, retry_count)
|
|
80
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
81
|
+
`),
|
|
82
|
+
|
|
83
|
+
getThread: this.db.prepare(`
|
|
84
|
+
SELECT local_id, server_id, thread_id, from_handle, to_handle, content, created_at,
|
|
85
|
+
status, sent_at, delivered_at, read_at, synced_at, retry_count
|
|
86
|
+
FROM messages
|
|
87
|
+
WHERE (from_handle = ? AND to_handle = ?)
|
|
88
|
+
OR (from_handle = ? AND to_handle = ?)
|
|
89
|
+
ORDER BY created_at ASC
|
|
90
|
+
LIMIT ?
|
|
91
|
+
`),
|
|
92
|
+
|
|
93
|
+
updateStatus: this.db.prepare(`
|
|
94
|
+
UPDATE messages
|
|
95
|
+
SET status = ?, server_id = COALESCE(?, server_id), thread_id = COALESCE(?, thread_id), sent_at = COALESCE(?, sent_at)
|
|
96
|
+
WHERE local_id = ?
|
|
97
|
+
`),
|
|
98
|
+
|
|
99
|
+
getInboxThreads: this.db.prepare(`
|
|
100
|
+
WITH thread_partners AS (
|
|
101
|
+
SELECT DISTINCT
|
|
102
|
+
CASE WHEN from_handle = ? THEN to_handle ELSE from_handle END as partner
|
|
103
|
+
FROM messages
|
|
104
|
+
WHERE from_handle = ? OR to_handle = ?
|
|
105
|
+
),
|
|
106
|
+
latest_messages AS (
|
|
107
|
+
SELECT
|
|
108
|
+
CASE WHEN from_handle = ? THEN to_handle ELSE from_handle END as partner,
|
|
109
|
+
local_id, server_id, from_handle, to_handle, content, created_at,
|
|
110
|
+
status, sent_at, delivered_at, read_at, synced_at, retry_count,
|
|
111
|
+
ROW_NUMBER() OVER (PARTITION BY CASE WHEN from_handle = ? THEN to_handle ELSE from_handle END
|
|
112
|
+
ORDER BY created_at DESC) as rn
|
|
113
|
+
FROM messages
|
|
114
|
+
WHERE from_handle = ? OR to_handle = ?
|
|
115
|
+
),
|
|
116
|
+
unread_counts AS (
|
|
117
|
+
SELECT
|
|
118
|
+
CASE WHEN to_handle = ? THEN from_handle ELSE to_handle END as partner,
|
|
119
|
+
COUNT(*) as unread
|
|
120
|
+
FROM messages
|
|
121
|
+
WHERE to_handle = ? AND status IN ('sent', 'delivered')
|
|
122
|
+
GROUP BY partner
|
|
123
|
+
)
|
|
124
|
+
SELECT
|
|
125
|
+
lm.partner, lm.local_id, lm.server_id, lm.from_handle, lm.to_handle,
|
|
126
|
+
lm.content, lm.created_at, lm.status, lm.sent_at, lm.delivered_at,
|
|
127
|
+
lm.read_at, lm.synced_at, lm.retry_count,
|
|
128
|
+
COALESCE(uc.unread, 0) as unread_count
|
|
129
|
+
FROM latest_messages lm
|
|
130
|
+
LEFT JOIN unread_counts uc ON lm.partner = uc.partner
|
|
131
|
+
WHERE lm.rn = 1
|
|
132
|
+
ORDER BY lm.created_at DESC
|
|
133
|
+
`),
|
|
134
|
+
|
|
135
|
+
markThreadRead: this.db.prepare(`
|
|
136
|
+
UPDATE messages
|
|
137
|
+
SET status = 'read', read_at = ?
|
|
138
|
+
WHERE from_handle = ? AND to_handle = ? AND status IN ('sent', 'delivered')
|
|
139
|
+
`)
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Save a local message (optimistic - before server confirmation)
|
|
145
|
+
*/
|
|
146
|
+
saveLocalMessage(message) {
|
|
147
|
+
const {
|
|
148
|
+
local_id = randomUUID(),
|
|
149
|
+
server_id = null,
|
|
150
|
+
thread_id = null,
|
|
151
|
+
from_handle,
|
|
152
|
+
to_handle,
|
|
153
|
+
content,
|
|
154
|
+
created_at = new Date().toISOString(),
|
|
155
|
+
status = 'pending',
|
|
156
|
+
sent_at = null,
|
|
157
|
+
delivered_at = null,
|
|
158
|
+
read_at = null,
|
|
159
|
+
synced_at = null,
|
|
160
|
+
retry_count = 0
|
|
161
|
+
} = message;
|
|
162
|
+
|
|
163
|
+
this.stmts.insert.run(
|
|
164
|
+
local_id,
|
|
165
|
+
server_id,
|
|
166
|
+
thread_id,
|
|
167
|
+
from_handle,
|
|
168
|
+
to_handle,
|
|
169
|
+
content,
|
|
170
|
+
created_at,
|
|
171
|
+
status,
|
|
172
|
+
sent_at,
|
|
173
|
+
delivered_at,
|
|
174
|
+
read_at,
|
|
175
|
+
synced_at,
|
|
176
|
+
retry_count
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
return local_id;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Get messages for a thread between two users
|
|
184
|
+
*/
|
|
185
|
+
getThreadMessages(handle1, handle2, limit = 100) {
|
|
186
|
+
return this.stmts.getThread.all(handle1, handle2, handle2, handle1, limit).map(row => ({
|
|
187
|
+
local_id: row.local_id,
|
|
188
|
+
server_id: row.server_id,
|
|
189
|
+
thread_id: row.thread_id,
|
|
190
|
+
from_handle: row.from_handle,
|
|
191
|
+
to_handle: row.to_handle,
|
|
192
|
+
content: row.content,
|
|
193
|
+
created_at: row.created_at,
|
|
194
|
+
status: row.status,
|
|
195
|
+
sent_at: row.sent_at,
|
|
196
|
+
delivered_at: row.delivered_at,
|
|
197
|
+
read_at: row.read_at,
|
|
198
|
+
synced_at: row.synced_at,
|
|
199
|
+
retry_count: row.retry_count
|
|
200
|
+
}));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Update message status after server response
|
|
205
|
+
*/
|
|
206
|
+
updateMessageStatus(local_id, status, server_id = null, thread_id = null) {
|
|
207
|
+
const sent_at = status === 'sent' || status === 'delivered' || status === 'read' ? new Date().toISOString() : null;
|
|
208
|
+
|
|
209
|
+
this.stmts.updateStatus.run(status, server_id, thread_id, sent_at, local_id);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Merge server messages into local cache (V2 Postgres format)
|
|
214
|
+
* Uses INSERT OR IGNORE to avoid overwriting local changes
|
|
215
|
+
*/
|
|
216
|
+
mergeServerMessages(messages) {
|
|
217
|
+
const insert = this.db.prepare(`
|
|
218
|
+
INSERT OR IGNORE INTO messages
|
|
219
|
+
(local_id, server_id, thread_id, from_handle, to_handle, content, created_at, status,
|
|
220
|
+
sent_at, delivered_at, read_at, synced_at, retry_count)
|
|
221
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
222
|
+
`);
|
|
223
|
+
|
|
224
|
+
const transaction = this.db.transaction(msgs => {
|
|
225
|
+
for (const msg of msgs) {
|
|
226
|
+
insert.run(
|
|
227
|
+
msg.local_id || msg.id || randomUUID(), // Handle both formats
|
|
228
|
+
msg.server_id || msg.id,
|
|
229
|
+
msg.thread_id, // V2 Postgres thread_id
|
|
230
|
+
msg.from_handle || msg.from,
|
|
231
|
+
msg.to_handle || msg.to,
|
|
232
|
+
msg.content || msg.body || msg.text || '',
|
|
233
|
+
msg.created_at || msg.createdAt || new Date().toISOString(),
|
|
234
|
+
msg.status || 'delivered',
|
|
235
|
+
msg.sent_at || msg.sentAt || msg.created_at || msg.createdAt,
|
|
236
|
+
msg.delivered_at || msg.deliveredAt,
|
|
237
|
+
msg.read_at || msg.readAt,
|
|
238
|
+
new Date().toISOString(), // synced_at
|
|
239
|
+
0 // retry_count
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
transaction(messages);
|
|
245
|
+
return messages.length;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Get inbox threads for a user
|
|
250
|
+
*/
|
|
251
|
+
getInboxThreads(handle) {
|
|
252
|
+
const rows = this.stmts.getInboxThreads.all(
|
|
253
|
+
handle,
|
|
254
|
+
handle,
|
|
255
|
+
handle, // thread_partners CTE
|
|
256
|
+
handle,
|
|
257
|
+
handle,
|
|
258
|
+
handle,
|
|
259
|
+
handle, // latest_messages CTE
|
|
260
|
+
handle,
|
|
261
|
+
handle // unread_counts CTE
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
return rows.map(row => ({
|
|
265
|
+
partner: row.partner,
|
|
266
|
+
latestMessage: {
|
|
267
|
+
local_id: row.local_id,
|
|
268
|
+
server_id: row.server_id,
|
|
269
|
+
from_handle: row.from_handle,
|
|
270
|
+
to_handle: row.to_handle,
|
|
271
|
+
content: row.content,
|
|
272
|
+
created_at: row.created_at,
|
|
273
|
+
status: row.status,
|
|
274
|
+
sent_at: row.sent_at,
|
|
275
|
+
delivered_at: row.delivered_at,
|
|
276
|
+
read_at: row.read_at,
|
|
277
|
+
synced_at: row.synced_at,
|
|
278
|
+
retry_count: row.retry_count
|
|
279
|
+
},
|
|
280
|
+
unreadCount: row.unread_count
|
|
281
|
+
}));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Mark all messages in a thread as read
|
|
286
|
+
*/
|
|
287
|
+
markThreadRead(my_handle, other_handle) {
|
|
288
|
+
const now = new Date().toISOString();
|
|
289
|
+
const result = this.stmts.markThreadRead.run(now, other_handle, my_handle);
|
|
290
|
+
return result.changes;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Get pending/failed messages for retry
|
|
295
|
+
*/
|
|
296
|
+
getPendingMessages() {
|
|
297
|
+
const rows = this.db
|
|
298
|
+
.prepare(
|
|
299
|
+
`
|
|
300
|
+
SELECT local_id, server_id, from_handle, to_handle, content, created_at,
|
|
301
|
+
status, sent_at, delivered_at, read_at, synced_at, retry_count
|
|
302
|
+
FROM messages
|
|
303
|
+
WHERE status = 'pending' OR status = 'failed'
|
|
304
|
+
ORDER BY created_at ASC
|
|
305
|
+
`
|
|
306
|
+
)
|
|
307
|
+
.all();
|
|
308
|
+
|
|
309
|
+
return rows.map(row => ({
|
|
310
|
+
local_id: row.local_id,
|
|
311
|
+
server_id: row.server_id,
|
|
312
|
+
from_handle: row.from_handle,
|
|
313
|
+
to_handle: row.to_handle,
|
|
314
|
+
content: row.content,
|
|
315
|
+
created_at: row.created_at,
|
|
316
|
+
status: row.status,
|
|
317
|
+
sent_at: row.sent_at,
|
|
318
|
+
delivered_at: row.delivered_at,
|
|
319
|
+
read_at: row.read_at,
|
|
320
|
+
synced_at: row.synced_at,
|
|
321
|
+
retry_count: row.retry_count
|
|
322
|
+
}));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
close() {
|
|
326
|
+
this.db.close();
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Export singleton instance
|
|
331
|
+
let instance = null;
|
|
332
|
+
|
|
333
|
+
function getInstance() {
|
|
334
|
+
if (!instance) {
|
|
335
|
+
try {
|
|
336
|
+
instance = new MessageStore();
|
|
337
|
+
} catch (error) {
|
|
338
|
+
console.error('[SQLite] Failed to initialize:', error.message);
|
|
339
|
+
// Return stub with no-op methods if SQLite fails
|
|
340
|
+
return {
|
|
341
|
+
saveLocalMessage: () => randomUUID(),
|
|
342
|
+
getThreadMessages: () => [],
|
|
343
|
+
updateMessageStatus: () => {},
|
|
344
|
+
mergeServerMessages: () => 0,
|
|
345
|
+
getInboxThreads: () => [],
|
|
346
|
+
markThreadRead: () => 0,
|
|
347
|
+
getPendingMessages: () => [],
|
|
348
|
+
close: () => {}
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return instance;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
module.exports = getInstance();
|