slashvibe-mcp 0.8.4 → 0.8.5

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/config.js CHANGED
@@ -104,6 +104,26 @@ function save(config) {
104
104
  }
105
105
 
106
106
  function getHandle() {
107
+ // THE CREDENTIAL NAMES US — the files below are only what we knew before one loaded.
108
+ //
109
+ // Twenty-eight call sites across the tools read this function, and it answered from
110
+ // disk independently of the token those same tools authenticate with. That is the
111
+ // split identity reproduced in review: a token for account A while display and
112
+ // self-checks said account B, so a tool could route a message to "you" that the
113
+ // server had authored as someone else. Fixing it at each call site would have been
114
+ // twenty-eight chances to miss one; fixing it here makes every reader correct at
115
+ // once, and leaves exactly one place that decides.
116
+ //
117
+ // Required lazily: auth-store loads config during hydration, and a top-level import
118
+ // here would close that circle.
119
+ try {
120
+ const authStore = require('./auth-store');
121
+ const fromCredential = authStore.getHandle();
122
+ if (fromCredential) return fromCredential;
123
+ } catch (e) {
124
+ // auth-store unavailable (early boot) — fall through to what is on disk.
125
+ }
126
+
107
127
  // Prefer session-specific handle over shared config
108
128
  const sessionHandle = getSessionHandle();
109
129
  if (sessionHandle) return sessionHandle;
package/index.js CHANGED
@@ -65,8 +65,19 @@ const NO_AUTH_REQUIRED = new Set([
65
65
  'vibe_start', 'vibe_init', 'vibe_token', 'vibe_who', 'vibe_help'
66
66
  ]);
67
67
 
68
- // Single source of truth for "is this session signed in" — token or OAuth.
69
- const isAuthed = () => authStore.isAuthenticated() || config.hasOAuth();
68
+ // "Is this session signed in" — the credential answers, not the filesystem.
69
+ //
70
+ // This used to OR in config.hasOAuth(), which asks a FILE. That made disk a second
71
+ // authority: a leftover config could unlock the authenticated tool surface for a
72
+ // session holding no usable credential. authStore already consults disk during
73
+ // hydration and refuses anything it cannot attribute, so it is the one place that
74
+ // should decide.
75
+ //
76
+ // Deliberately NOT gated on isVerified(): verification needs the network, and a
77
+ // person on a plane should still see their tools and get a clear error from the
78
+ // server rather than a surface that silently empties. Verification gates the things
79
+ // that must never be a guess — the green dot and any claim about who you are.
80
+ const isAuthed = () => authStore.isAuthenticated();
70
81
 
71
82
  // Infer user prompt from tool arguments (for pattern logging).
72
83
  // Cases exist only for registered tools whose args improve on the default.
@@ -674,13 +685,37 @@ class VibeMCPServer {
674
685
  const handle = authStore.getHandle();
675
686
  if (!handle || !authStore.isAuthenticated()) return;
676
687
 
688
+ // ASK THE SERVER BEFORE CLAIMING ANYTHING.
689
+ //
690
+ // I added an explicit verified-vs-saved state and then gated nothing on it, so
691
+ // this still announced on the strength of "a token-shaped string exists on
692
+ // disk". A forged JWT-shaped value in config or Buddy's auth.json produced a
693
+ // confident "🟢 Auto-connected as @victim" and a heartbeat against production.
694
+ // Holding a credential is not the same as having one that works, and the green
695
+ // dot is the one thing in this product that must never be a guess.
696
+ let verified;
697
+ try {
698
+ verified = await store.verifyAuthToken(authStore.getToken());
699
+ } catch (e) {
700
+ verified = null;
701
+ }
702
+ if (!verified || !verified.valid) {
703
+ // Definitive rejection means the session is dead; unreachable means unknown.
704
+ // Neither earns a green dot, and neither is worth shouting about at startup.
705
+ if (verified && verified.definitive) {
706
+ process.stderr.write('/vibe: saved session is no longer valid — say "vibe init" to sign in again\n');
707
+ }
708
+ return;
709
+ }
710
+ authStore.markVerified(verified.handle);
711
+
677
712
  const one_liner = config.getOneLiner() || '';
678
713
 
679
- // Start presence heartbeat
680
- presence.start(handle, one_liner);
714
+ // Start presence heartbeat — now, and only now, is this handle a fact.
715
+ presence.start(authStore.getHandle(), one_liner);
681
716
 
682
717
  // Log quietly (only to stderr, not intrusive)
683
- process.stderr.write(`🟢 Auto-connected as @${handle}\n`);
718
+ process.stderr.write(`🟢 Auto-connected as @${authStore.getHandle()}\n`);
684
719
  } catch (error) {
685
720
  // Silent fail - don't block startup
686
721
  }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "slashvibe-mcp",
3
- "version": "0.8.4",
3
+ "version": "0.8.5",
4
4
  "mcpName": "io.github.vibecodinginc/vibe",
5
- "description": "Presence + messaging for terminal coding agents (Claude Code, Codex, Cursor) — the /vibe kernel",
5
+ "description": "Presence + messaging for terminal coding agents (Claude Code, Codex, Cursor) \u2014 the /vibe kernel",
6
6
  "main": "index.js",
7
7
  "bin": {
8
8
  "slashvibe-mcp": "./cli.js",
@@ -17,7 +17,8 @@
17
17
  "version": "node scripts/sync-versions.js && git add server.json",
18
18
  "lint": "eslint . --ext .js",
19
19
  "lint:fix": "eslint . --ext .js --fix",
20
- "test:pack": "node --test test/pack-artifact.test.mjs"
20
+ "test:pack": "node --test test/pack-artifact.test.mjs",
21
+ "test:release": "node scripts/release-conformance.mjs"
21
22
  },
22
23
  "keywords": [
23
24
  "mcp",
package/setup.js CHANGED
@@ -273,15 +273,22 @@ async function testConnection() {
273
273
  // The client no longer guesses at the server's vocabulary. Reachability is what
274
274
  // HTTP already answers; the body is only allowed to veto by reporting an explicit
275
275
  // unhealthy state.
276
+ // Bounded: a hung connection must not leave a new user staring at a spinner with no
277
+ // way to know whether to wait. And "degraded" is not good enough to promise the next
278
+ // step will work — the very next thing setup does is an OAuth round trip.
279
+ const controller = new AbortController();
280
+ const timer = setTimeout(() => controller.abort(), 8000);
276
281
  try {
277
- const response = await fetch(`${API_BASE}/api/health`);
282
+ const response = await fetch(`${API_BASE}/api/health`, { signal: controller.signal });
278
283
  if (!response.ok) return false;
279
284
  const data = await response.json().catch(() => ({}));
280
285
  if (data.success === false) return false;
281
- if (typeof data.status === 'string' && /unhealthy|down|error/i.test(data.status)) return false;
286
+ if (typeof data.status === 'string' && /unhealthy|degraded|down|error/i.test(data.status)) return false;
282
287
  return true;
283
288
  } catch (e) {
284
289
  return false;
290
+ } finally {
291
+ clearTimeout(timer);
285
292
  }
286
293
  }
287
294
 
@@ -332,6 +339,13 @@ async function getOnlineUsers() {
332
339
  * Open URL in browser
333
340
  */
334
341
  function openBrowser(url) {
342
+ // Release conformance runs the real walk on a build machine; opening a browser there
343
+ // would authenticate against production as whoever that machine is signed in as.
344
+ // Nothing in normal use sets this.
345
+ if (process.env.VIBE_SETUP_STUB_BROWSER === '1') {
346
+ console.log(`${colors.dim} (browser suppressed for conformance run)${colors.reset}`);
347
+ return;
348
+ }
335
349
  const platform = process.platform;
336
350
  let command;
337
351
 
@@ -386,10 +400,18 @@ async function setup() {
386
400
  // Step 1: Detect coding agents on this machine
387
401
  printStep(1, 'Detecting coding agents...', 'running');
388
402
  const homeDir = os.homedir();
389
- const detected = ['Claude Code']; // always configured
403
+ // Detection reports what is HERE. `['Claude Code'] // always configured` was a
404
+ // detection step that had already decided its answer: a machine with no Claude Code
405
+ // was still told "→ Claude Code". #115 made the configuration honest but left this
406
+ // line printing the old claim, which is the difference between fixing the internals
407
+ // and fixing what a person actually reads.
408
+ const detected = [];
409
+ if (findClaudeConfig()) detected.push('Claude Code');
390
410
  if (fs.existsSync(codexHome())) detected.push('Codex');
391
411
  if (fs.existsSync(path.join(homeDir, '.cursor'))) detected.push('Cursor');
392
- console.log(`${colors.dim} → ${detected.join(', ')}${colors.reset}`);
412
+ console.log(detected.length
413
+ ? `${colors.dim} → ${detected.join(', ')}${colors.reset}`
414
+ : `${colors.dim} → none yet — setting up Claude Code so /vibe is ready when you install it${colors.reset}`);
393
415
  printStep(1, 'Detecting coding agents...', 'done');
394
416
 
395
417
  // Step 2: Add /vibe MCP server to every detected host
@@ -400,7 +422,8 @@ async function setup() {
400
422
  : r.status === 'exists' ? 'already configured'
401
423
  : `failed: ${r.error}`;
402
424
  const icon = r.status === 'error' ? colors.yellow + '!' : colors.green + '✓';
403
- console.log(` ${icon}${colors.reset} ${r.host} ${colors.dim}— ${note} (${r.path})${colors.reset}`);
425
+ const label = r.hostWasMissing ? `${r.host} ${colors.dim}(not installed yet)` : r.host;
426
+ console.log(` ${icon}${colors.reset} ${label} ${colors.dim}— ${note} (${r.path})${colors.reset}`);
404
427
  }
405
428
  const configuredCount = hostResults.filter(r => r.status !== 'error').length;
406
429
  if (configuredCount === 0) {
@@ -536,8 +559,25 @@ async function setup() {
536
559
  }
537
560
  }
538
561
 
539
- // Run setup when this file is loaded (invoked by cli.js or npx vibe-setup)
540
- setup().catch(err => {
541
- console.error(`${colors.red}Setup failed: ${err.message}${colors.reset}`);
542
- process.exit(1);
543
- });
562
+ // Run setup when this file is loaded (invoked by cli.js or npx vibe-setup).
563
+ //
564
+ // Requiring this module USED to be indistinguishable from running it, which is a
565
+ // live hazard rather than a style point: it means anything that so much as inspects
566
+ // setup.js opens a browser, writes host configs and authenticates against production.
567
+ // It happened during a rehearsal. VIBE_SETUP_NO_AUTORUN lets a caller load the module
568
+ // to look at it — nothing in normal use sets that variable.
569
+ if (process.env.VIBE_SETUP_NO_AUTORUN !== '1') {
570
+ setup().catch(err => {
571
+ console.error(`${colors.red}Setup failed: ${err.message}${colors.reset}`);
572
+ process.exit(1);
573
+ });
574
+ }
575
+
576
+ // Named exports for tests and for callers that want a piece rather than the whole
577
+ // ceremony. Deliberately narrow.
578
+ module.exports = {
579
+ setup,
580
+ findClaudeConfig,
581
+ configureAllHosts,
582
+ testConnection,
583
+ };
package/tools/_shared.js CHANGED
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  const config = require('../config');
9
+ const { inertField } = require('../incoming');
9
10
  const store = require('../store');
10
11
 
11
12
  // ─────────────────────────────────────────────────────────────
@@ -255,12 +256,18 @@ function pickDormantTarget(others) {
255
256
  // strip any double quotes/backslashes the ship text itself contains (ship
256
257
  // content is arbitrary) and single-quote the snippet — otherwise an embedded
257
258
  // quote closes the command early and breaks the paste.
259
+ // NO FOREIGN TEXT INSIDE A COMMAND WE ARE SUGGESTING SOMEONE RUN.
260
+ //
261
+ // The opener used to quote another user's ship description inside a ready-to-run
262
+ // `vibe dm @x "..."` line. Inerting it (collapsing newlines, defanging markup) made
263
+ // the injection harder but kept the wrong shape: attacker-authored words sitting in
264
+ // an executable-looking instruction. The fix is not better escaping — it is that
265
+ // the command contains only OUR words. What they shipped is shown separately, as
266
+ // labelled data, where it belongs.
258
267
  const rawShip = pick.lastShip && pick.lastShip.what;
259
- const shipped = rawShip ? String(rawShip).replace(/["\\]/g, '').trim().slice(0, 48) : '';
260
- const opener = shipped
261
- ? `hey! saw you shipped '${shipped}' — what are you building?`
262
- : `hey! what are you building?`;
263
- return { handle: pick.handle, opener };
268
+ const shippedNote = rawShip ? inertField(rawShip, 48) : '';
269
+ const opener = 'hey! what are you building?';
270
+ return { handle: pick.handle, opener, shippedNote };
264
271
  }
265
272
 
266
273
  /**
@@ -278,8 +285,14 @@ function firstDmNudge(others, threads) {
278
285
  if (hasEverMessaged(threads)) return '';
279
286
  const target = pickDormantTarget(others);
280
287
  if (!target) return '';
288
+ // What they shipped is shown as DATA, on its own line, outside the command — the
289
+ // command itself contains only our words.
290
+ const context = target.shippedNote
291
+ ? `\n _they last shipped:_ "${target.shippedNote}"`
292
+ : '';
281
293
  return (
282
294
  `\n\n**👋 You haven't messaged anyone yet — @${target.handle} is around right now:**` +
295
+ context +
283
296
  `\n vibe dm @${target.handle} "${target.opener}"`
284
297
  );
285
298
  }
package/version.json CHANGED
@@ -1,10 +1,12 @@
1
1
  {
2
- "version": "0.8.4",
2
+ "version": "0.8.5",
3
3
  "updated": "2026-07-31",
4
- "changelog": "Setup told every new user the service was unreachable. The health check asked the server for one word and the server answers another, so step 3 of every first run reported 'could not reach slashvibe.dev' \u2014 with troubleshooting steps \u2014 to people whose connection was fine. Fixed by letting HTTP answer reachability instead of guessing at vocabulary. Setup also claimed to configure Claude Code on machines that had never installed it; all three coding agents are now detected the same way.",
4
+ "changelog": "Identity is one thing now. Whoever your credential says you are is who the client shows, routes and self-checks as \u2014 previously a saved handle could name one account while requests went out as another. Presence waits for the server to confirm you before it turns green, so a stale or forged session no longer announces itself. Changing your handle is turned off for now: your messages and tasks are still tied to it, and both rename paths are closed rather than one. A suggested first message no longer contains anyone else's words.",
5
5
  "features": [
6
- "The connection test agrees with the server it is testing",
7
- "Coding agents are detected, not assumed \u2014 no more configuring a host you don't have"
6
+ "The credential names you everywhere the client asks who you are",
7
+ "Presence turns green only after the server confirms the session",
8
+ "Handle changes are off \u2014 both routes, not one \u2014 until ownership moves off handles",
9
+ "Suggested messages contain only our words; what someone shipped is shown as data"
8
10
  ],
9
11
  "deprecated": [],
10
12
  "breaking": false,