slashvibe-mcp 0.8.1 → 0.8.2
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 +54 -5
- package/index.js +8 -2
- package/package.json +1 -1
- package/tools/inbox.js +11 -1
- package/tools/init.js +130 -227
- package/version.json +7 -5
package/auth-store.js
CHANGED
|
@@ -25,6 +25,25 @@ let _hydrated = false;
|
|
|
25
25
|
* Hydrate auth state from disk (call once at MCP startup)
|
|
26
26
|
* This loads persisted state from config.json
|
|
27
27
|
*/
|
|
28
|
+
/**
|
|
29
|
+
* The handle a token was ISSUED FOR, read from its own subject claim.
|
|
30
|
+
*
|
|
31
|
+
* Decode, not verify — the signature is the server's business and it checks it on
|
|
32
|
+
* every call. What this answers is narrower and local: "whose credential is this?"
|
|
33
|
+
* Returns null for anything unparseable, so a malformed token can never masquerade.
|
|
34
|
+
*/
|
|
35
|
+
function handleFromToken(token) {
|
|
36
|
+
try {
|
|
37
|
+
const part = String(token).split('.')[1];
|
|
38
|
+
if (!part) return null;
|
|
39
|
+
const claims = JSON.parse(Buffer.from(part, 'base64url').toString('utf8'));
|
|
40
|
+
const who = claims.handle || claims.sub;
|
|
41
|
+
return typeof who === 'string' && who ? who : null;
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
28
47
|
function hydrate() {
|
|
29
48
|
if (_hydrated) return; // Only hydrate once
|
|
30
49
|
|
|
@@ -33,16 +52,35 @@ function hydrate() {
|
|
|
33
52
|
const cfg = config.load();
|
|
34
53
|
|
|
35
54
|
_token = cfg.authToken || cfg.privyToken || null;
|
|
36
|
-
_handle = cfg.handle || cfg.username || null;
|
|
37
55
|
_oneLiner = cfg.one_liner || cfg.workingOn || null;
|
|
38
56
|
|
|
39
57
|
// Cross-client: if no token from config.json, check terminal/buddy auth.json
|
|
40
58
|
if (!_token) {
|
|
41
59
|
const terminalAuth = config.loadTerminalAuth();
|
|
42
|
-
if (terminalAuth?.token)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
60
|
+
if (terminalAuth?.token) _token = terminalAuth.token;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// THE TOKEN IS THE IDENTITY.
|
|
64
|
+
//
|
|
65
|
+
// The handle used to be read from config.json independently of the token, so the
|
|
66
|
+
// two could name different people and nothing noticed: the client displayed
|
|
67
|
+
// @brightseth, the server saw @vibetester1, and a DM went out as the wrong
|
|
68
|
+
// principal while reporting success. (Found by the first terminal-to-terminal
|
|
69
|
+
// test, issue #107 — a self-send recorded in the database.)
|
|
70
|
+
//
|
|
71
|
+
// A stored handle is a label someone wrote down; the token is what the server
|
|
72
|
+
// acts on. When they disagree, the file is wrong — never the credential.
|
|
73
|
+
const stored = cfg.handle || cfg.username || null;
|
|
74
|
+
_handle = _token ? handleFromToken(_token) : null;
|
|
75
|
+
|
|
76
|
+
if (_token && !_handle) {
|
|
77
|
+
// A token we cannot attribute is one we must not act under.
|
|
78
|
+
console.error('[auth-store] Token carries no identity — treating as signed out');
|
|
79
|
+
_token = null;
|
|
80
|
+
} else if (_token && stored && stored !== _handle) {
|
|
81
|
+
console.error(
|
|
82
|
+
`[auth-store] Stored handle @${stored} does not own this session — acting as @${_handle}`
|
|
83
|
+
);
|
|
46
84
|
}
|
|
47
85
|
|
|
48
86
|
_hydrated = true;
|
|
@@ -65,6 +103,17 @@ function setToken(token) {
|
|
|
65
103
|
const hadToken = !!_token;
|
|
66
104
|
_token = token;
|
|
67
105
|
|
|
106
|
+
// Identity travels WITH the credential (see hydrate). A new token can belong to a
|
|
107
|
+
// different person — after a re-auth, an account switch, or a cross-client sync —
|
|
108
|
+
// so the handle is re-derived here rather than left pointing at the previous one.
|
|
109
|
+
if (token) {
|
|
110
|
+
const who = handleFromToken(token);
|
|
111
|
+
if (who && who !== _handle) {
|
|
112
|
+
if (_handle) console.error(`[auth-store] Now acting as @${who} (was @${_handle})`);
|
|
113
|
+
_handle = who;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
68
117
|
if (!hadToken && token) {
|
|
69
118
|
console.error('[auth-store] Token set (was empty)');
|
|
70
119
|
} else if (hadToken && token && token !== _token) {
|
package/index.js
CHANGED
|
@@ -665,8 +665,14 @@ class VibeMCPServer {
|
|
|
665
665
|
|
|
666
666
|
async autoPresence() {
|
|
667
667
|
try {
|
|
668
|
-
|
|
669
|
-
|
|
668
|
+
// Presence follows the CREDENTIAL, not a handle written in a file. This used to
|
|
669
|
+
// read config.getHandle() and announce "Auto-connected" on the strength of that
|
|
670
|
+
// alone — so an expired or foreign session still printed a confident green line
|
|
671
|
+
// and started a heartbeat that could only 401. That is the client-side version
|
|
672
|
+
// of the bug where an agent holds a green dot and never reads its mail: the dot
|
|
673
|
+
// has to mean someone is actually reachable. (issues #107, #91)
|
|
674
|
+
const handle = authStore.getHandle();
|
|
675
|
+
if (!handle || !authStore.isAuthenticated()) return;
|
|
670
676
|
|
|
671
677
|
const one_liner = config.getOneLiner() || '';
|
|
672
678
|
|
package/package.json
CHANGED
package/tools/inbox.js
CHANGED
|
@@ -55,8 +55,18 @@ function getStatusLabel(user) {
|
|
|
55
55
|
return 'away';
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
// Build recommended connections display for empty/caught-up inbox
|
|
58
|
+
// Build recommended connections display for empty/caught-up inbox.
|
|
59
|
+
//
|
|
60
|
+
// OFF by default. /vibe is a private fabric (docs/PRIVATE-FABRIC.md): discovery and
|
|
61
|
+
// cold introductions are out of scope, so an empty inbox has no business proposing
|
|
62
|
+
// strangers. It read especially badly on a brand-new account — five suggestions whose
|
|
63
|
+
// statuses were all the identical filler "something cool · Building the exact same
|
|
64
|
+
// thing", on the first screen an invitee ever sees (issue #108). An empty inbox
|
|
65
|
+
// should simply be empty; VIBE_EXTRAS=1 restores this with the rest of the culture
|
|
66
|
+
// layer for anyone who wants it.
|
|
59
67
|
async function buildRecommendationsDisplay(myHandle) {
|
|
68
|
+
if (process.env.VIBE_EXTRAS !== '1') return null;
|
|
69
|
+
|
|
60
70
|
// Fetch recommendations from relevancy API
|
|
61
71
|
const relevancy = await fetchRelevantUsers(myHandle, 'dm_suggest', 5);
|
|
62
72
|
|
package/tools/init.js
CHANGED
|
@@ -243,7 +243,13 @@ async function sendPersonalizedWelcome(handle, oneLiner) {
|
|
|
243
243
|
|
|
244
244
|
const LOGIN_URL = 'https://www.slashvibe.dev/login';
|
|
245
245
|
const API_URL = process.env.VIBE_API_URL || 'https://www.slashvibe.dev';
|
|
246
|
-
|
|
246
|
+
// A first sign-in is not two minutes of typing: it can be a GitHub login, 2FA, a
|
|
247
|
+
// password manager, or switching accounts. The old fuse expired mid-flow for people
|
|
248
|
+
// who WROTE this client (issue #108), so it is now generous.
|
|
249
|
+
const AUTH_TIMEOUT_MS = 300000; // 5 minutes to complete the browser flow
|
|
250
|
+
// After we stop waiting, keep the socket up briefly so a late callback still gets a
|
|
251
|
+
// real page instead of the browser's connection-refused error.
|
|
252
|
+
const LATE_CALLBACK_GRACE_MS = 300000; // 5 more minutes to answer honestly
|
|
247
253
|
|
|
248
254
|
/**
|
|
249
255
|
* Send welcome message from @seth (founder)
|
|
@@ -314,18 +320,67 @@ function openBrowser(url) {
|
|
|
314
320
|
function waitForCallback(requestedHandle, one_liner) {
|
|
315
321
|
return new Promise((resolve, reject) => {
|
|
316
322
|
let resolved = false;
|
|
323
|
+
let timedOut = false;
|
|
317
324
|
|
|
318
325
|
const server = http.createServer(async (req, res) => {
|
|
319
326
|
const url = new URL(req.url, `http://localhost:${CALLBACK_PORT}`);
|
|
320
327
|
|
|
328
|
+
// A callback that arrives after we stopped waiting still deserves an answer.
|
|
329
|
+
// Anything is better than the browser's raw connection-refused page (issue #108).
|
|
330
|
+
if (timedOut && url.pathname === '/callback') {
|
|
331
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
332
|
+
res.end(`<!DOCTYPE html>
|
|
333
|
+
<html lang="en"><head><meta charset="UTF-8">
|
|
334
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
335
|
+
<title>sign-in timed out · /vibe</title>
|
|
336
|
+
<link rel="stylesheet" href="https://www.slashvibe.dev/vibe-tokens.css">
|
|
337
|
+
<style>
|
|
338
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
339
|
+
body{background:var(--bg,#0A0A0A);color:var(--dim,#9CA3AF);font-family:var(--mono,ui-monospace,Menlo,monospace);
|
|
340
|
+
font-size:var(--t-14,14px);line-height:1.6;min-height:100vh;display:flex;align-items:center;
|
|
341
|
+
justify-content:center;padding:24px}
|
|
342
|
+
.card{border:1px solid var(--line,#1F2937);border-radius:var(--r-lg,10px);background:var(--panel,#111316);
|
|
343
|
+
padding:32px;max-width:420px;width:100%}
|
|
344
|
+
.logo{color:var(--ink,#E0E0E0);font-size:var(--t-16,16px);margin-bottom:24px}
|
|
345
|
+
.logo span{color:var(--blue,#6B8FFF)}
|
|
346
|
+
h1{color:var(--ink,#E0E0E0);font-size:var(--t-20,20px);font-weight:600;margin-bottom:8px}
|
|
347
|
+
code{color:var(--ink,#E0E0E0);background:var(--bg,#0A0A0A);border:1px solid var(--line,#1F2937);
|
|
348
|
+
border-radius:var(--r-sm,6px);padding:1px 6px}
|
|
349
|
+
.next{margin-top:24px;padding-top:16px;border-top:1px solid var(--line,#1F2937);font-size:var(--t-13,13px)}
|
|
350
|
+
</style></head>
|
|
351
|
+
<body><div class="card">
|
|
352
|
+
<div class="logo">/<span>vibe</span></div>
|
|
353
|
+
<h1>sign-in took too long</h1>
|
|
354
|
+
<p>your terminal stopped waiting, so this sign-in didn't finish. nothing is broken and
|
|
355
|
+
nothing was saved.</p>
|
|
356
|
+
<div class="next">back in your terminal, say <code>vibe init</code> and it will open a fresh
|
|
357
|
+
sign-in. this window can be closed.</div>
|
|
358
|
+
</div></body></html>`);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
321
362
|
// Handle callback
|
|
322
363
|
if (url.pathname === '/callback') {
|
|
323
364
|
const token = url.searchParams.get('token');
|
|
324
365
|
const callbackHandle = url.searchParams.get('handle');
|
|
325
366
|
|
|
326
367
|
if (token && callbackHandle) {
|
|
327
|
-
//
|
|
328
|
-
|
|
368
|
+
// WHO AUTHENTICATED WINS — not who we asked for.
|
|
369
|
+
//
|
|
370
|
+
// This was `requestedHandle || callbackHandle`, i.e. the handle passed into
|
|
371
|
+
// vibe_init overrode the identity the OAuth flow actually returned. Ask to
|
|
372
|
+
// sign in as @a while the browser is signed in as @b and the client stores
|
|
373
|
+
// @b's token under @a's name: the banner says @a, every message goes out as
|
|
374
|
+
// @b. That is exactly the mismatch issue #107 records, reproduced here.
|
|
375
|
+
//
|
|
376
|
+
// A requested handle is a preference for what to CALL a brand-new account,
|
|
377
|
+
// never a claim about whose credential this is.
|
|
378
|
+
const finalHandle = callbackHandle;
|
|
379
|
+
if (requestedHandle && requestedHandle !== callbackHandle) {
|
|
380
|
+
console.error(
|
|
381
|
+
`[vibe_init] Signed in as @${callbackHandle} (you asked for @${requestedHandle}) — using the account that authenticated.`
|
|
382
|
+
);
|
|
383
|
+
}
|
|
329
384
|
|
|
330
385
|
// Save to config (file persistence for restarts)
|
|
331
386
|
config.saveAuthToken(token);
|
|
@@ -364,228 +419,64 @@ function waitForCallback(requestedHandle, one_liner) {
|
|
|
364
419
|
<head>
|
|
365
420
|
<meta charset="UTF-8">
|
|
366
421
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
367
|
-
<title>
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
422
|
+
<title>signed in to /vibe</title>
|
|
423
|
+
<!-- ROOM TONE, from the one source of record. This page is the LAST thing an
|
|
424
|
+
invitee sees before the product, so it must not look like a different one:
|
|
425
|
+
it used neon #00FF88, cyan, glow shadows, CRT scanlines and two arcade fonts
|
|
426
|
+
— every item on the "not this" list. The user just completed a browser OAuth,
|
|
427
|
+
so they are online by definition and the hosted token file is safe to link. -->
|
|
428
|
+
<link rel="stylesheet" href="https://www.slashvibe.dev/vibe-tokens.css">
|
|
371
429
|
<style>
|
|
372
430
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
373
|
-
|
|
374
|
-
:root {
|
|
375
|
-
--neon-green: #00FF88;
|
|
376
|
-
--neon-cyan: #00FFFF;
|
|
377
|
-
--deep-black: #0A0A0A;
|
|
378
|
-
--glow-green: 0 0 15px #00FF88;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
431
|
body {
|
|
382
|
-
|
|
383
|
-
|
|
432
|
+
background: var(--bg, #0A0A0A);
|
|
433
|
+
color: var(--dim, #9CA3AF);
|
|
434
|
+
font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, monospace);
|
|
435
|
+
font-size: var(--t-14, 14px);
|
|
436
|
+
line-height: 1.6;
|
|
384
437
|
min-height: 100vh;
|
|
385
|
-
display: flex;
|
|
386
|
-
|
|
387
|
-
justify-content: center;
|
|
388
|
-
color: #fff;
|
|
389
|
-
overflow: hidden;
|
|
390
|
-
position: relative;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
/* Static CRT Scanline Effect - no animation */
|
|
394
|
-
body::before {
|
|
395
|
-
content: '';
|
|
396
|
-
position: fixed;
|
|
397
|
-
top: 0; left: 0;
|
|
398
|
-
width: 100%; height: 100%;
|
|
399
|
-
background: repeating-linear-gradient(
|
|
400
|
-
0deg,
|
|
401
|
-
rgba(0, 0, 0, 0.1),
|
|
402
|
-
rgba(0, 0, 0, 0.1) 1px,
|
|
403
|
-
transparent 1px,
|
|
404
|
-
transparent 2px
|
|
405
|
-
);
|
|
406
|
-
pointer-events: none;
|
|
407
|
-
z-index: 1000;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
/* Static vignette */
|
|
411
|
-
body::after {
|
|
412
|
-
content: '';
|
|
413
|
-
position: fixed;
|
|
414
|
-
top: 0; left: 0;
|
|
415
|
-
width: 100%; height: 100%;
|
|
416
|
-
background: radial-gradient(ellipse at center, transparent 0%, rgba(0, 0, 0, 0.3) 100%);
|
|
417
|
-
pointer-events: none;
|
|
418
|
-
z-index: 999;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
/* Static decorative symbols - NO animation */
|
|
422
|
-
.particles {
|
|
423
|
-
position: fixed;
|
|
424
|
-
top: 0; left: 0;
|
|
425
|
-
width: 100%; height: 100%;
|
|
426
|
-
pointer-events: none;
|
|
427
|
-
z-index: 1;
|
|
428
|
-
opacity: 0.15;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
.particle {
|
|
432
|
-
position: absolute;
|
|
433
|
-
font-size: 20px;
|
|
434
|
-
color: var(--neon-green);
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
.container {
|
|
438
|
-
background: rgba(0, 255, 136, 0.03);
|
|
439
|
-
border: 2px solid var(--neon-green);
|
|
440
|
-
padding: 48px 64px;
|
|
441
|
-
max-width: 480px;
|
|
442
|
-
width: 90%;
|
|
443
|
-
text-align: center;
|
|
444
|
-
position: relative;
|
|
445
|
-
z-index: 10;
|
|
446
|
-
box-shadow: var(--glow-green);
|
|
447
|
-
animation: fadeIn 0.4s ease-out forwards;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
@keyframes fadeIn {
|
|
451
|
-
from { opacity: 0; transform: scale(0.95); }
|
|
452
|
-
to { opacity: 1; transform: scale(1); }
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
/* Corner decorations */
|
|
456
|
-
.container::before, .container::after {
|
|
457
|
-
content: '+';
|
|
458
|
-
position: absolute;
|
|
459
|
-
font-family: 'VT323', monospace;
|
|
460
|
-
font-size: 24px;
|
|
461
|
-
color: var(--neon-green);
|
|
462
|
-
}
|
|
463
|
-
.container::before { top: 8px; left: 12px; }
|
|
464
|
-
.container::after { bottom: 8px; right: 12px; }
|
|
465
|
-
|
|
466
|
-
.logo {
|
|
467
|
-
font-family: 'Press Start 2P', cursive;
|
|
468
|
-
font-size: 28px;
|
|
469
|
-
color: var(--neon-green);
|
|
470
|
-
text-shadow: var(--glow-green);
|
|
471
|
-
margin-bottom: 24px;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
.checkmark {
|
|
475
|
-
width: 80px;
|
|
476
|
-
height: 80px;
|
|
477
|
-
margin: 0 auto 24px;
|
|
438
|
+
display: flex; align-items: center; justify-content: center;
|
|
439
|
+
padding: var(--s-6, 24px);
|
|
478
440
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
stroke-width: 4;
|
|
486
|
-
stroke-linecap: round;
|
|
487
|
-
stroke-linejoin: round;
|
|
488
|
-
filter: drop-shadow(0 0 8px var(--neon-green));
|
|
489
|
-
animation: drawCheck 0.5s ease-out forwards;
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
@keyframes drawCheck {
|
|
493
|
-
0% { stroke-dasharray: 100; stroke-dashoffset: 100; }
|
|
494
|
-
100% { stroke-dasharray: 100; stroke-dashoffset: 0; }
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
.welcome {
|
|
498
|
-
font-size: 28px;
|
|
499
|
-
color: rgba(255, 255, 255, 0.9);
|
|
500
|
-
margin-bottom: 8px;
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
.handle {
|
|
504
|
-
color: var(--neon-green);
|
|
505
|
-
text-shadow: 0 0 10px var(--neon-green);
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
.status {
|
|
509
|
-
font-size: 20px;
|
|
510
|
-
color: var(--neon-cyan);
|
|
511
|
-
margin: 20px 0;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
.close-msg {
|
|
515
|
-
font-size: 18px;
|
|
516
|
-
color: rgba(255, 255, 255, 0.6);
|
|
517
|
-
margin-top: 24px;
|
|
518
|
-
padding: 12px 20px;
|
|
519
|
-
border: 1px dashed rgba(255, 255, 255, 0.3);
|
|
441
|
+
.card {
|
|
442
|
+
border: 1px solid var(--line, #1F2937);
|
|
443
|
+
border-radius: var(--r-lg, 10px);
|
|
444
|
+
background: var(--panel, #111316);
|
|
445
|
+
padding: var(--s-8, 32px);
|
|
446
|
+
max-width: 420px; width: 100%;
|
|
520
447
|
}
|
|
521
|
-
|
|
522
|
-
.
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
animation: none !important;
|
|
532
|
-
transition: none !important;
|
|
533
|
-
}
|
|
448
|
+
.logo { color: var(--ink, #E0E0E0); font-size: var(--t-16, 16px); margin-bottom: var(--s-6, 24px); }
|
|
449
|
+
.logo span { color: var(--blue, #6B8FFF); }
|
|
450
|
+
.line { color: var(--ink, #E0E0E0); font-size: var(--t-20, 20px); margin-bottom: var(--s-2, 8px); }
|
|
451
|
+
/* green is presence and nothing else — here it states a fact that is now true:
|
|
452
|
+
this handle is on the board. */
|
|
453
|
+
.dot { color: var(--green, #22c55e); }
|
|
454
|
+
.meta { font-size: var(--t-13, 13px); }
|
|
455
|
+
.next {
|
|
456
|
+
margin-top: var(--s-6, 24px); padding-top: var(--s-4, 16px);
|
|
457
|
+
border-top: 1px solid var(--line, #1F2937); font-size: var(--t-13, 13px);
|
|
534
458
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
459
|
+
code {
|
|
460
|
+
color: var(--ink, #E0E0E0);
|
|
461
|
+
background: var(--bg, #0A0A0A);
|
|
462
|
+
border: 1px solid var(--line, #1F2937);
|
|
463
|
+
border-radius: var(--r-sm, 6px);
|
|
464
|
+
padding: 1px 6px;
|
|
541
465
|
}
|
|
466
|
+
.close { color: var(--faint, #6B7280); font-size: var(--t-11, 11px); margin-top: var(--s-6, 24px); }
|
|
542
467
|
</style>
|
|
543
468
|
</head>
|
|
544
469
|
<body>
|
|
545
|
-
<div class="
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
<
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
<
|
|
552
|
-
<circle cx="26" cy="26" r="22" stroke-opacity="0.3"/>
|
|
553
|
-
<path d="M14 27l8 8 16-16"/>
|
|
554
|
-
</svg>
|
|
470
|
+
<div class="card">
|
|
471
|
+
<div class="logo">/<span>vibe</span></div>
|
|
472
|
+
<p class="line"><span class="dot">🟢</span> you're in, @${finalHandle}</p>
|
|
473
|
+
<p class="meta">your session is signed in and you're on the board.</p>
|
|
474
|
+
<div class="next">
|
|
475
|
+
back in your terminal, say <code>vibe who</code> to see who's around,
|
|
476
|
+
or <code>vibe inbox</code> if someone already wrote to you.
|
|
555
477
|
</div>
|
|
556
|
-
|
|
557
|
-
<p class="welcome">Welcome, <span class="handle">@${finalHandle}</span></p>
|
|
558
|
-
<p class="status">Authentication successful</p>
|
|
559
|
-
|
|
560
|
-
<p class="close-msg">You can now close this window</p>
|
|
561
|
-
|
|
562
|
-
<div class="ascii">═══════════════════════════════</div>
|
|
478
|
+
<p class="close">you can close this window.</p>
|
|
563
479
|
</div>
|
|
564
|
-
|
|
565
|
-
<script>
|
|
566
|
-
// Create STATIC decorative symbols (no animation)
|
|
567
|
-
(function() {
|
|
568
|
-
var container = document.getElementById('particles');
|
|
569
|
-
var symbols = ['>', '<', '/', '*', '#', '@', '~'];
|
|
570
|
-
var positions = [
|
|
571
|
-
{x: 10, y: 15}, {x: 85, y: 20}, {x: 20, y: 75}, {x: 75, y: 80},
|
|
572
|
-
{x: 50, y: 10}, {x: 15, y: 45}, {x: 88, y: 55}, {x: 45, y: 85}
|
|
573
|
-
];
|
|
574
|
-
positions.forEach(function(pos, i) {
|
|
575
|
-
var particle = document.createElement('div');
|
|
576
|
-
particle.className = 'particle';
|
|
577
|
-
particle.textContent = symbols[i % symbols.length];
|
|
578
|
-
particle.style.left = pos.x + '%';
|
|
579
|
-
particle.style.top = pos.y + '%';
|
|
580
|
-
container.appendChild(particle);
|
|
581
|
-
});
|
|
582
|
-
})();
|
|
583
|
-
|
|
584
|
-
// Stop any remaining animations after 2 seconds (safety net)
|
|
585
|
-
setTimeout(function() {
|
|
586
|
-
document.body.classList.add('animations-stopped');
|
|
587
|
-
}, 2000);
|
|
588
|
-
</script>
|
|
589
480
|
</body>
|
|
590
481
|
</html>`);
|
|
591
482
|
|
|
@@ -616,11 +507,23 @@ function waitForCallback(requestedHandle, one_liner) {
|
|
|
616
507
|
console.log(`[vibe_init] Callback server listening on port ${CALLBACK_PORT}`);
|
|
617
508
|
});
|
|
618
509
|
|
|
619
|
-
//
|
|
510
|
+
// Stop WAITING at the timeout, but keep LISTENING for a grace period.
|
|
511
|
+
//
|
|
512
|
+
// These were the same moment before, and it produced the worst screen in the
|
|
513
|
+
// product: sign-in takes longer than the fuse (first-time GitHub, 2FA, a
|
|
514
|
+
// password manager, switching accounts — all normal), the listener closes, and
|
|
515
|
+
// the browser lands on a raw ERR_CONNECTION_REFUSED holding a valid token, with
|
|
516
|
+
// no explanation and nothing to click. It happened twice in one evening to
|
|
517
|
+
// people who built this. An invitee reads that as "broken". (issue #108)
|
|
518
|
+
//
|
|
519
|
+
// So: the promise rejects on time (the caller must not hang), and the socket
|
|
520
|
+
// stays up long enough to answer a late callback with a page that says what
|
|
521
|
+
// happened and how to retry.
|
|
620
522
|
setTimeout(() => {
|
|
621
523
|
if (!resolved) {
|
|
622
|
-
|
|
524
|
+
timedOut = true;
|
|
623
525
|
reject(new Error('AUTH_TIMEOUT'));
|
|
526
|
+
setTimeout(() => { if (!resolved) server.close(); }, LATE_CALLBACK_GRACE_MS);
|
|
624
527
|
}
|
|
625
528
|
}, AUTH_TIMEOUT_MS);
|
|
626
529
|
});
|
|
@@ -805,19 +708,19 @@ To check messages: \`vibe inbox\`${emailNudge}`
|
|
|
805
708
|
const whoNudge = presence.humans > 1
|
|
806
709
|
? `**"vibe who"** — see the ${presence.humans} builders online right now`
|
|
807
710
|
: `**"vibe who"** — see who's building right now`;
|
|
808
|
-
//
|
|
809
|
-
//
|
|
810
|
-
//
|
|
811
|
-
//
|
|
812
|
-
//
|
|
813
|
-
//
|
|
814
|
-
//
|
|
815
|
-
const nextSteps =
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
711
|
+
// You are here because someone you work with invited you. So the first move
|
|
712
|
+
// is answering THEM, not being matched with a stranger.
|
|
713
|
+
//
|
|
714
|
+
// This used to instruct the agent to call `vibe_intro` — auto-introduce the
|
|
715
|
+
// newcomer to whoever seemed related. That tool is RETIRED (docs/PRIVATE-FABRIC.md:
|
|
716
|
+
// cold introduction is out of scope), so the very first instruction a new
|
|
717
|
+
// invitee received pointed at the discovery model we deleted.
|
|
718
|
+
const nextSteps = unreadCount > 0
|
|
719
|
+
? `\n\n---\n**Someone is already waiting on you.** Say **"vibe inbox"** to read it, then **"vibe reply"** to answer.\n\n`
|
|
720
|
+
+ `_Stuck? Say **"vibe help"**._`
|
|
721
|
+
: `\n\n---\n**You're in.** Say **"vibe dm @<handle>"** to reach whoever invited you — they'll get it in their next session, whether or not they're online now.\n`
|
|
722
|
+
+ `\n${whoNudge}.\n\n`
|
|
723
|
+
+ `_Stuck? Say **"vibe help"**._`;
|
|
821
724
|
|
|
822
725
|
// One-time email nudge — only for users we can't otherwise reach (no
|
|
823
726
|
// address on file). This is the return loop: a DM that lands while you're
|
package/version.json
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.8.
|
|
2
|
+
"version": "0.8.2",
|
|
3
3
|
"updated": "2026-07-31",
|
|
4
|
-
"changelog": "
|
|
4
|
+
"changelog": "Identity and first-run honesty. The client could display one handle while acting as another: the shown name came from a config file and the acting identity from a token, and nothing tied them together \u2014 so a DM could go out as the wrong account and still report success. The token is now the identity everywhere, including the sign-in flow, which used to let a requested handle override the account that actually authenticated. Presence no longer announces a connection it has not verified. Sign-in gets five minutes instead of two, and a late callback now gets a page explaining what happened instead of the browser's connection-refused error. First-run copy points at the person who invited you rather than at strangers.",
|
|
5
5
|
"features": [
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
6
|
+
"The token is the identity \u2014 a stored handle can never override the credential",
|
|
7
|
+
"Sign-in keeps the account that authenticated, not the one that was requested",
|
|
8
|
+
"Presence announces a session only after it is verified",
|
|
9
|
+
"Sign-in window 2 -> 5 minutes, with an honest page if it still expires",
|
|
10
|
+
"First run points at your inviter; stranger suggestions move behind VIBE_EXTRAS"
|
|
9
11
|
],
|
|
10
12
|
"deprecated": [],
|
|
11
13
|
"breaking": false,
|