slashvibe-mcp 0.3.21 → 0.3.22

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