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.
Files changed (235) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +280 -47
  3. package/auto-update.js +10 -15
  4. package/config.js +36 -31
  5. package/crypto.js +1 -6
  6. package/debug.js +12 -0
  7. package/discord.js +19 -19
  8. package/eslint.config.js +54 -0
  9. package/index.js +217 -207
  10. package/intelligence/index.js +2 -9
  11. package/intelligence/infer.js +10 -16
  12. package/intelligence/patterns.js +23 -18
  13. package/intelligence/proactive.js +16 -15
  14. package/intelligence/serendipity.js +57 -20
  15. package/memory.js +13 -8
  16. package/migrate-v2.js +72 -0
  17. package/notification-emitter.js +2 -2
  18. package/notify.js +39 -14
  19. package/package.json +28 -29
  20. package/post-install.js +141 -0
  21. package/presence.js +2 -2
  22. package/prompts.js +5 -9
  23. package/protocol/index.js +123 -87
  24. package/protocol/telegram-commands.js +36 -37
  25. package/store/api.js +358 -529
  26. package/store/local.js +9 -10
  27. package/store/profiles.js +48 -192
  28. package/store/reservations.js +2 -9
  29. package/store/skills.js +69 -71
  30. package/store/sqlite.js +355 -0
  31. package/test-skills-bootstrap.js +20 -0
  32. package/test-v2-integration.js +385 -0
  33. package/tools/_actions.js +48 -387
  34. package/tools/_connection-queue.js +45 -56
  35. package/tools/_discovery-enhanced.js +52 -57
  36. package/tools/_discovery.js +87 -185
  37. package/tools/{l2-status.js → _experimental/l2-status.js} +68 -70
  38. package/tools/{shipback.js → _experimental/shipback.js} +4 -3
  39. package/tools/_proactive-discovery.js +60 -73
  40. package/tools/_shared/index.js +41 -64
  41. package/tools/admin-inbox.js +10 -15
  42. package/tools/agents.js +1 -1
  43. package/tools/artifact-create.js +13 -23
  44. package/tools/artifact-view.js +4 -4
  45. package/tools/{_deprecated/back.js → back.js} +1 -1
  46. package/tools/bye.js +3 -5
  47. package/tools/consent.js +2 -2
  48. package/tools/context.js +9 -10
  49. package/tools/crossword.js +3 -2
  50. package/tools/discover.js +94 -356
  51. package/tools/dm.js +27 -86
  52. package/tools/doctor.js +12 -41
  53. package/tools/drawing.js +34 -20
  54. package/tools/echo.js +11 -11
  55. package/tools/feed.js +30 -58
  56. package/tools/follow.js +64 -187
  57. package/tools/{_deprecated/forget.js → forget.js} +4 -7
  58. package/tools/game.js +144 -48
  59. package/tools/handoff.js +6 -8
  60. package/tools/help.js +3 -3
  61. package/tools/idea.js +15 -27
  62. package/tools/inbox.js +121 -293
  63. package/tools/init.js +54 -151
  64. package/tools/invite.js +8 -21
  65. package/tools/migrate.js +27 -24
  66. package/tools/multiplayer-game.js +50 -40
  67. package/tools/{_deprecated/mute.js → mute.js} +4 -3
  68. package/tools/notifications.js +58 -48
  69. package/tools/observe.js +12 -15
  70. package/tools/onboarding.js +8 -11
  71. package/tools/open.js +13 -144
  72. package/tools/party-game.js +23 -12
  73. package/tools/patterns.js +2 -1
  74. package/tools/ping.js +5 -7
  75. package/tools/react.js +28 -30
  76. package/tools/{_deprecated/recall.js → recall.js} +5 -10
  77. package/tools/release.js +4 -2
  78. package/tools/{_deprecated/remember.js → remember.js} +4 -6
  79. package/tools/report.js +2 -2
  80. package/tools/request.js +6 -26
  81. package/tools/reserve.js +1 -1
  82. package/tools/session-fork.js +97 -0
  83. package/tools/session-save.js +109 -0
  84. package/tools/settings.js +30 -99
  85. package/tools/ship.js +74 -56
  86. package/tools/{_deprecated/skills-exchange.js → skills-exchange.js} +38 -39
  87. package/tools/social-inbox.js +22 -28
  88. package/tools/social-post.js +24 -27
  89. package/tools/solo-game.js +54 -46
  90. package/tools/start.js +14 -148
  91. package/tools/status.js +21 -68
  92. package/tools/submit.js +4 -2
  93. package/tools/suggest-tags.js +36 -33
  94. package/tools/summarize.js +19 -16
  95. package/tools/tag-suggestions.js +72 -73
  96. package/tools/test.js +1 -1
  97. package/tools/{_deprecated/tictactoe.js → tictactoe.js} +26 -26
  98. package/tools/token.js +4 -4
  99. package/tools/update.js +1 -2
  100. package/tools/watch.js +132 -112
  101. package/tools/who.js +20 -40
  102. package/tools/{_deprecated/wordassociation.js → wordassociation.js} +23 -20
  103. package/tools/workshop-buddy.js +52 -53
  104. package/tools/x-mentions.js +0 -1
  105. package/tools/x-reply.js +0 -1
  106. package/twitter.js +14 -20
  107. package/version.json +8 -10
  108. package/webhook-runner.js +132 -0
  109. package/auth-store.js +0 -148
  110. package/bridges/bridge-monitor.js +0 -388
  111. package/bridges/discord-bot.js +0 -431
  112. package/bridges/farcaster.js +0 -299
  113. package/bridges/telegram.js +0 -261
  114. package/bridges/webhook-health.js +0 -420
  115. package/bridges/webhook-server.js +0 -437
  116. package/bridges/whatsapp.js +0 -441
  117. package/bridges/x-webhook.js +0 -423
  118. package/games/arcade.js +0 -406
  119. package/games/chess.js +0 -451
  120. package/games/colorguess.js +0 -343
  121. package/games/crossword-words.js +0 -171
  122. package/games/crossword.js +0 -461
  123. package/games/drawing.js +0 -347
  124. package/games/gameroulette.js +0 -300
  125. package/games/gamerouter.js +0 -336
  126. package/games/gamestatus.js +0 -337
  127. package/games/guessnumber.js +0 -209
  128. package/games/hangman.js +0 -279
  129. package/games/memory.js +0 -338
  130. package/games/multiplayer-tictactoe.js +0 -389
  131. package/games/pixelart.js +0 -399
  132. package/games/quickduel.js +0 -354
  133. package/games/riddle.js +0 -371
  134. package/games/rockpaperscissors.js +0 -291
  135. package/games/snake.js +0 -406
  136. package/games/storybuilder.js +0 -343
  137. package/games/tictactoe.js +0 -345
  138. package/games/twentyquestions.js +0 -286
  139. package/games/twotruths.js +0 -207
  140. package/games/werewolf.js +0 -508
  141. package/games/wordassociation.js +0 -247
  142. package/games/wordchain.js +0 -135
  143. package/intelligence/interests.js +0 -369
  144. package/setup.js +0 -480
  145. package/smart-inbox.js +0 -276
  146. package/tools/_deprecated/auto-suggest-connections.js +0 -304
  147. package/tools/_deprecated/bootstrap-skills.js +0 -231
  148. package/tools/_deprecated/bridge-dashboard.js +0 -342
  149. package/tools/_deprecated/bridge-health.js +0 -400
  150. package/tools/_deprecated/bridge-live.js +0 -384
  151. package/tools/_deprecated/bridges.js +0 -383
  152. package/tools/_deprecated/colorguess.js +0 -281
  153. package/tools/_deprecated/discover-insights.js +0 -379
  154. package/tools/_deprecated/discover-momentum.js +0 -256
  155. package/tools/_deprecated/discovery-analytics.js +0 -345
  156. package/tools/_deprecated/discovery-auto-suggest.js +0 -275
  157. package/tools/_deprecated/discovery-bootstrap.js +0 -267
  158. package/tools/_deprecated/discovery-daily.js +0 -375
  159. package/tools/_deprecated/discovery-dashboard.js +0 -385
  160. package/tools/_deprecated/discovery-digest.js +0 -314
  161. package/tools/_deprecated/discovery-hub.js +0 -357
  162. package/tools/_deprecated/discovery-insights.js +0 -384
  163. package/tools/_deprecated/discovery-momentum.js +0 -281
  164. package/tools/_deprecated/discovery-monitor.js +0 -319
  165. package/tools/_deprecated/discovery-proactive.js +0 -300
  166. package/tools/_deprecated/draw.js +0 -317
  167. package/tools/_deprecated/farcaster.js +0 -307
  168. package/tools/_deprecated/games-catalog.js +0 -376
  169. package/tools/_deprecated/games.js +0 -313
  170. package/tools/_deprecated/guessnumber.js +0 -194
  171. package/tools/_deprecated/hangman.js +0 -129
  172. package/tools/_deprecated/multiplayer-tictactoe.js +0 -303
  173. package/tools/_deprecated/riddle.js +0 -240
  174. package/tools/_deprecated/run-bootstrap.js +0 -69
  175. package/tools/_deprecated/skills-analytics.js +0 -349
  176. package/tools/_deprecated/skills-bootstrap.js +0 -301
  177. package/tools/_deprecated/skills-dashboard.js +0 -268
  178. package/tools/_deprecated/skills.js +0 -380
  179. package/tools/_deprecated/smart-intro.js +0 -353
  180. package/tools/_deprecated/storybuilder.js +0 -331
  181. package/tools/_deprecated/telegram-bot.js +0 -183
  182. package/tools/_deprecated/telegram-setup.js +0 -214
  183. package/tools/_deprecated/twentyquestions.js +0 -143
  184. package/tools/_shared.js +0 -234
  185. package/tools/_work-context.js +0 -338
  186. package/tools/_work-context.manual-test.js +0 -199
  187. package/tools/_work-context.test.js +0 -260
  188. package/tools/activity.js +0 -220
  189. package/tools/agent-treasury.js +0 -288
  190. package/tools/analytics.js +0 -191
  191. package/tools/approve.js +0 -197
  192. package/tools/arcade.js +0 -173
  193. package/tools/artifacts-price.js +0 -107
  194. package/tools/ask-expert.js +0 -160
  195. package/tools/available.js +0 -120
  196. package/tools/become-expert.js +0 -150
  197. package/tools/broadcast.js +0 -325
  198. package/tools/chat.js +0 -202
  199. package/tools/collaborative-drawing.js +0 -286
  200. package/tools/connection-status.js +0 -178
  201. package/tools/earnings.js +0 -126
  202. package/tools/friends.js +0 -207
  203. package/tools/genesis.js +0 -233
  204. package/tools/gig-browse.js +0 -206
  205. package/tools/gig-complete.js +0 -144
  206. package/tools/health.js +0 -87
  207. package/tools/leaderboard.js +0 -117
  208. package/tools/lib/git-apply.js +0 -206
  209. package/tools/lib/git-bundle.js +0 -407
  210. package/tools/mint.js +0 -377
  211. package/tools/plan.js +0 -225
  212. package/tools/profile.js +0 -219
  213. package/tools/proof-of-work.js +0 -144
  214. package/tools/pulse.js +0 -218
  215. package/tools/reply.js +0 -166
  216. package/tools/reputation.js +0 -175
  217. package/tools/schedule.js +0 -367
  218. package/tools/search-messages.js +0 -123
  219. package/tools/session.js +0 -467
  220. package/tools/session_price.js +0 -128
  221. package/tools/smart-check.js +0 -201
  222. package/tools/social-processor.js +0 -445
  223. package/tools/streak.js +0 -147
  224. package/tools/stuck.js +0 -297
  225. package/tools/subscribe.js +0 -148
  226. package/tools/subscriptions.js +0 -134
  227. package/tools/tip.js +0 -193
  228. package/tools/wallet.js +0 -269
  229. package/tools/webhook-test.js +0 -388
  230. package/tools/withdraw.js +0 -145
  231. package/tools/work-summary.js +0 -96
  232. package/tools/workshop.js +0 -327
  233. /package/tools/{l2-bridge.js → _experimental/l2-bridge.js} +0 -0
  234. /package/tools/{l2.js → _experimental/l2.js} +0 -0
  235. /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
- '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']
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(o =>
107
- o.skill.includes(filter) ||
108
- o.category.toLowerCase().includes(filter) ||
109
- o.description?.toLowerCase().includes(filter)
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(r =>
124
- r.skill.includes(filter) ||
125
- r.category.toLowerCase().includes(filter) ||
126
- r.description?.toLowerCase().includes(filter) ||
127
- r.context?.toLowerCase().includes(filter)
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: [] // People I can help
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(o =>
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(r =>
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
- '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']
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
+ };
@@ -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();