slashvibe-mcp 0.8.24 → 0.8.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,10 +1,9 @@
1
1
  # slashvibe-mcp
2
2
 
3
- Reach your collaborators from any coding session. Presence, DMs, and messages that wait across sessions
4
- between the people (and agents) you already work with — nobody has to be online at the
5
- same time.
3
+ **Message people from Claude Code or Codex.**
6
4
 
7
- **Works in:** Claude Code, Codex, Cursor — anything that speaks MCP.
5
+ Presence, DMs, and messages that wait across sessions, between the people (and agents)
6
+ you already work with — nobody has to be online at the same time.
8
7
 
9
8
  ## Install — one command
10
9
 
@@ -12,15 +11,23 @@ same time.
12
11
  npx slashvibe-mcp
13
12
  ```
14
13
 
15
- That's the whole thing: it configures Claude Code, Codex, and Cursor in one run, opens
16
- GitHub to sign you in, and your GitHub username becomes your @handle. About 30 seconds.
14
+ It configures your coding agent, opens GitHub to sign you in, and your GitHub username
15
+ becomes your @handle. About 30 seconds. **Then restart your coding session** so it loads
16
+ the /vibe server.
17
17
 
18
- **Then restart your coding agent** so it loads the /vibe MCP server. (Setup writes the
19
- config; the agent only picks up a new MCP server on start.) After the restart, run
20
- `vibe init` if you weren't signed in during setup.
18
+ ## Then talk normally
21
19
 
22
- Invited by someone? After setup and restart, say `vibe inbox` — their message is waiting.
23
- Reply and you're done; that's the whole onboarding.
20
+ ```
21
+ show me who's on /vibe
22
+ message @brightseth — what are you building?
23
+ did anyone message me?
24
+ ```
25
+
26
+ No commands to learn. Your coding agent routes those to /vibe. If it ever asks you to
27
+ sign in, say `sign in to /vibe`.
28
+
29
+ **Works in:** Claude Code and Codex. Setup also writes Cursor's config; Cursor is not part
30
+ of the walked, proven path yet.
24
31
 
25
32
  ## Put waiting messages at the top of your next Claude session
26
33
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slashvibe-mcp",
3
- "version": "0.8.24",
3
+ "version": "0.8.26",
4
4
  "mcpName": "io.github.vibecodinginc/vibe",
5
5
  "description": "Presence + messaging for terminal coding agents (Claude Code, Codex, Cursor) — the /vibe kernel",
6
6
  "main": "index.js",
@@ -30,12 +30,14 @@ function canonicalHandle(value) {
30
30
  if (typeof value !== 'string') return '';
31
31
  let h = value.replace(INVISIBLE, '');
32
32
  h = h.normalize ? h.normalize('NFC') : h;
33
- h = h.trim().replace(/^@+/, '').trim().toLowerCase();
34
- return h.replace(/-/g, '_');
33
+ // A handle is the GitHub login, and GitHub logins may contain '-'. Rewriting
34
+ // dashes to underscores made `vibe inbox synth-stan` look up synth_stan and
35
+ // render an empty thread (found live 2026-09-02). Keep the dash.
36
+ return h.trim().replace(/^@+/, '').trim().toLowerCase();
35
37
  }
36
38
 
37
- /** Platform-legal shape: ASCII letters, digits, underscore. Reject anything else. */
38
- const isCanonicalHandle = (value) => /^[a-z0-9_]{1,39}$/.test(canonicalHandle(value));
39
+ /** Platform-legal shape: ASCII letters, digits, underscore, dash (GitHub logins). Reject anything else. */
40
+ const isCanonicalHandle = (value) => /^[a-z0-9_-]{1,39}$/.test(canonicalHandle(value));
39
41
 
40
42
  /** True when two handles refer to the same principal label. */
41
43
  const sameHandle = (a, b) => {
package/store/api.js CHANGED
@@ -797,24 +797,53 @@ async function getRawInbox(handle) {
797
797
 
798
798
  // Transform threads with unread messages into message-like objects
799
799
  // for compatibility with notification system
800
- const unreadMessages = [];
801
-
802
- for (const thread of threads) {
803
- if (thread.unread > 0 && thread.last_message) {
804
- // Create message object from thread's last message
805
- unreadMessages.push({
806
- id: thread.last_message.id,
807
- from: thread.last_message.from,
808
- to: handle,
809
- text: thread.last_message.body,
810
- body: thread.last_message.body,
811
- createdAt: thread.last_message.created_at,
812
- read: false, // If it's in unread threads, it's unread
813
- thread_id: thread.id,
814
- unread_count: thread.unread,
815
- });
800
+ const me = String(handle || '').toLowerCase();
801
+ const isMine = (m) => String(m?.from || '').toLowerCase() === me;
802
+ const candidates = threads.filter((t) => t.unread > 0 && t.last_message);
803
+
804
+ // "Waiting" means THEIR words. When the newest message is mine — I replied
805
+ // and their earlier message is still unread — the thread's last_message
806
+ // is my own send, and presenting it as "MESSAGE from @me" told a person
807
+ // their own words had arrived (Seth, 2026-09-02, the day of the invite).
808
+ // For those threads read the TAIL of the thread (the API pages oldest-first)
809
+ // and take the newest message of theirs. Bounded parallelism: the session-
810
+ // start hook runs this under a 4s deadline (codex P2 ×2 on #38).
811
+ const TAIL = 50;
812
+ const resolveWaiting = async (thread) => {
813
+ if (!isMine(thread.last_message)) return thread.last_message;
814
+ try {
815
+ const count = Number(thread.message_count);
816
+ const offset = Number.isFinite(count) && count > TAIL ? count - TAIL : 0;
817
+ const t = await request('GET', `/api/v2/threads/${encodeURIComponent(thread.id)}?limit=${TAIL}&offset=${offset}`);
818
+ const theirs = (t.messages || []).filter((m) => !isMine(m));
819
+ return theirs.length ? theirs[theirs.length - 1] : null;
820
+ } catch {
821
+ return null;
816
822
  }
817
- }
823
+ };
824
+ const CONCURRENCY = 6;
825
+ const resolved = new Array(candidates.length);
826
+ let next = 0;
827
+ await Promise.all(Array.from({ length: Math.min(CONCURRENCY, candidates.length) }, async () => {
828
+ while (next < candidates.length) { const i = next++; resolved[i] = await resolveWaiting(candidates[i]); }
829
+ }));
830
+
831
+ const unreadMessages = [];
832
+ candidates.forEach((thread, i) => {
833
+ const waiting = resolved[i];
834
+ if (!waiting) return; // nothing of theirs to show — not a waiting message
835
+ unreadMessages.push({
836
+ id: waiting.id,
837
+ from: waiting.from,
838
+ to: handle,
839
+ text: waiting.body,
840
+ body: waiting.body,
841
+ createdAt: waiting.created_at,
842
+ read: false, // If it's in unread threads, it's unread
843
+ thread_id: thread.id,
844
+ unread_count: thread.unread,
845
+ });
846
+ });
818
847
 
819
848
  return unreadMessages;
820
849
  } catch (e) {
package/tools/inbox.js CHANGED
@@ -248,7 +248,18 @@ async function handler(args) {
248
248
  };
249
249
  }
250
250
 
251
- const threads = await store.getInbox(myHandle);
251
+ // A failed read is not an empty inbox. getInbox() flattens "nobody
252
+ // answered" into [] — and the tool then said "no messages yet", which every
253
+ // host repeated as "you're caught up" (routing corpus R02, 0/6). Keep the
254
+ // outcome, as vibe_start already does, and say what is actually known.
255
+ const read = await store.getInboxResult(myHandle);
256
+ if (read.ok !== true) {
257
+ return {
258
+ display: `⚠️ could not check your inbox (${read.error || 'unknown'}) — unread is unknown, not zero. Try again in a moment.`,
259
+ footer: 'minimal',
260
+ };
261
+ }
262
+ const threads = read.threads;
252
263
 
253
264
  // Check for guest session messages (multiplayer)
254
265
  let guestMessages = [];
package/version.json CHANGED
@@ -1,11 +1,9 @@
1
1
  {
2
- "version": "0.8.24",
3
- "updated": "2026-09-02",
4
- "changelog": "Quieter when you are away. The server used to check your inbox twice every fifteen seconds for as long as your coding session stayed open — days, sometimes. Now it checks every fifteen seconds while you are actually using /vibe and backs off to once a minute when you are not; the moment you use a tool it is fast again. Replies still reach a live session within fifteen seconds, and within a minute of an idle one.",
2
+ "version": "0.8.26",
3
+ "updated": "2026-09-04",
4
+ "changelog": "A failed read is not an empty inbox. When the inbox could not be reached, vibe_inbox said \"no messages yet\" and every host repeated it as \"you're caught up\" — six out of six runs in the Sep 4 routing corpus. Now it says what is actually known: could not check your inbox, unread is unknown, not zero. A real empty inbox still reads as empty.",
5
5
  "features": [
6
- "Inbox checks back off to once a minute when a session is idle, and snap back the moment you use a tool",
7
- "The rarely used guest-session poll runs once a minute while active and every five minutes while idle",
8
- "Presence heartbeat unchanged: you stay visible exactly as before"
6
+ "vibe_inbox reports a failed read as unknown, never as caught up (routing corpus R02)"
9
7
  ],
10
8
  "deprecated": [],
11
9
  "breaking": false,