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 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.3",
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/workosAuth.ts
253
- async function json(res) {
254
- const text = await res.text();
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(body)
257
+ body: JSON.stringify({ email })
316
258
  });
317
- const data = await json(res);
318
- if (!res.ok && !data.registration_id) {
319
- throw new Error(`register failed: ${data.error ?? `HTTP ${res.status}`}`);
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 pollClaim(tokenEndpoint, claimToken, f, nowMs = Date.now()) {
333
- const res = await f(tokenEndpoint, {
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/x-www-form-urlencoded" },
336
- body: new URLSearchParams({ grant_type: CLAIM_GRANT, claim_token: claimToken }).toString()
267
+ headers: { "content-type": "application/json" },
268
+ body: JSON.stringify({ email, code })
337
269
  });
338
- const data = await json(res);
339
- if (res.ok && data.access_token)
340
- return { status: "done", tokens: tokensFromResponse(data, nowMs) };
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, Badge } from "@inkjs/ui";
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: "connecting" } : { t: "email" });
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 start = (email) => {
447
- if (started.current)
448
- return;
449
- started.current = true;
450
- setPhase({ t: "connecting" });
451
- login(apiBase, fetch, {
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
- start(initialEmail);
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
- }, [phase]);
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
- start(email);
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 === "connecting" && /* @__PURE__ */ jsx(Spinner, {
523
- label: "Connecting to vibegroup…"
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 === "waiting" && /* @__PURE__ */ jsxs(Box, {
389
+ phase.t === "code" && /* @__PURE__ */ jsxs(Box, {
529
390
  flexDirection: "column",
530
- gap: 1,
531
391
  children: [
532
- /* @__PURE__ */ jsxs(Box, {
533
- borderStyle: "round",
534
- borderColor: color.accent,
535
- paddingX: 1,
536
- flexDirection: "column",
392
+ /* @__PURE__ */ jsxs(Text, {
537
393
  children: [
538
- /* @__PURE__ */ jsx(Text, {
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.url
397
+ children: phase.email
551
398
  }),
552
- /* @__PURE__ */ jsx(Text, {
553
- dimColor: true,
554
- children: "(the code goes into that page — not back here)"
555
- })
399
+ ":"
556
400
  ]
557
401
  }),
558
- /* @__PURE__ */ jsx(Spinner, {
559
- label: "Waiting for you to confirm in the browser…"
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 json2 = JSON.stringify(mergeManagedSettings(readManagedSettings()), null, 2);
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: json2, stdio: ["pipe", "inherit", "inherit"] });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibegroup",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Talk to your teammates' Claude Code agents — agent-to-agent collaboration for Claude Code over a shared channel.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,37 +1,29 @@
1
1
  ---
2
- description: Set up vibegroup — authenticate by following auth.md, create/join a team, launch a channel session.
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. **You (the agent) follow the auth.md protocol yourself** do NOT run `vibegroup login` (that's the human/terminal path). Use AskUserQuestion for choices; keep steps terse.
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 — Authenticate (follow auth.md, `service_auth`)
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. Register on their behalf:
14
+ 2. Send a code to that email:
17
15
  ```bash
18
- curl -s -X POST https://api.vibegroup.sh/agent/identity \
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
- Capture `claim_token`, `claim.user_code`, `claim.verification_uri`, and `claim.interval` from the JSON.
23
- 3. Surface to the user, in one message:
24
- > Open this link, sign in to vibegroup (it verifies your email), and enter the code **<user_code>**:
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/oauth2/token \
30
- -H 'content-type: application/x-www-form-urlencoded' \
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":"authorization_pending"}` → keep polling. `{"error":"expired_token"}` re-register (step 2). On success the response has `access_token` + `identity_assertion`.
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>" } }