slashvibe-mcp 0.8.4 → 0.8.6
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 +44 -6
- package/package.json +4 -3
- package/setup.js +51 -11
- package/tools/_shared.js +18 -5
- package/tools/dm.js +19 -2
- package/tools/inbox.js +22 -17
- package/tools/init.js +2 -2
- package/tools/start.js +1 -1
- package/tools/who.js +22 -20
- package/version.json +6 -4
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.
|
|
@@ -230,7 +241,10 @@ async function getPresenceFooter() {
|
|
|
230
241
|
|
|
231
242
|
// The status line. Counts only — no mood inference, no nudges.
|
|
232
243
|
const parts = ['vibe'];
|
|
233
|
-
|
|
244
|
+
// "2 online" under a board showing three rows — one of them you — reads as a
|
|
245
|
+
// contradiction, even though both numbers are right. The footer counts the room
|
|
246
|
+
// AROUND you, so it should say so.
|
|
247
|
+
if (onlineCount > 0) parts.push(`${onlineCount} other${onlineCount === 1 ? '' : 's'}`);
|
|
234
248
|
if (unreadCount > 0) parts.push(`**${unreadCount} unread**`);
|
|
235
249
|
if (liveCount > 0) parts.push(`${liveCount} live`);
|
|
236
250
|
let footer = `\n\n────────────────────────\n${parts.join(' · ')}`;
|
|
@@ -674,13 +688,37 @@ class VibeMCPServer {
|
|
|
674
688
|
const handle = authStore.getHandle();
|
|
675
689
|
if (!handle || !authStore.isAuthenticated()) return;
|
|
676
690
|
|
|
691
|
+
// ASK THE SERVER BEFORE CLAIMING ANYTHING.
|
|
692
|
+
//
|
|
693
|
+
// I added an explicit verified-vs-saved state and then gated nothing on it, so
|
|
694
|
+
// this still announced on the strength of "a token-shaped string exists on
|
|
695
|
+
// disk". A forged JWT-shaped value in config or Buddy's auth.json produced a
|
|
696
|
+
// confident "🟢 Auto-connected as @victim" and a heartbeat against production.
|
|
697
|
+
// Holding a credential is not the same as having one that works, and the green
|
|
698
|
+
// dot is the one thing in this product that must never be a guess.
|
|
699
|
+
let verified;
|
|
700
|
+
try {
|
|
701
|
+
verified = await store.verifyAuthToken(authStore.getToken());
|
|
702
|
+
} catch (e) {
|
|
703
|
+
verified = null;
|
|
704
|
+
}
|
|
705
|
+
if (!verified || !verified.valid) {
|
|
706
|
+
// Definitive rejection means the session is dead; unreachable means unknown.
|
|
707
|
+
// Neither earns a green dot, and neither is worth shouting about at startup.
|
|
708
|
+
if (verified && verified.definitive) {
|
|
709
|
+
process.stderr.write('/vibe: saved session is no longer valid — say "vibe init" to sign in again\n');
|
|
710
|
+
}
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
authStore.markVerified(verified.handle);
|
|
714
|
+
|
|
677
715
|
const one_liner = config.getOneLiner() || '';
|
|
678
716
|
|
|
679
|
-
// Start presence heartbeat
|
|
680
|
-
presence.start(
|
|
717
|
+
// Start presence heartbeat — now, and only now, is this handle a fact.
|
|
718
|
+
presence.start(authStore.getHandle(), one_liner);
|
|
681
719
|
|
|
682
720
|
// Log quietly (only to stderr, not intrusive)
|
|
683
|
-
process.stderr.write(`🟢 Auto-connected as @${
|
|
721
|
+
process.stderr.write(`🟢 Auto-connected as @${authStore.getHandle()}\n`);
|
|
684
722
|
} catch (error) {
|
|
685
723
|
// Silent fail - don't block startup
|
|
686
724
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slashvibe-mcp",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.6",
|
|
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
|
@@ -39,7 +39,7 @@ const API_BASE = 'https://www.slashvibe.dev';
|
|
|
39
39
|
function printBanner() {
|
|
40
40
|
console.log(`
|
|
41
41
|
${colors.green} █░█ █ █▄▄ █▀▀${colors.reset}
|
|
42
|
-
${colors.green} ▀▄▀ █ █▄█ ██▄${colors.reset} ${colors.dim}
|
|
42
|
+
${colors.green} ▀▄▀ █ █▄█ ██▄${colors.reset} ${colors.dim}ask here · answer there${colors.reset}
|
|
43
43
|
──────────────────────────────────────────────────
|
|
44
44
|
`);
|
|
45
45
|
}
|
|
@@ -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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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
|
|
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/tools/dm.js
CHANGED
|
@@ -110,10 +110,27 @@ async function handler(args) {
|
|
|
110
110
|
origin: origin || 'composed',
|
|
111
111
|
});
|
|
112
112
|
|
|
113
|
-
// Check for errors
|
|
113
|
+
// Check for errors.
|
|
114
|
+
//
|
|
115
|
+
// The generic tail — "please try again, check your connection" — used to be appended
|
|
116
|
+
// to EVERY failure, including the ones that already name their own fix. An expired
|
|
117
|
+
// session then read as: run `vibe init`, and also try again, and also check your
|
|
118
|
+
// connection. Three instructions, two of them wrong, at the moment someone's first
|
|
119
|
+
// message failed. docs/ROOM-TONE.md: an error names the fix in the same sentence as
|
|
120
|
+
// the problem — which means not adding a second, contradictory one after it.
|
|
121
|
+
//
|
|
122
|
+
// Errors carrying a specific remedy (auth_expired, not_signed_in, handle_not_found)
|
|
123
|
+
// are shown as-is. Only genuinely unknown failures get the retry hint.
|
|
114
124
|
if (result && result.error) {
|
|
125
|
+
const REMEDY_CARRYING = new Set([
|
|
126
|
+
'auth_expired', 'not_signed_in', 'auth_failed',
|
|
127
|
+
'handle_not_found', 'self_dm', 'storage_error',
|
|
128
|
+
]);
|
|
129
|
+
const detail = result.message || 'the message did not send.';
|
|
115
130
|
return {
|
|
116
|
-
display:
|
|
131
|
+
display: REMEDY_CARRYING.has(result.error)
|
|
132
|
+
? detail
|
|
133
|
+
: `${detail}\n\n_worth one retry — if it keeps failing, say_ \`vibe doctor\`_._`,
|
|
117
134
|
};
|
|
118
135
|
}
|
|
119
136
|
|
package/tools/inbox.js
CHANGED
|
@@ -183,7 +183,7 @@ async function handler(args) {
|
|
|
183
183
|
// If there are guest messages but no DMs, show guest messages
|
|
184
184
|
if (guestSection) {
|
|
185
185
|
return {
|
|
186
|
-
display: guestSection + '📭
|
|
186
|
+
display: guestSection + '📭 no messages yet — say "dm @someone" to start one.',
|
|
187
187
|
actions: formatActions(actions.recommendedConnections([]))
|
|
188
188
|
};
|
|
189
189
|
}
|
|
@@ -197,6 +197,9 @@ async function handler(args) {
|
|
|
197
197
|
'All caught up!',
|
|
198
198
|
'No messages yet.'
|
|
199
199
|
);
|
|
200
|
+
// NOTE: reachable only with VIBE_EXTRAS — the suggestion block is off by
|
|
201
|
+
// default (private fabric: no proposing strangers). The default empty inbox
|
|
202
|
+
// is the branch below.
|
|
200
203
|
return {
|
|
201
204
|
display,
|
|
202
205
|
actions: recommendations.actions
|
|
@@ -206,20 +209,17 @@ async function handler(args) {
|
|
|
206
209
|
console.log('[inbox] recommendations error:', e.message);
|
|
207
210
|
}
|
|
208
211
|
|
|
209
|
-
//
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if (!repliedToSeth) {
|
|
216
|
-
cta = 'Reply to @vibe to get started!';
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
} catch (e) {}
|
|
220
|
-
|
|
212
|
+
// THE FIRST SCREEN A NEW INVITEE SEES. Designed, not apologised for: an empty
|
|
213
|
+
// room should say what to do next in the same breath as being empty.
|
|
214
|
+
//
|
|
215
|
+
// The old copy pointed at "@vibe" — a system account — as the thing to reply to.
|
|
216
|
+
// Under docs/PRIVATE-FABRIC.md you are here because a PERSON invited you, and the
|
|
217
|
+
// useful first move is answering them, not a bot.
|
|
221
218
|
return {
|
|
222
|
-
display:
|
|
219
|
+
display:
|
|
220
|
+
'📭 no messages yet.\n\n' +
|
|
221
|
+
'_whoever invited you is the place to start:_ `dm @them`\n' +
|
|
222
|
+
"_they'll get it in their next session — neither of you has to be online._",
|
|
223
223
|
actions: formatActions(actions.recommendedConnections([]))
|
|
224
224
|
};
|
|
225
225
|
}
|
|
@@ -284,10 +284,15 @@ async function handler(args) {
|
|
|
284
284
|
console.log('[inbox] recommendations error:', e.message);
|
|
285
285
|
}
|
|
286
286
|
|
|
287
|
-
//
|
|
288
|
-
|
|
287
|
+
// Caught up, WITH history — a different state from "you just arrived", and they
|
|
288
|
+
// were rendering as the same sentence. "Recent: @a, @b, @c" also named the
|
|
289
|
+
// handles without saying what they were: recent conversations, recent arrivals,
|
|
290
|
+
// or a suggestion? Naming the relationship costs three words.
|
|
291
|
+
const recentHandles = sorted.slice(0, 3).map((th) => `@${th.handle}`).join(', ');
|
|
289
292
|
return {
|
|
290
|
-
display:
|
|
293
|
+
display: recentHandles
|
|
294
|
+
? `📭 nothing unread.\n\n_your recent threads:_ ${recentHandles}\n_open one with_ \`vibe inbox @handle\``
|
|
295
|
+
: '📭 nothing unread.',
|
|
291
296
|
actions: formatActions(actions.recommendedConnections([]))
|
|
292
297
|
};
|
|
293
298
|
}
|
package/tools/init.js
CHANGED
|
@@ -59,7 +59,7 @@ function generatePreAuthBanner(presence) {
|
|
|
59
59
|
const label = formatPresenceLabel(presence);
|
|
60
60
|
const onlineText = label ? `🟢 ${label}` : '🟢 join the crew';
|
|
61
61
|
return `
|
|
62
|
-
█░█ █ █▄▄ █▀▀
|
|
62
|
+
█░█ █ █▄▄ █▀▀ ask here · answer there
|
|
63
63
|
▀▄▀ █ █▄█ ██▄ ${onlineText}
|
|
64
64
|
──────────────────────────────────────────────────
|
|
65
65
|
`;
|
|
@@ -75,7 +75,7 @@ function generateAuthBanner(handle, unreadCount, presence) {
|
|
|
75
75
|
const unreadCol = unreadCount > 0 ? `📬 ${unreadCount} unread`.padEnd(14) : `📬 0 messages`.padEnd(14);
|
|
76
76
|
const onlineText = formatPresenceLabel(presence) || 'just you so far';
|
|
77
77
|
|
|
78
|
-
return ` █░█ █ █▄▄ █▀▀ ${handleCol}
|
|
78
|
+
return ` █░█ █ █▄▄ █▀▀ ${handleCol} ask here · answer there
|
|
79
79
|
▀▄▀ █ █▄█ ██▄ ${unreadCol} 🟢 ${onlineText}
|
|
80
80
|
──────────────────────────────────────────────────`;
|
|
81
81
|
}
|
package/tools/start.js
CHANGED
|
@@ -167,7 +167,7 @@ function generateWelcomeCard({ handle, onlineCount, unreadCount, versionInfo })
|
|
|
167
167
|
versionSuffix = ' ⬆️';
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
return ` █░█ █ █▄▄ █▀▀ ${handleCol}
|
|
170
|
+
return ` █░█ █ █▄▄ █▀▀ ${handleCol} ask here · answer there
|
|
171
171
|
▀▄▀ █ █▄█ ██▄ ${unreadCol} 🟢 ${onlineCount} online${versionSuffix}
|
|
172
172
|
──────────────────────────────────────────────────`;
|
|
173
173
|
}
|
package/tools/who.js
CHANGED
|
@@ -312,16 +312,20 @@ say hi to **@vibe** (the platform account, always around), or send
|
|
|
312
312
|
}
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
-
//
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
315
|
+
// One line, and it must name a tool that is ACTUALLY REGISTERED.
|
|
316
|
+
//
|
|
317
|
+
// This rotated three suggestions at random, one of which advertised `play` — a
|
|
318
|
+
// culture-layer tool that 0.8.0 moved behind VIBE_EXTRAS. So the default board
|
|
319
|
+
// routinely told people to use something their session does not have. That is the
|
|
320
|
+
// help-drift class we already have a guard for on `vibe_help`; the board escaped it.
|
|
321
|
+
//
|
|
322
|
+
// Randomness went too: a board that says something different every time reads as
|
|
323
|
+
// decoration. The hint now follows from state — if you have not set a status,
|
|
324
|
+
// that is the useful thing to say.
|
|
323
325
|
display += `---\n`;
|
|
324
|
-
display +=
|
|
326
|
+
display += config.getOneLiner()
|
|
327
|
+
? `say "dm @handle" to reach someone`
|
|
328
|
+
: `say "status <what you're on>" so others see what you're building`;
|
|
325
329
|
|
|
326
330
|
// Check for unread to add urgency
|
|
327
331
|
try {
|
|
@@ -336,17 +340,15 @@ say hi to **@vibe** (the platform account, always around), or send
|
|
|
336
340
|
|
|
337
341
|
}
|
|
338
342
|
|
|
339
|
-
//
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
// Silent fail — genesis display is nice-to-have
|
|
349
|
-
}
|
|
343
|
+
// The scarcity line is gone.
|
|
344
|
+
//
|
|
345
|
+
// "genesis is full — 282 builders" was a growth-era artifact on two counts. It sold
|
|
346
|
+
// scarcity, which docs/PRIVATE-FABRIC.md removed from scope; and the number was a
|
|
347
|
+
// handle count, not people — 282 "builders" on a network where a handful of humans
|
|
348
|
+
// are actually active reads as a claim the room immediately contradicts. Nobody
|
|
349
|
+
// invited into a private fabric needs to be told the list is closed.
|
|
350
|
+
//
|
|
351
|
+
// What the board says about who is here is now only what it can see: the rows above.
|
|
350
352
|
|
|
351
353
|
// If not initialized, append join prompt instead of actions/unread
|
|
352
354
|
if (!isInit) {
|
package/version.json
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.8.
|
|
2
|
+
"version": "0.8.6",
|
|
3
3
|
"updated": "2026-07-31",
|
|
4
|
-
"changelog": "
|
|
4
|
+
"changelog": "Who is here is no longer public. Counts are; handles are not \u2014 sign in to see names, which every normal install already does. The rest is the first ninety minutes: an empty inbox now says what to do next instead of 'all caught up' to someone who never had a message, the board stops advertising tools your session does not have, and a failed message names one fix instead of three contradictory ones.",
|
|
5
5
|
"features": [
|
|
6
|
-
"
|
|
7
|
-
"
|
|
6
|
+
"Presence names require a credential; counts stay public",
|
|
7
|
+
"Empty inbox tells you what to do next, and knows arriving from caught-up",
|
|
8
|
+
"The board only suggests commands your session actually has",
|
|
9
|
+
"Errors name one fix and stop"
|
|
8
10
|
],
|
|
9
11
|
"deprecated": [],
|
|
10
12
|
"breaking": false,
|