slashvibe-mcp 0.8.5 → 0.8.7
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/auth-store.js +84 -1
- package/cli.js +30 -4
- package/config.js +15 -0
- package/index.js +4 -1
- package/package.json +2 -2
- package/setup.js +19 -4
- package/store/api.js +30 -0
- package/tools/dm.js +19 -2
- package/tools/inbox.js +22 -17
- package/tools/init.js +2 -2
- package/tools/start.js +10 -39
- package/tools/who.js +46 -20
- package/version.json +7 -7
package/auth-store.js
CHANGED
|
@@ -26,6 +26,10 @@ let _hydrated = false;
|
|
|
26
26
|
// unlocking a tool on the strength of a filename is how a green dot comes to mean
|
|
27
27
|
// nothing (issues #107, #110).
|
|
28
28
|
let _verified = false;
|
|
29
|
+
// We held a credential and could not tell whose it was. Distinct from "no credential":
|
|
30
|
+
// signed out is an honest state, but a rejected token means the name on disk is not
|
|
31
|
+
// backed by anything, and nothing may present it as the current identity (#107/#110).
|
|
32
|
+
let _rejected = false;
|
|
29
33
|
|
|
30
34
|
/**
|
|
31
35
|
* Hydrate auth state from disk (call once at MCP startup)
|
|
@@ -83,6 +87,7 @@ function hydrate() {
|
|
|
83
87
|
// A token we cannot attribute is one we must not act under.
|
|
84
88
|
console.error('[auth-store] Token carries no identity — treating as signed out');
|
|
85
89
|
_token = null;
|
|
90
|
+
_rejected = true;
|
|
86
91
|
} else if (_token && stored && stored !== _handle) {
|
|
87
92
|
console.error(
|
|
88
93
|
`[auth-store] Stored handle @${stored} does not own this session — acting as @${_handle}`
|
|
@@ -112,12 +117,13 @@ function setToken(token, { verified = false } = {}) {
|
|
|
112
117
|
// Same rule hydrate() applies, applied here too — a credential we cannot
|
|
113
118
|
// attribute must not become the session.
|
|
114
119
|
console.error('[auth-store] Refusing a token with no identity');
|
|
115
|
-
_token = null; _handle = null; _verified = false;
|
|
120
|
+
_token = null; _handle = null; _verified = false; _rejected = true;
|
|
116
121
|
return;
|
|
117
122
|
}
|
|
118
123
|
}
|
|
119
124
|
|
|
120
125
|
const hadToken = !!_token;
|
|
126
|
+
_rejected = false;
|
|
121
127
|
_token = token;
|
|
122
128
|
_verified = !!token && verified;
|
|
123
129
|
|
|
@@ -152,6 +158,25 @@ function getToken() {
|
|
|
152
158
|
* @param {string} handle - User handle (without @)
|
|
153
159
|
*/
|
|
154
160
|
function setHandle(handle) {
|
|
161
|
+
// A handle may be REMEMBERED, never asserted over a credential.
|
|
162
|
+
//
|
|
163
|
+
// `vibe_init` called this immediately after setToken(), passing the callback's separate
|
|
164
|
+
// `handle` field — so the identity derived from the token was overwritten by a value
|
|
165
|
+
// that merely travelled alongside it. When they agree nothing happens; when they
|
|
166
|
+
// disagree the client acts as one principal and displays another, which is issue #107
|
|
167
|
+
// recreated at the exact moment a new person signs in.
|
|
168
|
+
//
|
|
169
|
+
// tests/identity-binding.test.js did not catch it: it asserts setToken() AFTER
|
|
170
|
+
// setHandle(), where the token re-anchors. The real order is the reverse.
|
|
171
|
+
if (_token) {
|
|
172
|
+
const owner = handleFromToken(_token);
|
|
173
|
+
if (owner && handle && handle !== owner) {
|
|
174
|
+
console.error(
|
|
175
|
+
`[auth-store] Ignoring handle @${handle} — this credential belongs to @${owner}`
|
|
176
|
+
);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
155
180
|
_handle = handle;
|
|
156
181
|
}
|
|
157
182
|
|
|
@@ -163,6 +188,52 @@ function getHandle() {
|
|
|
163
188
|
return _handle;
|
|
164
189
|
}
|
|
165
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Did we hold a credential we could not attribute?
|
|
193
|
+
*
|
|
194
|
+
* "Signed out" and "signed in as nobody" look identical from `getHandle()` — both null.
|
|
195
|
+
* They are not the same fact, and callers that fall back to a remembered name need to
|
|
196
|
+
* tell them apart: with no credential the name on disk is the best we have, but with a
|
|
197
|
+
* REJECTED one it is a name nothing supports (#110).
|
|
198
|
+
*
|
|
199
|
+
* @returns {boolean}
|
|
200
|
+
*/
|
|
201
|
+
function hasRejectedCredential() {
|
|
202
|
+
return _rejected;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Is a stored credential actually usable right now?
|
|
207
|
+
*
|
|
208
|
+
* "A token exists" and "you are signed in" are different facts, and conflating them is
|
|
209
|
+
* how `npx slashvibe-mcp` came to answer "/vibe is configured" on a machine holding a
|
|
210
|
+
* credential that had expired 23 days earlier — then advise restarting the coding agent,
|
|
211
|
+
* which cannot help. The file existed, so the check passed.
|
|
212
|
+
*
|
|
213
|
+
* Local and cheap: decodes `exp` rather than asking the server, because this runs on the
|
|
214
|
+
* recovery path where the network may be the thing that is wrong. It cannot prove a token
|
|
215
|
+
* is GOOD — only the server can — but it reliably catches the case that actually stranded
|
|
216
|
+
* someone, which is a credential that is definitively over.
|
|
217
|
+
*
|
|
218
|
+
* @returns {{ ok: boolean, reason: 'none'|'unattributable'|'expired'|'usable', handle: string|null, expiresAt: number|null }}
|
|
219
|
+
*/
|
|
220
|
+
function inspectToken(token) {
|
|
221
|
+
if (!token) return { ok: false, reason: 'none', handle: null, expiresAt: null };
|
|
222
|
+
const who = handleFromToken(token);
|
|
223
|
+
if (!who) return { ok: false, reason: 'unattributable', handle: null, expiresAt: null };
|
|
224
|
+
let exp = null;
|
|
225
|
+
try {
|
|
226
|
+
const part = String(token).split('.')[1];
|
|
227
|
+
exp = JSON.parse(Buffer.from(part, 'base64url').toString('utf8')).exp ?? null;
|
|
228
|
+
} catch { exp = null; }
|
|
229
|
+
// No exp claim is not a failure — legacy tokens carry none, and refusing them here
|
|
230
|
+
// would sign people out to fix a display bug.
|
|
231
|
+
if (exp && exp * 1000 <= Date.now()) {
|
|
232
|
+
return { ok: false, reason: 'expired', handle: who, expiresAt: exp * 1000 };
|
|
233
|
+
}
|
|
234
|
+
return { ok: true, reason: 'usable', handle: who, expiresAt: exp ? exp * 1000 : null };
|
|
235
|
+
}
|
|
236
|
+
|
|
166
237
|
/**
|
|
167
238
|
* Set one-liner (what user is building)
|
|
168
239
|
* @param {string} oneLiner - One-liner description
|
|
@@ -210,6 +281,16 @@ function clear() {
|
|
|
210
281
|
_handle = null;
|
|
211
282
|
_oneLiner = null;
|
|
212
283
|
_verified = false;
|
|
284
|
+
_rejected = false;
|
|
285
|
+
// Clearing must also forget that we ever loaded, or `hydrate()` returns early forever
|
|
286
|
+
// and the store stays empty while disk holds a perfectly good credential. Everything
|
|
287
|
+
// that reads identity then falls through to the file — which is issue #107 reappearing
|
|
288
|
+
// by a different route, and silently, since the file usually names the right person.
|
|
289
|
+
//
|
|
290
|
+
// NOTE for whoever wires logout to this: clearing memory is not signing out. Disk is
|
|
291
|
+
// re-read on the next question, so a logout must also remove the credential from
|
|
292
|
+
// config.json — otherwise the session comes straight back.
|
|
293
|
+
_hydrated = false;
|
|
213
294
|
console.error('[auth-store] Cleared');
|
|
214
295
|
}
|
|
215
296
|
|
|
@@ -233,6 +314,8 @@ module.exports = {
|
|
|
233
314
|
getToken,
|
|
234
315
|
setHandle,
|
|
235
316
|
getHandle,
|
|
317
|
+
hasRejectedCredential,
|
|
318
|
+
inspectToken,
|
|
236
319
|
setOneLiner,
|
|
237
320
|
getOneLiner,
|
|
238
321
|
isAuthenticated,
|
package/cli.js
CHANGED
|
@@ -19,12 +19,38 @@ if (args.includes('setup')) {
|
|
|
19
19
|
} else if (process.stdin.isTTY) {
|
|
20
20
|
// Running from terminal (not as MCP server)
|
|
21
21
|
const config = require('./config');
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
const authStore = require('./auth-store');
|
|
23
|
+
|
|
24
|
+
// A TOKEN EXISTING IS NOT BEING SIGNED IN.
|
|
25
|
+
//
|
|
26
|
+
// This used to pass on `config.getAuthToken()` being truthy, which is true of a
|
|
27
|
+
// credential that expired months ago. On a Mac Studio holding a token 23 days dead it
|
|
28
|
+
// printed "/vibe is configured" and suggested restarting the coding agent — advice that
|
|
29
|
+
// cannot help, given to someone whose only real problem was that they were signed out.
|
|
30
|
+
// The recovery path told them there was nothing to recover.
|
|
31
|
+
const cred = authStore.inspectToken(config.getAuthToken());
|
|
32
|
+
|
|
33
|
+
if (!config.isInitialized() && !cred.ok) {
|
|
34
|
+
// Never signed in — set up.
|
|
35
|
+
require('./setup.js');
|
|
36
|
+
} else if (!cred.ok && cred.reason !== 'none') {
|
|
37
|
+
// Signed in once, not any more. Say which, and fix it rather than describing it.
|
|
38
|
+
const when = cred.expiresAt
|
|
39
|
+
? ` (it expired ${Math.max(1, Math.round((Date.now() - cred.expiresAt) / 86400000))} days ago)`
|
|
40
|
+
: '';
|
|
41
|
+
console.log(cred.handle
|
|
42
|
+
? `Your /vibe sign-in for @${cred.handle} is no longer valid${when}.`
|
|
43
|
+
: `Your /vibe sign-in is no longer valid${when}.`);
|
|
44
|
+
console.log('Signing you back in...');
|
|
45
|
+
console.log('');
|
|
24
46
|
require('./setup.js');
|
|
25
47
|
} else {
|
|
26
|
-
//
|
|
27
|
-
|
|
48
|
+
// Genuinely signed in — say WHO, because a status line that cannot name you is the
|
|
49
|
+
// one that hid this in the first place.
|
|
50
|
+
console.log(cred.handle
|
|
51
|
+
? `/vibe is configured — signed in as @${cred.handle}.`
|
|
52
|
+
: '/vibe is configured.');
|
|
53
|
+
console.log('Restart your coding agent (Claude Code / Codex / Cursor) to connect.');
|
|
28
54
|
console.log('');
|
|
29
55
|
console.log('Commands:');
|
|
30
56
|
console.log(' npx slashvibe-mcp setup — re-run setup wizard');
|
package/config.js
CHANGED
|
@@ -116,10 +116,25 @@ function getHandle() {
|
|
|
116
116
|
//
|
|
117
117
|
// Required lazily: auth-store loads config during hydration, and a top-level import
|
|
118
118
|
// here would close that circle.
|
|
119
|
+
//
|
|
120
|
+
// We hydrate here rather than assuming someone already did. The store answers from
|
|
121
|
+
// memory, so before hydration it answers null — and this function would quietly fall
|
|
122
|
+
// through to the file, which is the exact bug above. That made the fix depend on an
|
|
123
|
+
// ordering nothing enforced: correct inside the MCP server (which hydrates in its
|
|
124
|
+
// constructor) and wrong in any script, CLI path or test that reads identity first.
|
|
125
|
+
// A guarantee that holds only when called in the right order is not a guarantee.
|
|
126
|
+
// hydrate() is idempotent and reads two files it has usually already read.
|
|
119
127
|
try {
|
|
120
128
|
const authStore = require('./auth-store');
|
|
129
|
+
authStore.hydrate();
|
|
121
130
|
const fromCredential = authStore.getHandle();
|
|
122
131
|
if (fromCredential) return fromCredential;
|
|
132
|
+
|
|
133
|
+
// A credential was present and could not be attributed. The remembered name below
|
|
134
|
+
// must NOT step in: it would put a handle on screen that nothing backs, which is
|
|
135
|
+
// this bug wearing the file's clothes. No credential at all is a different and
|
|
136
|
+
// honest state, and falls through.
|
|
137
|
+
if (authStore.hasRejectedCredential()) return null;
|
|
123
138
|
} catch (e) {
|
|
124
139
|
// auth-store unavailable (early boot) — fall through to what is on disk.
|
|
125
140
|
}
|
package/index.js
CHANGED
|
@@ -241,7 +241,10 @@ async function getPresenceFooter() {
|
|
|
241
241
|
|
|
242
242
|
// The status line. Counts only — no mood inference, no nudges.
|
|
243
243
|
const parts = ['vibe'];
|
|
244
|
-
|
|
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'}`);
|
|
245
248
|
if (unreadCount > 0) parts.push(`**${unreadCount} unread**`);
|
|
246
249
|
if (liveCount > 0) parts.push(`${liveCount} live`);
|
|
247
250
|
let footer = `\n\n────────────────────────\n${parts.join(' · ')}`;
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slashvibe-mcp",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.7",
|
|
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) — the /vibe kernel",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"bin": {
|
|
8
8
|
"slashvibe-mcp": "./cli.js",
|
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
|
}
|
|
@@ -274,8 +274,23 @@ async function testConnection() {
|
|
|
274
274
|
// HTTP already answers; the body is only allowed to veto by reporting an explicit
|
|
275
275
|
// unhealthy state.
|
|
276
276
|
// Bounded: a hung connection must not leave a new user staring at a spinner with no
|
|
277
|
-
// way to know whether to wait.
|
|
278
|
-
//
|
|
277
|
+
// way to know whether to wait.
|
|
278
|
+
//
|
|
279
|
+
// "DEGRADED" IS NOT UNREACHABLE, and treating it as such blocked every new install on
|
|
280
|
+
// 2026-08-01. Upstash hit a quota wall, /api/health honestly reported `degraded`
|
|
281
|
+
// because a CACHE was down, and this returned false — so setup told people "Could not
|
|
282
|
+
// reach slashvibe.dev" and advised them to check their internet connection, about a
|
|
283
|
+
// site that was serving perfectly.
|
|
284
|
+
//
|
|
285
|
+
// The reasoning in the version that did this was mine and it was wrong: I blocked on
|
|
286
|
+
// degraded because "the next step is an OAuth round trip". OAuth does not touch the
|
|
287
|
+
// cache. A partial-capability signal about a subsystem we do not need is not a reason
|
|
288
|
+
// to refuse someone the product, and "we cannot reach the server" is not a true thing
|
|
289
|
+
// to say when we just did.
|
|
290
|
+
//
|
|
291
|
+
// Only an explicitly unhealthy service blocks. Everything else proceeds, because the
|
|
292
|
+
// OAuth round trip that follows is a better test of what setup actually needs than any
|
|
293
|
+
// status string.
|
|
279
294
|
const controller = new AbortController();
|
|
280
295
|
const timer = setTimeout(() => controller.abort(), 8000);
|
|
281
296
|
try {
|
|
@@ -283,7 +298,7 @@ async function testConnection() {
|
|
|
283
298
|
if (!response.ok) return false;
|
|
284
299
|
const data = await response.json().catch(() => ({}));
|
|
285
300
|
if (data.success === false) return false;
|
|
286
|
-
if (typeof data.status === 'string' && /
|
|
301
|
+
if (typeof data.status === 'string' && /\bunhealthy\b|\bdown\b/i.test(data.status)) return false;
|
|
287
302
|
return true;
|
|
288
303
|
} catch (e) {
|
|
289
304
|
return false;
|
package/store/api.js
CHANGED
|
@@ -355,6 +355,28 @@ async function getActiveUsers() {
|
|
|
355
355
|
const liveAgents = (result.agents || []).filter(a => a.isLive && a.broadcastRoom);
|
|
356
356
|
const users = [...(result.active || []), ...(result.away || []), ...liveAgents];
|
|
357
357
|
|
|
358
|
+
// SIGNED OUT IS NOT AN EMPTY ROOM.
|
|
359
|
+
//
|
|
360
|
+
// When the caller has no valid credential the server answers `anonymous: true` with
|
|
361
|
+
// empty lists and PUBLIC COUNTS — "2 people are here, sign in to see who". This
|
|
362
|
+
// function used to return only the mapped array, so every caller saw `[]` and could
|
|
363
|
+
// not tell the two apart. `vibe_who` rendered it as "Quiet right now — you're the
|
|
364
|
+
// only one here", which is the most damaging sentence this product can say: the
|
|
365
|
+
// entire promise is that other people are there, and a signed-out user was told the
|
|
366
|
+
// room was dead. Observed on the Mac Studio 2026-08-01 with a token that had expired
|
|
367
|
+
// 23 days earlier, while four people were actually online.
|
|
368
|
+
//
|
|
369
|
+
// Attached to the array rather than changing the return type because sixteen call
|
|
370
|
+
// sites read this as a list. Callers that do not care are unaffected; callers that
|
|
371
|
+
// render a claim about the room now have what they need to be honest.
|
|
372
|
+
// Two shapes mean the same thing. v1 answers 200 with `anonymous: true` and public
|
|
373
|
+
// COUNTS; v2 answers a hard 401 and no counts at all. Both are "we cannot see who is
|
|
374
|
+
// here", and neither is "nobody is here" — so both must reach the caller as the same
|
|
375
|
+
// fact, with counts present only when the server actually supplied them. Claiming
|
|
376
|
+
// "0 people are here" because we lack a number would swap one false statement for
|
|
377
|
+
// another.
|
|
378
|
+
const unauthenticated = result?.anonymous === true || result?.statusCode === 401;
|
|
379
|
+
|
|
358
380
|
// Map to normalized format (v2 uses 'handle', v1 uses 'username')
|
|
359
381
|
const mappedUsers = users.map(u => ({
|
|
360
382
|
handle: u.handle || u.username,
|
|
@@ -400,6 +422,14 @@ async function getActiveUsers() {
|
|
|
400
422
|
// Non-fatal: profiles module may not be available in some contexts
|
|
401
423
|
}
|
|
402
424
|
|
|
425
|
+
// On the RETURNED array, not the intermediate `users` — they are different objects
|
|
426
|
+
// and attaching to the wrong one is a silent no-op that reads as "the signal is not
|
|
427
|
+
// there" rather than as a bug.
|
|
428
|
+
Object.defineProperties(mappedUsers, {
|
|
429
|
+
anonymous: { value: unauthenticated, enumerable: false },
|
|
430
|
+
counts: { value: result?.counts || null, enumerable: false },
|
|
431
|
+
});
|
|
432
|
+
|
|
403
433
|
return mappedUsers;
|
|
404
434
|
} catch (e) {
|
|
405
435
|
console.error('Who failed:', e.message);
|
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
|
}
|
|
@@ -291,23 +291,15 @@ async function handler(args) {
|
|
|
291
291
|
}
|
|
292
292
|
} catch (e) {}
|
|
293
293
|
|
|
294
|
-
// Step 4
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
const data = await suggestionsResponse.json();
|
|
304
|
-
if (data.success && data.suggestions) {
|
|
305
|
-
suggestions = data.suggestions;
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
} catch (e) {
|
|
309
|
-
// Silent fail - suggestions are nice-to-have
|
|
310
|
-
}
|
|
294
|
+
// Step 4 used to fetch /api/suggestions and render "Suggested connections" — three
|
|
295
|
+
// strangers proposed on every session start, in the DEFAULT surface rather than behind
|
|
296
|
+
// VIBE_EXTRAS. Removed 2026-08-01 (Seth) with the automated cold intros in
|
|
297
|
+
// api/lib/board-service.js.
|
|
298
|
+
//
|
|
299
|
+
// Proposing people you do not know is the retired public-network product. A private
|
|
300
|
+
// fabric shows you the people you already work with, and says nothing when there is
|
|
301
|
+
// nobody to say anything about. NOTE: `workContext.suggestions` elsewhere in this file
|
|
302
|
+
// is unrelated — that is local work context, not people.
|
|
311
303
|
|
|
312
304
|
// Generate the ASCII welcome card (matches init.js format)
|
|
313
305
|
const welcomeCard = generateWelcomeCard({
|
|
@@ -380,17 +372,6 @@ async function handler(args) {
|
|
|
380
372
|
display += `\n_Session sharing active. Use vibe_guest to exchange messages._`;
|
|
381
373
|
}
|
|
382
374
|
|
|
383
|
-
// Add connection suggestions (if any)
|
|
384
|
-
if (suggestions.length > 0) {
|
|
385
|
-
display += `\n\n**🤝 Suggested connections:**`;
|
|
386
|
-
suggestions.slice(0, 3).forEach(s => {
|
|
387
|
-
display += `\n• @${s.handle} — ${s.reason}`;
|
|
388
|
-
if (s.github_name) {
|
|
389
|
-
display += ` (${s.github_name})`;
|
|
390
|
-
}
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
|
|
394
375
|
// Add memory context for returning users
|
|
395
376
|
if (threads.length > 0) {
|
|
396
377
|
const recentThreads = threads.slice(0, 3);
|
|
@@ -454,16 +435,6 @@ async function handler(args) {
|
|
|
454
435
|
};
|
|
455
436
|
}
|
|
456
437
|
|
|
457
|
-
// Include connection suggestions for smart discovery
|
|
458
|
-
if (suggestions.length > 0) {
|
|
459
|
-
response.suggestions = suggestions.map(s => ({
|
|
460
|
-
handle: s.handle,
|
|
461
|
-
reason: s.reason,
|
|
462
|
-
githubName: s.github_name || null,
|
|
463
|
-
lastActive: s.last_active || null,
|
|
464
|
-
matchScore: s.score || null
|
|
465
|
-
}));
|
|
466
|
-
}
|
|
467
438
|
|
|
468
439
|
// Determine session state and suggest appropriate flow
|
|
469
440
|
let suggestion = null;
|
package/tools/who.js
CHANGED
|
@@ -178,6 +178,30 @@ async function handler(args) {
|
|
|
178
178
|
// Check for notifications (presence + messages)
|
|
179
179
|
notify.checkAll(store);
|
|
180
180
|
|
|
181
|
+
// TWO EMPTY STATES, AND THEY ARE NOT THE SAME FACT.
|
|
182
|
+
//
|
|
183
|
+
// An empty list can mean "nobody is here" or "we cannot see" — and only one of those
|
|
184
|
+
// is about the room. A signed-out session used to get the first message while four
|
|
185
|
+
// people were online, which tells someone the product is dead at the exact moment they
|
|
186
|
+
// are deciding whether it works.
|
|
187
|
+
if (rawUsers.anonymous) {
|
|
188
|
+
// Say the number only when the server gave us one. v1 supplies public counts; v2
|
|
189
|
+
// answers 401 with nothing, and inventing "0 people" there would be the same lie in
|
|
190
|
+
// a different costume.
|
|
191
|
+
const n = rawUsers.counts?.active;
|
|
192
|
+
const line = typeof n === 'number'
|
|
193
|
+
? `_You're signed out — ${n === 1 ? '1 person is' : `${n} people are`} here right now, but names need a sign-in._`
|
|
194
|
+
: `_You're signed out, so I can't see who's here._`;
|
|
195
|
+
return {
|
|
196
|
+
structured: { generatedAt: Date.now(), users: [], signedOut: true, counts: rawUsers.counts || null },
|
|
197
|
+
display: `## Who's Around
|
|
198
|
+
|
|
199
|
+
${line}
|
|
200
|
+
|
|
201
|
+
Run \`vibe init\` and you'll see who, and they'll see you.`
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
181
205
|
if (users.length === 0) {
|
|
182
206
|
return {
|
|
183
207
|
structured: { generatedAt: Date.now(), users: [] },
|
|
@@ -312,16 +336,20 @@ say hi to **@vibe** (the platform account, always around), or send
|
|
|
312
336
|
}
|
|
313
337
|
}
|
|
314
338
|
|
|
315
|
-
//
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
339
|
+
// One line, and it must name a tool that is ACTUALLY REGISTERED.
|
|
340
|
+
//
|
|
341
|
+
// This rotated three suggestions at random, one of which advertised `play` — a
|
|
342
|
+
// culture-layer tool that 0.8.0 moved behind VIBE_EXTRAS. So the default board
|
|
343
|
+
// routinely told people to use something their session does not have. That is the
|
|
344
|
+
// help-drift class we already have a guard for on `vibe_help`; the board escaped it.
|
|
345
|
+
//
|
|
346
|
+
// Randomness went too: a board that says something different every time reads as
|
|
347
|
+
// decoration. The hint now follows from state — if you have not set a status,
|
|
348
|
+
// that is the useful thing to say.
|
|
323
349
|
display += `---\n`;
|
|
324
|
-
display +=
|
|
350
|
+
display += config.getOneLiner()
|
|
351
|
+
? `say "dm @handle" to reach someone`
|
|
352
|
+
: `say "status <what you're on>" so others see what you're building`;
|
|
325
353
|
|
|
326
354
|
// Check for unread to add urgency
|
|
327
355
|
try {
|
|
@@ -336,17 +364,15 @@ say hi to **@vibe** (the platform account, always around), or send
|
|
|
336
364
|
|
|
337
365
|
}
|
|
338
366
|
|
|
339
|
-
//
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
// Silent fail — genesis display is nice-to-have
|
|
349
|
-
}
|
|
367
|
+
// The scarcity line is gone.
|
|
368
|
+
//
|
|
369
|
+
// "genesis is full — 282 builders" was a growth-era artifact on two counts. It sold
|
|
370
|
+
// scarcity, which docs/PRIVATE-FABRIC.md removed from scope; and the number was a
|
|
371
|
+
// handle count, not people — 282 "builders" on a network where a handful of humans
|
|
372
|
+
// are actually active reads as a claim the room immediately contradicts. Nobody
|
|
373
|
+
// invited into a private fabric needs to be told the list is closed.
|
|
374
|
+
//
|
|
375
|
+
// What the board says about who is here is now only what it can see: the rows above.
|
|
350
376
|
|
|
351
377
|
// If not initialized, append join prompt instead of actions/unread
|
|
352
378
|
if (!isInit) {
|
package/version.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.8.
|
|
3
|
-
"updated": "2026-
|
|
4
|
-
"changelog": "
|
|
2
|
+
"version": "0.8.7",
|
|
3
|
+
"updated": "2026-08-01",
|
|
4
|
+
"changelog": "Three ways /vibe told you something untrue, all on the path a new person walks. Installing failed with \"Could not reach slashvibe.dev\" whenever a cache was over quota — the site was serving fine, and a subsystem setup never uses was refusing you the product. Re-running setup on a machine whose login had expired answered \"/vibe is configured\" and quit, so the one command for fixing it said there was nothing to fix. And asking who was around while signed out reported an empty room instead of saying you were signed out, which is the worst thing this product can get wrong. Logins are now 30 days everywhere; new accounts had been getting one-hour ones.",
|
|
5
5
|
"features": [
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"
|
|
6
|
+
"A degraded cache no longer blocks installing",
|
|
7
|
+
"An expired login says so, names the account, and signs you back in",
|
|
8
|
+
"Signed out reads as signed out, not as an empty room",
|
|
9
|
+
"One login lifetime — 30 days, including for brand-new accounts"
|
|
10
10
|
],
|
|
11
11
|
"deprecated": [],
|
|
12
12
|
"breaking": false,
|