vibegroup 0.1.3 → 0.1.5

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.5",
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: {
@@ -230,6 +230,32 @@ var init_install = __esm(() => {
230
230
  init_pluginInstall();
231
231
  });
232
232
 
233
+ // src/lib/channel.ts
234
+ import { spawnSync as spawnSync2 } from "node:child_process";
235
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
236
+ import { dirname as dirname3 } from "node:path";
237
+ function readManagedSettings() {
238
+ try {
239
+ const p = managedSettingsPath();
240
+ if (existsSync3(p))
241
+ return JSON.parse(readFileSync3(p, "utf8"));
242
+ } catch {}
243
+ return null;
244
+ }
245
+ function channelAllowlisted() {
246
+ return isAllowlisted(readManagedSettings());
247
+ }
248
+ function enableChannelWithSudo() {
249
+ const path = managedSettingsPath();
250
+ const json = JSON.stringify(mergeManagedSettings(readManagedSettings()), null, 2);
251
+ const script = `mkdir -p "${dirname3(path)}" && cat > "${path}"`;
252
+ const r = spawnSync2("sudo", ["sh", "-c", script], { input: json, stdio: ["pipe", "inherit", "inherit"] });
253
+ return r.status === 0;
254
+ }
255
+ var init_channel = __esm(() => {
256
+ init_allowlist();
257
+ });
258
+
233
259
  // src/ui/runner.ts
234
260
  import { render } from "ink";
235
261
  function runInk(make) {
@@ -249,144 +275,30 @@ function runInk(make) {
249
275
  }
250
276
  var init_runner = () => {};
251
277
 
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, {
278
+ // src/lib/emailAuth.ts
279
+ async function startEmailLogin(apiBase, email, f) {
280
+ const res = await f(`${apiBase}/auth/email/start`, {
313
281
  method: "POST",
314
282
  headers: { "content-type": "application/json" },
315
- body: JSON.stringify(body)
283
+ body: JSON.stringify({ email })
316
284
  });
317
- const data = await json(res);
318
- if (!res.ok && !data.registration_id) {
319
- throw new Error(`register failed: ${data.error ?? `HTTP ${res.status}`}`);
285
+ if (!res.ok) {
286
+ const err = await res.json().catch(() => ({}));
287
+ throw new Error(err.detail ?? "could not send the code");
320
288
  }
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
289
  }
332
- async function pollClaim(tokenEndpoint, claimToken, f, nowMs = Date.now()) {
333
- const res = await f(tokenEndpoint, {
290
+ async function verifyEmailLogin(apiBase, email, code, f) {
291
+ const res = await f(`${apiBase}/auth/email/verify`, {
334
292
  method: "POST",
335
- headers: { "content-type": "application/x-www-form-urlencoded" },
336
- body: new URLSearchParams({ grant_type: CLAIM_GRANT, claim_token: claimToken }).toString()
293
+ headers: { "content-type": "application/json" },
294
+ body: JSON.stringify({ email, code })
337
295
  });
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}`}`);
296
+ const data = await res.json().catch(() => ({}));
297
+ if (!res.ok) {
298
+ throw new Error(data.error === "invalid_code" ? "that code is incorrect or expired" : data.detail ?? "verification failed");
350
299
  }
300
+ return { accessToken: data.access_token ?? "", identityAssertion: data.identity_assertion, scope: data.scope };
351
301
  }
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
302
 
391
303
  // src/ui/session.ts
392
304
  function jwtClaim(jwt, key) {
@@ -406,17 +318,6 @@ function sessionFromTokens(tokens, email) {
406
318
  };
407
319
  }
408
320
 
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
321
  // src/ui/theme.ts
421
322
  var color;
422
323
  var init_theme = __esm(() => {
@@ -433,53 +334,42 @@ var init_theme = __esm(() => {
433
334
  // src/ui/Login.tsx
434
335
  import { useEffect, useRef, useState } from "react";
435
336
  import { Box, Text } from "ink";
436
- import { Spinner, StatusMessage, TextInput, Alert, Badge } from "@inkjs/ui";
337
+ import { Spinner, StatusMessage, TextInput, Alert } from "@inkjs/ui";
437
338
  import { jsx, jsxs } from "react/jsx-runtime";
438
339
  function Login({
439
340
  apiBase,
440
341
  initialEmail,
441
342
  onExit
442
343
  }) {
443
- const [phase, setPhase] = useState(initialEmail ? { t: "connecting" } : { t: "email" });
444
- const opened = useRef(false);
344
+ const [phase, setPhase] = useState(initialEmail ? { t: "sending", email: initialEmail } : { t: "email" });
445
345
  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) => {
346
+ const send = async (email) => {
347
+ setPhase({ t: "sending", email });
348
+ try {
349
+ await startEmailLogin(apiBase, email, fetch);
350
+ setPhase({ t: "code", email });
351
+ } catch (err) {
469
352
  setPhase({ t: "error", message: err?.message ?? String(err) });
470
353
  setTimeout(() => onExit(1), 800);
471
- });
354
+ }
355
+ };
356
+ const verify = async (email, code) => {
357
+ setPhase({ t: "verifying", email });
358
+ try {
359
+ const tokens = await verifyEmailLogin(apiBase, email, code, fetch);
360
+ writeAuth(sessionFromTokens(tokens, email));
361
+ setPhase({ t: "done", email });
362
+ setTimeout(() => onExit(0), 500);
363
+ } catch (err) {
364
+ setPhase({ t: "code", email, error: err?.message ?? String(err) });
365
+ }
472
366
  };
473
367
  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);
368
+ if (initialEmail && !started.current) {
369
+ started.current = true;
370
+ send(initialEmail);
481
371
  }
482
- }, [phase]);
372
+ }, []);
483
373
  return /* @__PURE__ */ jsxs(Box, {
484
374
  flexDirection: "column",
485
375
  gap: 1,
@@ -509,9 +399,9 @@ function Login({
509
399
  children: /* @__PURE__ */ jsx(TextInput, {
510
400
  placeholder: "you@company.com",
511
401
  onSubmit: (value) => {
512
- const email = value.trim();
513
- if (email)
514
- start(email);
402
+ const email = value.trim().toLowerCase();
403
+ if (email.includes("@"))
404
+ send(email);
515
405
  else
516
406
  onExit(1);
517
407
  }
@@ -519,47 +409,48 @@ function Login({
519
409
  })
520
410
  ]
521
411
  }),
522
- phase.t === "connecting" && /* @__PURE__ */ jsx(Spinner, {
523
- label: "Connecting to vibegroup…"
412
+ phase.t === "sending" && /* @__PURE__ */ jsx(Spinner, {
413
+ label: `Sending a code to ${phase.email}…`
524
414
  }),
525
- phase.t === "registering" && /* @__PURE__ */ jsx(Spinner, {
526
- label: `Registering ${phase.email}…`
527
- }),
528
- phase.t === "waiting" && /* @__PURE__ */ jsxs(Box, {
415
+ phase.t === "code" && /* @__PURE__ */ jsxs(Box, {
529
416
  flexDirection: "column",
530
- gap: 1,
531
417
  children: [
532
- /* @__PURE__ */ jsxs(Box, {
533
- borderStyle: "round",
534
- borderColor: color.accent,
535
- paddingX: 1,
536
- flexDirection: "column",
418
+ /* @__PURE__ */ jsxs(Text, {
537
419
  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
- }),
420
+ "Enter the 6-digit code sent to ",
548
421
  /* @__PURE__ */ jsx(Text, {
549
422
  color: color.accent,
550
- children: phase.url
423
+ children: phase.email
551
424
  }),
552
- /* @__PURE__ */ jsx(Text, {
553
- dimColor: true,
554
- children: "(the code goes into that page — not back here)"
555
- })
425
+ ":"
556
426
  ]
557
427
  }),
558
- /* @__PURE__ */ jsx(Spinner, {
559
- label: "Waiting for you to confirm in the browser…"
428
+ /* @__PURE__ */ jsx(Box, {
429
+ marginTop: 1,
430
+ children: /* @__PURE__ */ jsx(TextInput, {
431
+ placeholder: "123456",
432
+ onSubmit: (value) => {
433
+ const code = value.trim();
434
+ if (code)
435
+ verify(phase.email, code);
436
+ }
437
+ })
438
+ }),
439
+ phase.error && /* @__PURE__ */ jsx(Box, {
440
+ marginTop: 1,
441
+ children: /* @__PURE__ */ jsxs(Text, {
442
+ color: color.err,
443
+ children: [
444
+ phase.error,
445
+ " — try again."
446
+ ]
447
+ })
560
448
  })
561
449
  ]
562
450
  }),
451
+ phase.t === "verifying" && /* @__PURE__ */ jsx(Spinner, {
452
+ label: "Verifying…"
453
+ }),
563
454
  phase.t === "done" && /* @__PURE__ */ jsxs(StatusMessage, {
564
455
  variant: "success",
565
456
  children: [
@@ -576,9 +467,7 @@ function Login({
576
467
  });
577
468
  }
578
469
  var init_Login = __esm(() => {
579
- init_loginFlow();
580
470
  init_auth();
581
- init_openBrowser();
582
471
  init_theme();
583
472
  });
584
473
 
@@ -603,24 +492,6 @@ var exports_init = {};
603
492
  __export(exports_init, {
604
493
  initCommand: () => initCommand
605
494
  });
606
- import { spawnSync as spawnSync2 } from "node:child_process";
607
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
608
- import { dirname as dirname3 } from "node:path";
609
- function readManagedSettings() {
610
- try {
611
- const p = managedSettingsPath();
612
- if (existsSync3(p))
613
- return JSON.parse(readFileSync3(p, "utf8"));
614
- } catch {}
615
- return null;
616
- }
617
- function writeAllowlistWithSudo() {
618
- const path = managedSettingsPath();
619
- const json2 = JSON.stringify(mergeManagedSettings(readManagedSettings()), null, 2);
620
- const script = `mkdir -p "${dirname3(path)}" && cat > "${path}"`;
621
- const r = spawnSync2("sudo", ["sh", "-c", script], { input: json2, stdio: ["pipe", "inherit", "inherit"] });
622
- return r.status === 0;
623
- }
624
495
  async function initCommand(rest, flags = {}, env = process.env) {
625
496
  const dev = flags.dev === true;
626
497
  console.log(`vibegroup setup
@@ -642,13 +513,13 @@ async function initCommand(rest, flags = {}, env = process.env) {
642
513
  }
643
514
  if (dev) {
644
515
  console.log("• Dev mode: skipping the channel allowlist (launch with `vibegroup claude --dev`).");
645
- } else if (isAllowlisted(readManagedSettings())) {
516
+ } else if (channelAllowlisted()) {
646
517
  console.log("✓ Channel already enabled.");
647
518
  } else {
648
519
  console.log(`
649
520
  • Enabling the vibegroup channel needs admin once (Claude Code gates channel plugins).`);
650
- console.log(` Writing ${managedSettingsPath()} — enter your password if prompted.`);
651
- if (!writeAllowlistWithSudo()) {
521
+ console.log(" Enter your password if prompted.");
522
+ if (!enableChannelWithSudo()) {
652
523
  console.error(" Could not write managed settings. Retry, or run `vibegroup init --dev` to skip it and use `vibegroup claude --dev`.");
653
524
  return 1;
654
525
  }
@@ -671,7 +542,7 @@ async function initCommand(rest, flags = {}, env = process.env) {
671
542
  }
672
543
  var init_init = __esm(() => {
673
544
  init_auth();
674
- init_allowlist();
545
+ init_channel();
675
546
  init_pluginInstall();
676
547
  init_login();
677
548
  });
@@ -836,7 +707,7 @@ function extractFlag(args, name) {
836
707
  }
837
708
  return { value, rest };
838
709
  }
839
- function claudeCommand(args, env = process.env, launcher) {
710
+ function claudeCommand(args, env = process.env, launcher, channel = realChannelGate) {
840
711
  if (!isLoggedIn(readAuth(env))) {
841
712
  console.error("Not logged in — run `vibegroup login` first.");
842
713
  return 1;
@@ -848,6 +719,13 @@ function claudeCommand(args, env = process.env, launcher) {
848
719
  console.error("No team selected — pass `--team <slug>` (the team whose room you want to join).");
849
720
  return 1;
850
721
  }
722
+ if (!dangerously && !channel.allowlisted()) {
723
+ console.log("Enabling the vibegroup channel — one-time, needs admin. Enter your password if prompted.");
724
+ if (!channel.enable()) {
725
+ console.error("Could not enable the channel. Retry, or launch with `vibegroup claude --dev` (no admin needed).");
726
+ return 1;
727
+ }
728
+ }
851
729
  const vg = {
852
730
  VIBEGROUP_TEAM: team,
853
731
  VIBEGROUP_ROOM: room && room.length > 0 ? room : "general",
@@ -855,10 +733,12 @@ function claudeCommand(args, env = process.env, launcher) {
855
733
  };
856
734
  return launchClaude({ extraArgs: extra, dangerously, env: vg }, launcher);
857
735
  }
858
- var DEFAULT_API_BASE = "https://api.vibegroup.sh";
736
+ var DEFAULT_API_BASE = "https://api.vibegroup.sh", realChannelGate;
859
737
  var init_claudeCmd = __esm(() => {
860
738
  init_claudeLaunch();
861
739
  init_auth();
740
+ init_channel();
741
+ realChannelGate = { allowlisted: channelAllowlisted, enable: enableChannelWithSudo };
862
742
  });
863
743
 
864
744
  // src/cli.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibegroup",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
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>" } }
@@ -45,10 +37,11 @@ The session is cached now, so the `vibegroup` CLI works. AskUserQuestion: **"Cre
45
37
 
46
38
  ## Phase 3 — Launch a channel session
47
39
 
48
- If the vibegroup channel isn't set up on this machine yet, tell the user to run `vibegroup init` once in their terminal (it installs the plugin and enables the channel needs their password once). Then:
40
+ The vibegroup channel is admin-gated (it lives in root-owned managed settings), so **you can't enable it from here** it's a terminal action, like login. Tell the user to run, in their terminal:
49
41
  ```bash
50
42
  vibegroup claude --team <slug>
51
43
  ```
44
+ The **first run enables the channel** (one `sudo` password prompt), then launches Claude Code in the room. Every later run just launches. If they'd rather not use admin, `vibegroup claude --team <slug> --dev` skips it via the development-channel flag (no password).
52
45
 
53
46
  ## Done
54
47