vibegroup 0.1.3 → 0.1.4
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/dist/cli.js +75 -212
- package/package.json +1 -1
- package/plugin/commands/init.md +12 -20
package/dist/cli.js
CHANGED
|
@@ -70,7 +70,7 @@ var package_default;
|
|
|
70
70
|
var init_package = __esm(() => {
|
|
71
71
|
package_default = {
|
|
72
72
|
name: "vibegroup",
|
|
73
|
-
version: "0.1.
|
|
73
|
+
version: "0.1.4",
|
|
74
74
|
description: "Talk to your teammates' Claude Code agents — agent-to-agent collaboration for Claude Code over a shared channel.",
|
|
75
75
|
type: "module",
|
|
76
76
|
bin: {
|
|
@@ -249,144 +249,30 @@ function runInk(make) {
|
|
|
249
249
|
}
|
|
250
250
|
var init_runner = () => {};
|
|
251
251
|
|
|
252
|
-
// src/lib/
|
|
253
|
-
async function
|
|
254
|
-
const
|
|
255
|
-
try {
|
|
256
|
-
return text ? JSON.parse(text) : {};
|
|
257
|
-
} catch {
|
|
258
|
-
return {};
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
function parseCeremony(c) {
|
|
262
|
-
if (!c || !c.user_code || !c.verification_uri)
|
|
263
|
-
return;
|
|
264
|
-
return {
|
|
265
|
-
userCode: c.user_code,
|
|
266
|
-
verificationUri: c.verification_uri,
|
|
267
|
-
interval: typeof c.interval === "number" ? c.interval : 5,
|
|
268
|
-
expiresIn: typeof c.expires_in === "number" ? c.expires_in : 600
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
|
-
function tokensFromResponse(body, nowMs) {
|
|
272
|
-
const tokens = { accessToken: body.access_token };
|
|
273
|
-
if (typeof body.expires_in === "number") {
|
|
274
|
-
tokens.accessTokenExpiresAt = new Date(nowMs + body.expires_in * 1000).toISOString();
|
|
275
|
-
}
|
|
276
|
-
if (body.identity_assertion)
|
|
277
|
-
tokens.identityAssertion = body.identity_assertion;
|
|
278
|
-
if (body.assertion_expires)
|
|
279
|
-
tokens.assertionExpiresAt = body.assertion_expires;
|
|
280
|
-
if (body.scope)
|
|
281
|
-
tokens.scope = body.scope;
|
|
282
|
-
return tokens;
|
|
283
|
-
}
|
|
284
|
-
async function discover(apiBase, f) {
|
|
285
|
-
const base = apiBase.replace(/\/+$/, "");
|
|
286
|
-
const prmRes = await f(`${base}/.well-known/oauth-protected-resource`);
|
|
287
|
-
if (!prmRes.ok)
|
|
288
|
-
throw new Error(`discovery failed: protected-resource metadata HTTP ${prmRes.status}`);
|
|
289
|
-
const prm = await json(prmRes);
|
|
290
|
-
const as = (prm.authorization_servers ?? [])[0];
|
|
291
|
-
if (!as)
|
|
292
|
-
throw new Error("discovery failed: no authorization_servers in protected-resource metadata");
|
|
293
|
-
const asRes = await f(`${as.replace(/\/+$/, "")}/.well-known/oauth-authorization-server`);
|
|
294
|
-
if (!asRes.ok)
|
|
295
|
-
throw new Error(`discovery failed: authorization-server metadata HTTP ${asRes.status}`);
|
|
296
|
-
const meta = await json(asRes);
|
|
297
|
-
const agent = meta.agent_auth ?? {};
|
|
298
|
-
if (!meta.token_endpoint || !agent.identity_endpoint || !agent.claim_endpoint) {
|
|
299
|
-
throw new Error("discovery failed: authorization server is missing agent_auth endpoints");
|
|
300
|
-
}
|
|
301
|
-
return {
|
|
302
|
-
issuer: meta.issuer,
|
|
303
|
-
tokenEndpoint: meta.token_endpoint,
|
|
304
|
-
revocationEndpoint: meta.revocation_endpoint,
|
|
305
|
-
identityEndpoint: agent.identity_endpoint,
|
|
306
|
-
claimEndpoint: agent.claim_endpoint,
|
|
307
|
-
resource: prm.resource ?? meta.resource,
|
|
308
|
-
scopesSupported: prm.scopes_supported
|
|
309
|
-
};
|
|
310
|
-
}
|
|
311
|
-
async function register(ep, body, f) {
|
|
312
|
-
const res = await f(ep.identityEndpoint, {
|
|
252
|
+
// src/lib/emailAuth.ts
|
|
253
|
+
async function startEmailLogin(apiBase, email, f) {
|
|
254
|
+
const res = await f(`${apiBase}/auth/email/start`, {
|
|
313
255
|
method: "POST",
|
|
314
256
|
headers: { "content-type": "application/json" },
|
|
315
|
-
body: JSON.stringify(
|
|
257
|
+
body: JSON.stringify({ email })
|
|
316
258
|
});
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
throw new Error(
|
|
259
|
+
if (!res.ok) {
|
|
260
|
+
const err = await res.json().catch(() => ({}));
|
|
261
|
+
throw new Error(err.detail ?? "could not send the code");
|
|
320
262
|
}
|
|
321
|
-
return {
|
|
322
|
-
registrationId: data.registration_id,
|
|
323
|
-
registrationType: data.registration_type,
|
|
324
|
-
identityAssertion: data.identity_assertion,
|
|
325
|
-
assertionExpires: data.assertion_expires,
|
|
326
|
-
preClaimScopes: data.pre_claim_scopes,
|
|
327
|
-
postClaimScopes: data.post_claim_scopes,
|
|
328
|
-
claimToken: data.claim_token,
|
|
329
|
-
claim: parseCeremony(data.claim)
|
|
330
|
-
};
|
|
331
263
|
}
|
|
332
|
-
async function
|
|
333
|
-
const res = await f(
|
|
264
|
+
async function verifyEmailLogin(apiBase, email, code, f) {
|
|
265
|
+
const res = await f(`${apiBase}/auth/email/verify`, {
|
|
334
266
|
method: "POST",
|
|
335
|
-
headers: { "content-type": "application/
|
|
336
|
-
body:
|
|
267
|
+
headers: { "content-type": "application/json" },
|
|
268
|
+
body: JSON.stringify({ email, code })
|
|
337
269
|
});
|
|
338
|
-
const data = await json(
|
|
339
|
-
if (res.ok
|
|
340
|
-
|
|
341
|
-
switch (data.error) {
|
|
342
|
-
case "authorization_pending":
|
|
343
|
-
return { status: "pending" };
|
|
344
|
-
case "slow_down":
|
|
345
|
-
return { status: "slow_down" };
|
|
346
|
-
case "expired_token":
|
|
347
|
-
return { status: "expired" };
|
|
348
|
-
default:
|
|
349
|
-
throw new Error(`claim poll failed: ${data.error ?? `HTTP ${res.status}`}`);
|
|
270
|
+
const data = await res.json().catch(() => ({}));
|
|
271
|
+
if (!res.ok) {
|
|
272
|
+
throw new Error(data.error === "invalid_code" ? "that code is incorrect or expired" : data.detail ?? "verification failed");
|
|
350
273
|
}
|
|
274
|
+
return { accessToken: data.access_token ?? "", identityAssertion: data.identity_assertion, scope: data.scope };
|
|
351
275
|
}
|
|
352
|
-
var CLAIM_GRANT = "urn:workos:agent-auth:grant-type:claim";
|
|
353
|
-
|
|
354
|
-
// src/lib/loginFlow.ts
|
|
355
|
-
async function login(apiBase, f, hooks) {
|
|
356
|
-
const emit = (e) => hooks.onEvent?.(e);
|
|
357
|
-
const sleep = hooks.sleep ?? defaultSleep;
|
|
358
|
-
const now = hooks.now ?? Date.now;
|
|
359
|
-
emit({ type: "discovering", apiBase });
|
|
360
|
-
const ep = await discover(apiBase, f);
|
|
361
|
-
emit({ type: "discovered", endpoints: ep });
|
|
362
|
-
const email = (await hooks.email())?.trim();
|
|
363
|
-
if (!email)
|
|
364
|
-
return null;
|
|
365
|
-
emit({ type: "registering", email });
|
|
366
|
-
const reg = await register(ep, { type: "service_auth", login_hint: email }, f);
|
|
367
|
-
if (!reg.claim || !reg.claimToken)
|
|
368
|
-
throw new Error("service_auth did not return a claim ceremony");
|
|
369
|
-
emit({ type: "claim", ceremony: reg.claim });
|
|
370
|
-
emit({ type: "polling" });
|
|
371
|
-
let interval = Math.max(1, reg.claim.interval);
|
|
372
|
-
const deadline = now() + (hooks.timeoutMs ?? 10 * 60 * 1000);
|
|
373
|
-
for (;; ) {
|
|
374
|
-
const r = await pollClaim(ep.tokenEndpoint, reg.claimToken, f, now());
|
|
375
|
-
if (r.status === "done") {
|
|
376
|
-
emit({ type: "done" });
|
|
377
|
-
return { tokens: r.tokens, email };
|
|
378
|
-
}
|
|
379
|
-
if (r.status === "expired")
|
|
380
|
-
throw new Error("the code expired — run `vibegroup login` again");
|
|
381
|
-
if (r.status === "slow_down")
|
|
382
|
-
interval += 5;
|
|
383
|
-
if (now() > deadline)
|
|
384
|
-
throw new Error("login timed out");
|
|
385
|
-
await sleep(interval * 1000);
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
389
|
-
var init_loginFlow = () => {};
|
|
390
276
|
|
|
391
277
|
// src/ui/session.ts
|
|
392
278
|
function jwtClaim(jwt, key) {
|
|
@@ -406,17 +292,6 @@ function sessionFromTokens(tokens, email) {
|
|
|
406
292
|
};
|
|
407
293
|
}
|
|
408
294
|
|
|
409
|
-
// src/lib/openBrowser.ts
|
|
410
|
-
import { spawn } from "node:child_process";
|
|
411
|
-
function openBrowser(url) {
|
|
412
|
-
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
413
|
-
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
414
|
-
try {
|
|
415
|
-
spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
|
|
416
|
-
} catch {}
|
|
417
|
-
}
|
|
418
|
-
var init_openBrowser = () => {};
|
|
419
|
-
|
|
420
295
|
// src/ui/theme.ts
|
|
421
296
|
var color;
|
|
422
297
|
var init_theme = __esm(() => {
|
|
@@ -433,53 +308,42 @@ var init_theme = __esm(() => {
|
|
|
433
308
|
// src/ui/Login.tsx
|
|
434
309
|
import { useEffect, useRef, useState } from "react";
|
|
435
310
|
import { Box, Text } from "ink";
|
|
436
|
-
import { Spinner, StatusMessage, TextInput, Alert
|
|
311
|
+
import { Spinner, StatusMessage, TextInput, Alert } from "@inkjs/ui";
|
|
437
312
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
438
313
|
function Login({
|
|
439
314
|
apiBase,
|
|
440
315
|
initialEmail,
|
|
441
316
|
onExit
|
|
442
317
|
}) {
|
|
443
|
-
const [phase, setPhase] = useState(initialEmail ? { t: "
|
|
444
|
-
const opened = useRef(false);
|
|
318
|
+
const [phase, setPhase] = useState(initialEmail ? { t: "sending", email: initialEmail } : { t: "email" });
|
|
445
319
|
const started = useRef(false);
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
email: async () => email,
|
|
453
|
-
onEvent: (e) => {
|
|
454
|
-
if (e.type === "registering")
|
|
455
|
-
setPhase({ t: "registering", email: e.email });
|
|
456
|
-
else if (e.type === "claim")
|
|
457
|
-
setPhase({ t: "waiting", code: e.ceremony.userCode, url: e.ceremony.verificationUri });
|
|
458
|
-
}
|
|
459
|
-
}).then((res) => {
|
|
460
|
-
if (!res) {
|
|
461
|
-
setPhase({ t: "error", message: "Login cancelled." });
|
|
462
|
-
setTimeout(() => onExit(1), 50);
|
|
463
|
-
return;
|
|
464
|
-
}
|
|
465
|
-
writeAuth(sessionFromTokens(res.tokens, res.email));
|
|
466
|
-
setPhase({ t: "done", email: res.email });
|
|
467
|
-
setTimeout(() => onExit(0), 500);
|
|
468
|
-
}).catch((err) => {
|
|
320
|
+
const send = async (email) => {
|
|
321
|
+
setPhase({ t: "sending", email });
|
|
322
|
+
try {
|
|
323
|
+
await startEmailLogin(apiBase, email, fetch);
|
|
324
|
+
setPhase({ t: "code", email });
|
|
325
|
+
} catch (err) {
|
|
469
326
|
setPhase({ t: "error", message: err?.message ?? String(err) });
|
|
470
327
|
setTimeout(() => onExit(1), 800);
|
|
471
|
-
}
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
const verify = async (email, code) => {
|
|
331
|
+
setPhase({ t: "verifying", email });
|
|
332
|
+
try {
|
|
333
|
+
const tokens = await verifyEmailLogin(apiBase, email, code, fetch);
|
|
334
|
+
writeAuth(sessionFromTokens(tokens, email));
|
|
335
|
+
setPhase({ t: "done", email });
|
|
336
|
+
setTimeout(() => onExit(0), 500);
|
|
337
|
+
} catch (err) {
|
|
338
|
+
setPhase({ t: "code", email, error: err?.message ?? String(err) });
|
|
339
|
+
}
|
|
472
340
|
};
|
|
473
341
|
useEffect(() => {
|
|
474
|
-
if (initialEmail)
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
useEffect(() => {
|
|
478
|
-
if (phase.t === "waiting" && !opened.current) {
|
|
479
|
-
opened.current = true;
|
|
480
|
-
openBrowser(phase.url);
|
|
342
|
+
if (initialEmail && !started.current) {
|
|
343
|
+
started.current = true;
|
|
344
|
+
send(initialEmail);
|
|
481
345
|
}
|
|
482
|
-
}, [
|
|
346
|
+
}, []);
|
|
483
347
|
return /* @__PURE__ */ jsxs(Box, {
|
|
484
348
|
flexDirection: "column",
|
|
485
349
|
gap: 1,
|
|
@@ -509,9 +373,9 @@ function Login({
|
|
|
509
373
|
children: /* @__PURE__ */ jsx(TextInput, {
|
|
510
374
|
placeholder: "you@company.com",
|
|
511
375
|
onSubmit: (value) => {
|
|
512
|
-
const email = value.trim();
|
|
513
|
-
if (email)
|
|
514
|
-
|
|
376
|
+
const email = value.trim().toLowerCase();
|
|
377
|
+
if (email.includes("@"))
|
|
378
|
+
send(email);
|
|
515
379
|
else
|
|
516
380
|
onExit(1);
|
|
517
381
|
}
|
|
@@ -519,47 +383,48 @@ function Login({
|
|
|
519
383
|
})
|
|
520
384
|
]
|
|
521
385
|
}),
|
|
522
|
-
phase.t === "
|
|
523
|
-
label:
|
|
524
|
-
}),
|
|
525
|
-
phase.t === "registering" && /* @__PURE__ */ jsx(Spinner, {
|
|
526
|
-
label: `Registering ${phase.email}…`
|
|
386
|
+
phase.t === "sending" && /* @__PURE__ */ jsx(Spinner, {
|
|
387
|
+
label: `Sending a code to ${phase.email}…`
|
|
527
388
|
}),
|
|
528
|
-
phase.t === "
|
|
389
|
+
phase.t === "code" && /* @__PURE__ */ jsxs(Box, {
|
|
529
390
|
flexDirection: "column",
|
|
530
|
-
gap: 1,
|
|
531
391
|
children: [
|
|
532
|
-
/* @__PURE__ */ jsxs(
|
|
533
|
-
borderStyle: "round",
|
|
534
|
-
borderColor: color.accent,
|
|
535
|
-
paddingX: 1,
|
|
536
|
-
flexDirection: "column",
|
|
392
|
+
/* @__PURE__ */ jsxs(Text, {
|
|
537
393
|
children: [
|
|
538
|
-
|
|
539
|
-
children: "Open this link, sign in, and enter the code:"
|
|
540
|
-
}),
|
|
541
|
-
/* @__PURE__ */ jsx(Box, {
|
|
542
|
-
marginY: 1,
|
|
543
|
-
children: /* @__PURE__ */ jsx(Badge, {
|
|
544
|
-
color: "cyan",
|
|
545
|
-
children: phase.code
|
|
546
|
-
})
|
|
547
|
-
}),
|
|
394
|
+
"Enter the 6-digit code sent to ",
|
|
548
395
|
/* @__PURE__ */ jsx(Text, {
|
|
549
396
|
color: color.accent,
|
|
550
|
-
children: phase.
|
|
397
|
+
children: phase.email
|
|
551
398
|
}),
|
|
552
|
-
|
|
553
|
-
dimColor: true,
|
|
554
|
-
children: "(the code goes into that page — not back here)"
|
|
555
|
-
})
|
|
399
|
+
":"
|
|
556
400
|
]
|
|
557
401
|
}),
|
|
558
|
-
/* @__PURE__ */ jsx(
|
|
559
|
-
|
|
402
|
+
/* @__PURE__ */ jsx(Box, {
|
|
403
|
+
marginTop: 1,
|
|
404
|
+
children: /* @__PURE__ */ jsx(TextInput, {
|
|
405
|
+
placeholder: "123456",
|
|
406
|
+
onSubmit: (value) => {
|
|
407
|
+
const code = value.trim();
|
|
408
|
+
if (code)
|
|
409
|
+
verify(phase.email, code);
|
|
410
|
+
}
|
|
411
|
+
})
|
|
412
|
+
}),
|
|
413
|
+
phase.error && /* @__PURE__ */ jsx(Box, {
|
|
414
|
+
marginTop: 1,
|
|
415
|
+
children: /* @__PURE__ */ jsxs(Text, {
|
|
416
|
+
color: color.err,
|
|
417
|
+
children: [
|
|
418
|
+
phase.error,
|
|
419
|
+
" — try again."
|
|
420
|
+
]
|
|
421
|
+
})
|
|
560
422
|
})
|
|
561
423
|
]
|
|
562
424
|
}),
|
|
425
|
+
phase.t === "verifying" && /* @__PURE__ */ jsx(Spinner, {
|
|
426
|
+
label: "Verifying…"
|
|
427
|
+
}),
|
|
563
428
|
phase.t === "done" && /* @__PURE__ */ jsxs(StatusMessage, {
|
|
564
429
|
variant: "success",
|
|
565
430
|
children: [
|
|
@@ -576,9 +441,7 @@ function Login({
|
|
|
576
441
|
});
|
|
577
442
|
}
|
|
578
443
|
var init_Login = __esm(() => {
|
|
579
|
-
init_loginFlow();
|
|
580
444
|
init_auth();
|
|
581
|
-
init_openBrowser();
|
|
582
445
|
init_theme();
|
|
583
446
|
});
|
|
584
447
|
|
|
@@ -616,9 +479,9 @@ function readManagedSettings() {
|
|
|
616
479
|
}
|
|
617
480
|
function writeAllowlistWithSudo() {
|
|
618
481
|
const path = managedSettingsPath();
|
|
619
|
-
const
|
|
482
|
+
const json = JSON.stringify(mergeManagedSettings(readManagedSettings()), null, 2);
|
|
620
483
|
const script = `mkdir -p "${dirname3(path)}" && cat > "${path}"`;
|
|
621
|
-
const r = spawnSync2("sudo", ["sh", "-c", script], { input:
|
|
484
|
+
const r = spawnSync2("sudo", ["sh", "-c", script], { input: json, stdio: ["pipe", "inherit", "inherit"] });
|
|
622
485
|
return r.status === 0;
|
|
623
486
|
}
|
|
624
487
|
async function initCommand(rest, flags = {}, env = process.env) {
|
package/package.json
CHANGED
package/plugin/commands/init.md
CHANGED
|
@@ -1,37 +1,29 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Set up vibegroup —
|
|
2
|
+
description: Set up vibegroup — sign in with an email code, create/join a team, launch a channel session.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
Onboard the user to vibegroup. **
|
|
5
|
+
Onboard the user to vibegroup. Sign-in is **passwordless email code** — you send a code, the user reads it from their inbox and pastes it to you. Do NOT run `vibegroup login` (that's the human/terminal path). Use AskUserQuestion for choices; keep steps terse.
|
|
6
6
|
|
|
7
7
|
## Phase 0 — Already authenticated?
|
|
8
8
|
|
|
9
9
|
Read `~/.claude/vibegroup/auth.json` (honor `CLAUDE_CONFIG_DIR`). If it has a non-empty `accessToken`, you're signed in — skip to Phase 2.
|
|
10
10
|
|
|
11
|
-
## Phase 1 —
|
|
12
|
-
|
|
13
|
-
vibegroup publishes its agent-auth instructions at `https://api.vibegroup.sh/auth.md`. Follow the **`service_auth` (user-claimed)** path — no anonymous:
|
|
11
|
+
## Phase 1 — Sign in (email code, no browser)
|
|
14
12
|
|
|
15
13
|
1. Ask the user which email to sign in with (e.g. `terry@cruz.pe`).
|
|
16
|
-
2.
|
|
14
|
+
2. Send a code to that email:
|
|
17
15
|
```bash
|
|
18
|
-
curl -s -X POST https://api.vibegroup.sh/
|
|
19
|
-
-H 'content-type: application/json'
|
|
20
|
-
-d '{"type":"service_auth","login_hint":"<email>"}'
|
|
16
|
+
curl -s -X POST https://api.vibegroup.sh/auth/email/start \
|
|
17
|
+
-H 'content-type: application/json' -d '{"email":"<email>"}'
|
|
21
18
|
```
|
|
22
|
-
|
|
23
|
-
3.
|
|
24
|
-
|
|
25
|
-
> <verification_uri>
|
|
26
|
-
> (the code goes into that page — not back to me)
|
|
27
|
-
4. Poll until they confirm — wait `interval` seconds (default 5) between calls, give up after ~10 min:
|
|
19
|
+
(`{"ok":true}` means it's on the way.)
|
|
20
|
+
3. Tell the user, in one message: *"I sent a 6-digit code to `<email>` — paste it here when it arrives."* Then wait for them to give you the code.
|
|
21
|
+
4. Verify the code (this proves they own the email + signs them in):
|
|
28
22
|
```bash
|
|
29
|
-
curl -s -X POST https://api.vibegroup.sh/
|
|
30
|
-
-H 'content-type: application/
|
|
31
|
-
--data-urlencode 'grant_type=urn:workos:agent-auth:grant-type:claim' \
|
|
32
|
-
--data-urlencode 'claim_token=<claim_token>'
|
|
23
|
+
curl -s -X POST https://api.vibegroup.sh/auth/email/verify \
|
|
24
|
+
-H 'content-type: application/json' -d '{"email":"<email>","code":"<code>"}'
|
|
33
25
|
```
|
|
34
|
-
`{"error":"
|
|
26
|
+
`{"error":"invalid_code"}` → tell them it was wrong/expired and ask again (re-send with step 2 if needed). On success the response has `access_token` + `identity_assertion`.
|
|
35
27
|
5. Persist the session so the channel + CLI pick it up — write `~/.claude/vibegroup/auth.json` (file mode 600):
|
|
36
28
|
```json
|
|
37
29
|
{ "accessToken": "<access_token>", "identityAssertion": "<identity_assertion>", "user": { "id": "<email>", "email": "<email>" } }
|