slashvibe-mcp 0.8.3 → 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 +20 -0
- package/index.js +40 -5
- package/package.json +4 -3
- package/setup.js +95 -17
- package/tools/_shared.js +18 -5
- package/version.json +6 -8
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
|
-
//
|
|
69
|
-
|
|
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(
|
|
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 @${
|
|
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.
|
|
3
|
+
"version": "0.8.5",
|
|
4
4
|
"mcpName": "io.github.vibecodinginc/vibe",
|
|
5
|
-
"description": "Presence + messaging for terminal coding agents (Claude Code, Codex, Cursor)
|
|
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
|
@@ -93,8 +93,17 @@ function findClaudeConfig() {
|
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
//
|
|
97
|
-
|
|
96
|
+
// Nothing found. Returning a path anyway made setup report "→ Claude Code" and
|
|
97
|
+
// "configured" on machines with no Claude Code at all — a success message for work
|
|
98
|
+
// that helps nobody, and the reason "no supported host" was unreachable. Callers
|
|
99
|
+
// now get null and decide honestly; the default path is only for the create case,
|
|
100
|
+
// which is signalled explicitly.
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Where we WOULD write a Claude Code config if the user wants one created. */
|
|
105
|
+
function defaultClaudeConfigPath() {
|
|
106
|
+
return path.join(os.homedir(), '.claude.json');
|
|
98
107
|
}
|
|
99
108
|
|
|
100
109
|
/**
|
|
@@ -140,10 +149,18 @@ function addVibeToJsonConfig(configPath) {
|
|
|
140
149
|
}
|
|
141
150
|
|
|
142
151
|
/**
|
|
143
|
-
* Configure Claude Code (~/.claude.json et al).
|
|
152
|
+
* Configure Claude Code (~/.claude.json et al).
|
|
153
|
+
*
|
|
154
|
+
* Cursor and Codex only configure when their directories exist; Claude Code used to
|
|
155
|
+
* configure unconditionally, inventing ~/.claude.json on machines that had never run
|
|
156
|
+
* it. That is what made setup announce "→ Claude Code · configured" during a walk on
|
|
157
|
+
* a clean machine with no Claude Code installed. Same rule for all three hosts now:
|
|
158
|
+
* we write where the host actually lives, and say so honestly when none is found.
|
|
144
159
|
*/
|
|
145
|
-
function configureClaudeCode() {
|
|
146
|
-
const
|
|
160
|
+
function configureClaudeCode({ createIfMissing = false } = {}) {
|
|
161
|
+
const found = findClaudeConfig();
|
|
162
|
+
const configPath = found || (createIfMissing ? defaultClaudeConfigPath() : null);
|
|
163
|
+
if (!configPath) return null; // Claude Code not installed here
|
|
147
164
|
try {
|
|
148
165
|
return { host: 'Claude Code', path: configPath, status: addVibeToJsonConfig(configPath) };
|
|
149
166
|
} catch (e) {
|
|
@@ -231,19 +248,47 @@ function configureCodex() {
|
|
|
231
248
|
* Configure every detected host. Returns results for the ones present.
|
|
232
249
|
*/
|
|
233
250
|
function configureAllHosts() {
|
|
234
|
-
|
|
251
|
+
const found = [configureClaudeCode(), configureCodex(), configureCursor()].filter(Boolean);
|
|
252
|
+
// No host detected at all — someone ran this before installing a coding agent, or
|
|
253
|
+
// in a container. Rather than silently doing nothing (or, as before, inventing a
|
|
254
|
+
// Claude Code config to have something to report), create the Claude Code config so
|
|
255
|
+
// /vibe is wired the moment they install it, and let the caller say which case it is.
|
|
256
|
+
if (found.length === 0) {
|
|
257
|
+
const created = configureClaudeCode({ createIfMissing: true });
|
|
258
|
+
return created ? [{ ...created, hostWasMissing: true }] : [];
|
|
259
|
+
}
|
|
260
|
+
return found;
|
|
235
261
|
}
|
|
236
262
|
|
|
237
263
|
/**
|
|
238
264
|
* Test API connection
|
|
239
265
|
*/
|
|
240
266
|
async function testConnection() {
|
|
267
|
+
// Found by actually RUNNING this on a clean machine, which no amount of reading
|
|
268
|
+
// the code surfaced: /api/health answers `{"status":"healthy"}`, and this asked
|
|
269
|
+
// for `'ok'`. So every first run — on a perfectly good connection — printed
|
|
270
|
+
// "Could not reach slashvibe.dev" and three troubleshooting steps, to a person
|
|
271
|
+
// whose very first experience of /vibe was being told it was broken.
|
|
272
|
+
//
|
|
273
|
+
// The client no longer guesses at the server's vocabulary. Reachability is what
|
|
274
|
+
// HTTP already answers; the body is only allowed to veto by reporting an explicit
|
|
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);
|
|
241
281
|
try {
|
|
242
|
-
const response = await fetch(`${API_BASE}/api/health
|
|
243
|
-
|
|
244
|
-
|
|
282
|
+
const response = await fetch(`${API_BASE}/api/health`, { signal: controller.signal });
|
|
283
|
+
if (!response.ok) return false;
|
|
284
|
+
const data = await response.json().catch(() => ({}));
|
|
285
|
+
if (data.success === false) return false;
|
|
286
|
+
if (typeof data.status === 'string' && /unhealthy|degraded|down|error/i.test(data.status)) return false;
|
|
287
|
+
return true;
|
|
245
288
|
} catch (e) {
|
|
246
289
|
return false;
|
|
290
|
+
} finally {
|
|
291
|
+
clearTimeout(timer);
|
|
247
292
|
}
|
|
248
293
|
}
|
|
249
294
|
|
|
@@ -294,6 +339,13 @@ async function getOnlineUsers() {
|
|
|
294
339
|
* Open URL in browser
|
|
295
340
|
*/
|
|
296
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
|
+
}
|
|
297
349
|
const platform = process.platform;
|
|
298
350
|
let command;
|
|
299
351
|
|
|
@@ -348,10 +400,18 @@ async function setup() {
|
|
|
348
400
|
// Step 1: Detect coding agents on this machine
|
|
349
401
|
printStep(1, 'Detecting coding agents...', 'running');
|
|
350
402
|
const homeDir = os.homedir();
|
|
351
|
-
|
|
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');
|
|
352
410
|
if (fs.existsSync(codexHome())) detected.push('Codex');
|
|
353
411
|
if (fs.existsSync(path.join(homeDir, '.cursor'))) detected.push('Cursor');
|
|
354
|
-
console.log(
|
|
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}`);
|
|
355
415
|
printStep(1, 'Detecting coding agents...', 'done');
|
|
356
416
|
|
|
357
417
|
// Step 2: Add /vibe MCP server to every detected host
|
|
@@ -362,7 +422,8 @@ async function setup() {
|
|
|
362
422
|
: r.status === 'exists' ? 'already configured'
|
|
363
423
|
: `failed: ${r.error}`;
|
|
364
424
|
const icon = r.status === 'error' ? colors.yellow + '!' : colors.green + '✓';
|
|
365
|
-
|
|
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}`);
|
|
366
427
|
}
|
|
367
428
|
const configuredCount = hostResults.filter(r => r.status !== 'error').length;
|
|
368
429
|
if (configuredCount === 0) {
|
|
@@ -498,8 +559,25 @@ async function setup() {
|
|
|
498
559
|
}
|
|
499
560
|
}
|
|
500
561
|
|
|
501
|
-
// Run setup when this file is loaded (invoked by cli.js or npx vibe-setup)
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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
|
|
260
|
-
const opener =
|
|
261
|
-
|
|
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,14 +1,12 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.8.
|
|
2
|
+
"version": "0.8.5",
|
|
3
3
|
"updated": "2026-07-31",
|
|
4
|
-
"changelog": "
|
|
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
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"Presence names the account your credential names",
|
|
11
|
-
"Inbound messages are never seated beside an instruction to act on them"
|
|
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"
|
|
12
10
|
],
|
|
13
11
|
"deprecated": [],
|
|
14
12
|
"breaking": false,
|