vibegroup 0.1.7 → 0.1.9

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.7",
73
+ version: "0.1.9",
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: {
@@ -134,6 +134,209 @@ var init_package = __esm(() => {
134
134
  };
135
135
  });
136
136
 
137
+ // src/ui/runner.ts
138
+ import { render } from "ink";
139
+ function runInk(make) {
140
+ return new Promise((resolve) => {
141
+ let done = false;
142
+ let instance;
143
+ const finish = (code) => {
144
+ if (done)
145
+ return;
146
+ done = true;
147
+ instance?.unmount();
148
+ resolve(code);
149
+ };
150
+ instance = render(make(finish));
151
+ instance.waitUntilExit().then(() => finish(130));
152
+ });
153
+ }
154
+ var init_runner = () => {};
155
+
156
+ // src/ui/theme.ts
157
+ var color;
158
+ var init_theme = __esm(() => {
159
+ color = {
160
+ brand: "magenta",
161
+ accent: "cyan",
162
+ ok: "green",
163
+ warn: "yellow",
164
+ err: "red",
165
+ dim: "gray"
166
+ };
167
+ });
168
+
169
+ // src/ui/Home.tsx
170
+ import { useState } from "react";
171
+ import { Box, Text, useInput } from "ink";
172
+ import { jsx, jsxs } from "react/jsx-runtime";
173
+ function Home({
174
+ identity,
175
+ version,
176
+ items = HOME_ITEMS,
177
+ onSelect
178
+ }) {
179
+ const [i, setI] = useState(0);
180
+ useInput((input, key) => {
181
+ if (input === "q" || key.escape || key.ctrl && input === "c")
182
+ return onSelect(null);
183
+ if (key.upArrow)
184
+ setI((s) => (s - 1 + items.length) % items.length);
185
+ if (key.downArrow)
186
+ setI((s) => (s + 1) % items.length);
187
+ if (key.return)
188
+ return onSelect(items[i].cmd);
189
+ });
190
+ const sel = items[i];
191
+ return /* @__PURE__ */ jsxs(Box, {
192
+ flexDirection: "column",
193
+ paddingX: 1,
194
+ paddingY: 1,
195
+ gap: 1,
196
+ children: [
197
+ /* @__PURE__ */ jsxs(Box, {
198
+ borderStyle: "round",
199
+ borderColor: color.brand,
200
+ paddingX: 1,
201
+ flexDirection: "column",
202
+ children: [
203
+ /* @__PURE__ */ jsxs(Text, {
204
+ children: [
205
+ /* @__PURE__ */ jsx(Text, {
206
+ color: color.brand,
207
+ bold: true,
208
+ children: "vibegroup"
209
+ }),
210
+ /* @__PURE__ */ jsx(Text, {
211
+ dimColor: true,
212
+ children: " talk to your teammates' Claude Code agents"
213
+ })
214
+ ]
215
+ }),
216
+ /* @__PURE__ */ jsxs(Text, {
217
+ dimColor: true,
218
+ children: [
219
+ identity ? `signed in as ${identity}` : "not signed in — start with init or login",
220
+ " · ",
221
+ "v",
222
+ version
223
+ ]
224
+ })
225
+ ]
226
+ }),
227
+ /* @__PURE__ */ jsx(Box, {
228
+ flexDirection: "column",
229
+ children: items.map((it, idx) => {
230
+ const active = idx === i;
231
+ return /* @__PURE__ */ jsxs(Box, {
232
+ children: [
233
+ /* @__PURE__ */ jsx(Text, {
234
+ color: active ? color.accent : undefined,
235
+ children: active ? "❯ " : " "
236
+ }),
237
+ /* @__PURE__ */ jsx(Box, {
238
+ width: 14,
239
+ children: /* @__PURE__ */ jsx(Text, {
240
+ bold: active,
241
+ color: active ? color.accent : undefined,
242
+ children: it.label
243
+ })
244
+ }),
245
+ /* @__PURE__ */ jsx(Text, {
246
+ dimColor: true,
247
+ children: it.desc
248
+ })
249
+ ]
250
+ }, it.cmd);
251
+ })
252
+ }),
253
+ /* @__PURE__ */ jsx(Box, {
254
+ borderStyle: "round",
255
+ borderColor: color.dim,
256
+ paddingX: 1,
257
+ children: /* @__PURE__ */ jsxs(Text, {
258
+ children: [
259
+ /* @__PURE__ */ jsxs(Text, {
260
+ dimColor: true,
261
+ children: [
262
+ sel.runnable ? "runs" : "type",
263
+ ": "
264
+ ]
265
+ }),
266
+ /* @__PURE__ */ jsx(Text, {
267
+ color: color.accent,
268
+ children: sel.usage
269
+ })
270
+ ]
271
+ })
272
+ }),
273
+ /* @__PURE__ */ jsxs(Text, {
274
+ dimColor: true,
275
+ children: [
276
+ "↑↓ navigate · ⏎ ",
277
+ sel.runnable ? "run" : "show command",
278
+ " · q quit"
279
+ ]
280
+ })
281
+ ]
282
+ });
283
+ }
284
+ var HOME_ITEMS;
285
+ var init_Home = __esm(() => {
286
+ init_theme();
287
+ HOME_ITEMS = [
288
+ { cmd: "init", label: "init", desc: "One-time setup: install plugin, enable channel, sign in", usage: "vibegroup init [email]", runnable: true },
289
+ { cmd: "who", label: "who", desc: "Live view of who's in a room (people + sessions)", usage: "vibegroup who --team <slug> [--room <name>]", runnable: false },
290
+ { cmd: "claude", label: "claude", desc: "Launch Claude Code wired to a room", usage: "vibegroup claude --team <slug> [--room <name>] [--session <label>]", runnable: false },
291
+ { cmd: "status", label: "status", desc: "Your auth + connection status", usage: "vibegroup status", runnable: true },
292
+ { cmd: "team", label: "team create", desc: "Create a team (a WorkOS org + a general room)", usage: "vibegroup team create <slug> [--name <name>]", runnable: false },
293
+ { cmd: "room", label: "room create", desc: "Add a room to a team", usage: "vibegroup room create <name> --team <slug>", runnable: false },
294
+ { cmd: "rooms", label: "rooms", desc: "List a team's rooms", usage: "vibegroup rooms --team <slug>", runnable: false },
295
+ { cmd: "invite", label: "invite", desc: "Invite someone to a team", usage: "vibegroup invite <email> --team <slug>", runnable: false },
296
+ { cmd: "login", label: "login", desc: "Sign in / sign up (email code)", usage: "vibegroup login [email]", runnable: true },
297
+ { cmd: "logout", label: "logout", desc: "Clear the cached session", usage: "vibegroup logout", runnable: true },
298
+ { cmd: "install", label: "install", desc: "Register the vibegroup plugin in Claude Code", usage: "vibegroup install", runnable: true }
299
+ ];
300
+ });
301
+
302
+ // src/commands/home.ts
303
+ var exports_home = {};
304
+ __export(exports_home, {
305
+ homeCommand: () => homeCommand
306
+ });
307
+ import { createElement } from "react";
308
+ async function homeCommand(env, dispatch, version, help) {
309
+ if (!process.stdout.isTTY) {
310
+ console.log(help);
311
+ return 0;
312
+ }
313
+ const identity = readAuth(env)?.user?.email ?? null;
314
+ let chosen = null;
315
+ await runInk((onExit) => createElement(Home, {
316
+ identity,
317
+ version,
318
+ onSelect: (cmd) => {
319
+ chosen = cmd;
320
+ onExit(0);
321
+ }
322
+ }));
323
+ if (!chosen)
324
+ return 0;
325
+ const item = HOME_ITEMS.find((it) => it.cmd === chosen);
326
+ if (item && !item.runnable) {
327
+ console.log(`
328
+ Run:
329
+ ${item.usage}`);
330
+ return 0;
331
+ }
332
+ return dispatch(chosen);
333
+ }
334
+ var init_home = __esm(() => {
335
+ init_runner();
336
+ init_Home();
337
+ init_auth();
338
+ });
339
+
137
340
  // src/lib/allowlist.ts
138
341
  function managedSettingsPath(plat = process.platform) {
139
342
  if (plat === "darwin")
@@ -266,25 +469,6 @@ var init_channel = __esm(() => {
266
469
  init_allowlist();
267
470
  });
268
471
 
269
- // src/ui/runner.ts
270
- import { render } from "ink";
271
- function runInk(make) {
272
- return new Promise((resolve) => {
273
- let done = false;
274
- let instance;
275
- const finish = (code) => {
276
- if (done)
277
- return;
278
- done = true;
279
- instance?.unmount();
280
- resolve(code);
281
- };
282
- instance = render(make(finish));
283
- instance.waitUntilExit().then(() => finish(130));
284
- });
285
- }
286
- var init_runner = () => {};
287
-
288
472
  // src/lib/emailAuth.ts
289
473
  async function startEmailLogin(apiBase, email, f) {
290
474
  const res = await f(`${apiBase}/auth/email/start`, {
@@ -328,30 +512,17 @@ function sessionFromTokens(tokens, email) {
328
512
  };
329
513
  }
330
514
 
331
- // src/ui/theme.ts
332
- var color;
333
- var init_theme = __esm(() => {
334
- color = {
335
- brand: "magenta",
336
- accent: "cyan",
337
- ok: "green",
338
- warn: "yellow",
339
- err: "red",
340
- dim: "gray"
341
- };
342
- });
343
-
344
515
  // src/ui/Login.tsx
345
- import { useEffect, useRef, useState } from "react";
346
- import { Box, Text } from "ink";
516
+ import { useEffect, useRef, useState as useState2 } from "react";
517
+ import { Box as Box2, Text as Text2 } from "ink";
347
518
  import { Spinner, StatusMessage, TextInput, Alert } from "@inkjs/ui";
348
- import { jsx, jsxs } from "react/jsx-runtime";
519
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
349
520
  function Login({
350
521
  apiBase,
351
522
  initialEmail,
352
523
  onExit
353
524
  }) {
354
- const [phase, setPhase] = useState(initialEmail ? { t: "sending", email: initialEmail } : { t: "email" });
525
+ const [phase, setPhase] = useState2(initialEmail ? { t: "sending", email: initialEmail } : { t: "email" });
355
526
  const started = useRef(false);
356
527
  const send = async (email) => {
357
528
  setPhase({ t: "sending", email });
@@ -380,33 +551,33 @@ function Login({
380
551
  send(initialEmail);
381
552
  }
382
553
  }, []);
383
- return /* @__PURE__ */ jsxs(Box, {
554
+ return /* @__PURE__ */ jsxs2(Box2, {
384
555
  flexDirection: "column",
385
556
  gap: 1,
386
557
  paddingY: 1,
387
558
  children: [
388
- /* @__PURE__ */ jsxs(Text, {
559
+ /* @__PURE__ */ jsxs2(Text2, {
389
560
  children: [
390
- /* @__PURE__ */ jsx(Text, {
561
+ /* @__PURE__ */ jsx2(Text2, {
391
562
  color: color.brand,
392
563
  bold: true,
393
564
  children: "vibegroup"
394
565
  }),
395
- /* @__PURE__ */ jsx(Text, {
566
+ /* @__PURE__ */ jsx2(Text2, {
396
567
  dimColor: true,
397
568
  children: " · sign in"
398
569
  })
399
570
  ]
400
571
  }),
401
- phase.t === "email" && /* @__PURE__ */ jsxs(Box, {
572
+ phase.t === "email" && /* @__PURE__ */ jsxs2(Box2, {
402
573
  flexDirection: "column",
403
574
  children: [
404
- /* @__PURE__ */ jsx(Text, {
575
+ /* @__PURE__ */ jsx2(Text2, {
405
576
  children: "Email to sign in with:"
406
577
  }),
407
- /* @__PURE__ */ jsx(Box, {
578
+ /* @__PURE__ */ jsx2(Box2, {
408
579
  marginTop: 1,
409
- children: /* @__PURE__ */ jsx(TextInput, {
580
+ children: /* @__PURE__ */ jsx2(TextInput, {
410
581
  placeholder: "you@company.com",
411
582
  onSubmit: (value) => {
412
583
  const email = value.trim().toLowerCase();
@@ -419,25 +590,25 @@ function Login({
419
590
  })
420
591
  ]
421
592
  }),
422
- phase.t === "sending" && /* @__PURE__ */ jsx(Spinner, {
593
+ phase.t === "sending" && /* @__PURE__ */ jsx2(Spinner, {
423
594
  label: `Sending a code to ${phase.email}…`
424
595
  }),
425
- phase.t === "code" && /* @__PURE__ */ jsxs(Box, {
596
+ phase.t === "code" && /* @__PURE__ */ jsxs2(Box2, {
426
597
  flexDirection: "column",
427
598
  children: [
428
- /* @__PURE__ */ jsxs(Text, {
599
+ /* @__PURE__ */ jsxs2(Text2, {
429
600
  children: [
430
601
  "Enter the 6-digit code sent to ",
431
- /* @__PURE__ */ jsx(Text, {
602
+ /* @__PURE__ */ jsx2(Text2, {
432
603
  color: color.accent,
433
604
  children: phase.email
434
605
  }),
435
606
  ":"
436
607
  ]
437
608
  }),
438
- /* @__PURE__ */ jsx(Box, {
609
+ /* @__PURE__ */ jsx2(Box2, {
439
610
  marginTop: 1,
440
- children: /* @__PURE__ */ jsx(TextInput, {
611
+ children: /* @__PURE__ */ jsx2(TextInput, {
441
612
  placeholder: "123456",
442
613
  onSubmit: (value) => {
443
614
  const code = value.trim();
@@ -446,9 +617,9 @@ function Login({
446
617
  }
447
618
  })
448
619
  }),
449
- phase.error && /* @__PURE__ */ jsx(Box, {
620
+ phase.error && /* @__PURE__ */ jsx2(Box2, {
450
621
  marginTop: 1,
451
- children: /* @__PURE__ */ jsxs(Text, {
622
+ children: /* @__PURE__ */ jsxs2(Text2, {
452
623
  color: color.err,
453
624
  children: [
454
625
  phase.error,
@@ -458,10 +629,10 @@ function Login({
458
629
  })
459
630
  ]
460
631
  }),
461
- phase.t === "verifying" && /* @__PURE__ */ jsx(Spinner, {
632
+ phase.t === "verifying" && /* @__PURE__ */ jsx2(Spinner, {
462
633
  label: "Verifying…"
463
634
  }),
464
- phase.t === "done" && /* @__PURE__ */ jsxs(StatusMessage, {
635
+ phase.t === "done" && /* @__PURE__ */ jsxs2(StatusMessage, {
465
636
  variant: "success",
466
637
  children: [
467
638
  "Signed in as ",
@@ -469,7 +640,7 @@ function Login({
469
640
  ". You're good to go!"
470
641
  ]
471
642
  }),
472
- phase.t === "error" && /* @__PURE__ */ jsx(Alert, {
643
+ phase.t === "error" && /* @__PURE__ */ jsx2(Alert, {
473
644
  variant: "error",
474
645
  children: phase.message
475
646
  })
@@ -486,10 +657,10 @@ var exports_login = {};
486
657
  __export(exports_login, {
487
658
  loginCommand: () => loginCommand
488
659
  });
489
- import { createElement } from "react";
660
+ import { createElement as createElement2 } from "react";
490
661
  async function loginCommand(rest, env = process.env) {
491
662
  const base = apiBase(env);
492
- return runInk((onExit) => createElement(Login, { apiBase: base, initialEmail: rest[0], onExit }));
663
+ return runInk((onExit) => createElement2(Login, { apiBase: base, initialEmail: rest[0], onExit }));
493
664
  }
494
665
  var init_login = __esm(() => {
495
666
  init_runner();
@@ -557,6 +728,60 @@ var init_init = __esm(() => {
557
728
  init_login();
558
729
  });
559
730
 
731
+ // src/lib/refresh.ts
732
+ function jwtExpMs(token) {
733
+ try {
734
+ const payload = JSON.parse(Buffer.from(token.split(".")[1], "base64url").toString());
735
+ return typeof payload.exp === "number" ? payload.exp * 1000 : null;
736
+ } catch {
737
+ return null;
738
+ }
739
+ }
740
+ function tokenExpired(token, now = Date.now(), skewMs = 30000) {
741
+ const exp = jwtExpMs(token);
742
+ return exp !== null && now >= exp - skewMs;
743
+ }
744
+ async function refreshAccessToken(apiBase2, assertion, f = fetch) {
745
+ const res = await f(`${apiBase2.replace(/\/+$/, "")}/oauth2/token`, {
746
+ method: "POST",
747
+ headers: { "content-type": "application/x-www-form-urlencoded" },
748
+ body: new URLSearchParams({ grant_type: JWT_BEARER_GRANT, assertion }).toString()
749
+ });
750
+ if (!res.ok)
751
+ throw new Error(`token refresh failed (HTTP ${res.status})`);
752
+ const d = await res.json();
753
+ if (!d.access_token)
754
+ throw new Error("token refresh response missing access_token");
755
+ return { accessToken: d.access_token, expiresIn: d.expires_in ?? 3600, scope: d.scope };
756
+ }
757
+ async function getValidAccessToken(apiBase2, env = process.env, f = fetch, now = Date.now()) {
758
+ const auth = readAuth(env);
759
+ if (!auth?.accessToken)
760
+ return null;
761
+ if (!tokenExpired(auth.accessToken, now))
762
+ return auth.accessToken;
763
+ if (!auth.identityAssertion || tokenExpired(auth.identityAssertion, now, 0))
764
+ return null;
765
+ try {
766
+ const r = await refreshAccessToken(apiBase2, auth.identityAssertion, f);
767
+ const expMs = jwtExpMs(r.accessToken);
768
+ const next = {
769
+ ...auth,
770
+ accessToken: r.accessToken,
771
+ scope: r.scope ?? auth.scope,
772
+ accessTokenExpiresAt: expMs ? new Date(expMs).toISOString() : auth.accessTokenExpiresAt
773
+ };
774
+ writeAuth(next, env);
775
+ return r.accessToken;
776
+ } catch {
777
+ return null;
778
+ }
779
+ }
780
+ var JWT_BEARER_GRANT = "urn:ietf:params:oauth:grant-type:jwt-bearer";
781
+ var init_refresh = __esm(() => {
782
+ init_auth();
783
+ });
784
+
560
785
  // src/lib/apiClient.ts
561
786
  async function call(opts, method, path, body) {
562
787
  const f = opts.fetchImpl ?? fetch;
@@ -607,20 +832,24 @@ __export(exports_team, {
607
832
  roomCommand: () => roomCommand,
608
833
  inviteCommand: () => inviteCommand
609
834
  });
610
- function client(env) {
611
- const auth = readAuth(env);
612
- if (!isLoggedIn(auth)) {
835
+ async function client(env) {
836
+ if (!isLoggedIn(readAuth(env))) {
613
837
  console.error("Not logged in — run `vibegroup login` first.");
614
838
  return null;
615
839
  }
616
- return { apiBase: apiBase(env), token: auth.accessToken };
840
+ const token = await getValidAccessToken(apiBase(env), env);
841
+ if (!token) {
842
+ console.error("Session expired — run `vibegroup login` again.");
843
+ return null;
844
+ }
845
+ return { apiBase: apiBase(env), token };
617
846
  }
618
847
  async function teamCommand(rest, flags, env) {
619
848
  if (rest[0] !== "create" || !rest[1]) {
620
849
  console.error("usage: vibegroup team create <slug> [--name <name>]");
621
850
  return 1;
622
851
  }
623
- const opts = client(env);
852
+ const opts = await client(env);
624
853
  if (!opts)
625
854
  return 1;
626
855
  try {
@@ -639,7 +868,7 @@ async function roomCommand(rest, flags, env) {
639
868
  console.error("usage: vibegroup room create <name> --team <slug>");
640
869
  return 1;
641
870
  }
642
- const opts = client(env);
871
+ const opts = await client(env);
643
872
  if (!opts)
644
873
  return 1;
645
874
  try {
@@ -658,7 +887,7 @@ async function roomsCommand(flags, env) {
658
887
  console.error("usage: vibegroup rooms --team <slug>");
659
888
  return 1;
660
889
  }
661
- const opts = client(env);
890
+ const opts = await client(env);
662
891
  if (!opts)
663
892
  return 1;
664
893
  try {
@@ -678,7 +907,7 @@ async function inviteCommand(rest, flags, env) {
678
907
  console.error("usage: vibegroup invite <email> --team <slug>");
679
908
  return 1;
680
909
  }
681
- const opts = client(env);
910
+ const opts = await client(env);
682
911
  if (!opts)
683
912
  return 1;
684
913
  try {
@@ -693,10 +922,299 @@ async function inviteCommand(rest, flags, env) {
693
922
  var str = (v) => typeof v === "string" ? v : undefined;
694
923
  var init_team = __esm(() => {
695
924
  init_auth();
925
+ init_refresh();
696
926
  init_apiClient();
697
927
  init_cli();
698
928
  });
699
929
 
930
+ // src/lib/presence.ts
931
+ async function fetchPresence(relayHttp, team, room, token, f = fetch) {
932
+ const url = `${relayHttp.replace(/\/+$/, "")}/presence?team=${encodeURIComponent(team)}&room=${encodeURIComponent(room)}`;
933
+ const res = await f(url, { headers: { authorization: `Bearer ${token}` } });
934
+ if (!res.ok)
935
+ throw new Error(`presence request failed (HTTP ${res.status})`);
936
+ const data = await res.json();
937
+ return data.peers ?? [];
938
+ }
939
+ function groupPeers(peers, selfMemberId) {
940
+ const byMember = new Map;
941
+ for (const p of peers) {
942
+ const key = p.memberId || p.peerId;
943
+ let g = byMember.get(key);
944
+ if (!g) {
945
+ g = { name: p.name, memberId: p.memberId ?? "", sessions: [], state: "offline", lastSeen: 0, isYou: false };
946
+ byMember.set(key, g);
947
+ }
948
+ g.sessions.push(p);
949
+ g.lastSeen = Math.max(g.lastSeen, p.lastSeen);
950
+ if (p.state === "available")
951
+ g.state = "available";
952
+ if (p.name && (!g.name || g.name === g.memberId))
953
+ g.name = p.name;
954
+ if (selfMemberId && p.memberId === selfMemberId)
955
+ g.isYou = true;
956
+ }
957
+ return [...byMember.values()].sort((a, b) => b.lastSeen - a.lastSeen);
958
+ }
959
+ var DEFAULT_RELAY_HTTP = "https://relay.vibegroup.sh";
960
+
961
+ // src/ui/Who.tsx
962
+ import { useEffect as useEffect2, useRef as useRef2, useState as useState3 } from "react";
963
+ import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
964
+ import { Spinner as Spinner2 } from "@inkjs/ui";
965
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
966
+ function ago(ms) {
967
+ if (!ms)
968
+ return "—";
969
+ const s = Math.max(0, Math.round((Date.now() - ms) / 1000));
970
+ if (s < 5)
971
+ return "now";
972
+ if (s < 60)
973
+ return `${s}s ago`;
974
+ if (s < 3600)
975
+ return `${Math.round(s / 60)}m ago`;
976
+ return `${Math.round(s / 3600)}h ago`;
977
+ }
978
+ function Who({
979
+ relayHttp,
980
+ team,
981
+ room,
982
+ getToken,
983
+ selfMemberId,
984
+ onExit,
985
+ intervalMs = 3000
986
+ }) {
987
+ const [people, setPeople] = useState3(null);
988
+ const [selected, setSelected] = useState3(0);
989
+ const [error, setError] = useState3("");
990
+ const [updated, setUpdated] = useState3(0);
991
+ const selRef = useRef2(0);
992
+ selRef.current = selected;
993
+ const refresh = async () => {
994
+ try {
995
+ const token = await getToken();
996
+ if (!token) {
997
+ setError("Session expired — run `vibegroup login` again.");
998
+ return;
999
+ }
1000
+ const peers = await fetchPresence(relayHttp, team, room, token, fetch);
1001
+ const grouped = groupPeers(peers, selfMemberId);
1002
+ setPeople(grouped);
1003
+ setSelected((s) => Math.min(s, Math.max(0, grouped.length - 1)));
1004
+ setUpdated(Date.now());
1005
+ setError("");
1006
+ } catch (e) {
1007
+ setError(e?.message ?? String(e));
1008
+ }
1009
+ };
1010
+ useEffect2(() => {
1011
+ refresh();
1012
+ const t = setInterval(() => void refresh(), intervalMs);
1013
+ return () => clearInterval(t);
1014
+ }, []);
1015
+ useInput2((input, key) => {
1016
+ if (input === "q" || key.escape || key.ctrl && input === "c")
1017
+ return onExit(0);
1018
+ if (input === "r")
1019
+ return void refresh();
1020
+ const n = people?.length ?? 0;
1021
+ if (n === 0)
1022
+ return;
1023
+ if (key.upArrow)
1024
+ setSelected((s) => (s - 1 + n) % n);
1025
+ if (key.downArrow)
1026
+ setSelected((s) => (s + 1) % n);
1027
+ });
1028
+ const sel = people && people.length > 0 ? people[Math.min(selected, people.length - 1)] : undefined;
1029
+ return /* @__PURE__ */ jsxs3(Box3, {
1030
+ flexDirection: "column",
1031
+ paddingX: 1,
1032
+ paddingY: 1,
1033
+ gap: 1,
1034
+ children: [
1035
+ /* @__PURE__ */ jsxs3(Box3, {
1036
+ borderStyle: "round",
1037
+ borderColor: color.brand,
1038
+ paddingX: 1,
1039
+ justifyContent: "space-between",
1040
+ children: [
1041
+ /* @__PURE__ */ jsxs3(Text3, {
1042
+ children: [
1043
+ /* @__PURE__ */ jsx3(Text3, {
1044
+ color: color.brand,
1045
+ bold: true,
1046
+ children: "vibegroup"
1047
+ }),
1048
+ /* @__PURE__ */ jsx3(Text3, {
1049
+ dimColor: true,
1050
+ children: " · "
1051
+ }),
1052
+ /* @__PURE__ */ jsxs3(Text3, {
1053
+ color: color.accent,
1054
+ children: [
1055
+ team,
1056
+ "/",
1057
+ room
1058
+ ]
1059
+ })
1060
+ ]
1061
+ }),
1062
+ /* @__PURE__ */ jsx3(Text3, {
1063
+ dimColor: true,
1064
+ children: people ? `${people.length} ${people.length === 1 ? "person" : "people"}` : ""
1065
+ })
1066
+ ]
1067
+ }),
1068
+ !people && !error && /* @__PURE__ */ jsx3(Spinner2, {
1069
+ label: "Loading who's here…"
1070
+ }),
1071
+ error && /* @__PURE__ */ jsxs3(Text3, {
1072
+ color: color.err,
1073
+ children: [
1074
+ "Couldn't read presence: ",
1075
+ error
1076
+ ]
1077
+ }),
1078
+ people && people.length === 0 && /* @__PURE__ */ jsx3(Text3, {
1079
+ dimColor: true,
1080
+ children: "No one's in this room yet."
1081
+ }),
1082
+ people && people.length > 0 && /* @__PURE__ */ jsx3(Box3, {
1083
+ flexDirection: "column",
1084
+ children: people.map((p, i) => {
1085
+ const active = i === selected;
1086
+ const n = p.sessions.length;
1087
+ return /* @__PURE__ */ jsxs3(Box3, {
1088
+ children: [
1089
+ /* @__PURE__ */ jsx3(Text3, {
1090
+ color: active ? color.accent : undefined,
1091
+ children: active ? "❯ " : " "
1092
+ }),
1093
+ dot(p.state),
1094
+ /* @__PURE__ */ jsxs3(Text3, {
1095
+ bold: active,
1096
+ children: [
1097
+ " ",
1098
+ p.name || "unknown"
1099
+ ]
1100
+ }),
1101
+ p.isYou && /* @__PURE__ */ jsx3(Text3, {
1102
+ color: color.dim,
1103
+ children: " (you)"
1104
+ }),
1105
+ /* @__PURE__ */ jsxs3(Text3, {
1106
+ dimColor: true,
1107
+ children: [
1108
+ " ",
1109
+ n,
1110
+ " ",
1111
+ n === 1 ? "session" : "sessions",
1112
+ " · ",
1113
+ ago(p.lastSeen)
1114
+ ]
1115
+ })
1116
+ ]
1117
+ }, p.memberId || p.name);
1118
+ })
1119
+ }),
1120
+ sel && /* @__PURE__ */ jsxs3(Box3, {
1121
+ flexDirection: "column",
1122
+ borderStyle: "round",
1123
+ borderColor: color.dim,
1124
+ paddingX: 1,
1125
+ children: [
1126
+ /* @__PURE__ */ jsxs3(Text3, {
1127
+ dimColor: true,
1128
+ children: [
1129
+ sel.name,
1130
+ " · ",
1131
+ sel.sessions.length,
1132
+ " ",
1133
+ sel.sessions.length === 1 ? "session" : "sessions"
1134
+ ]
1135
+ }),
1136
+ sel.sessions.slice().sort((a, b) => b.lastSeen - a.lastSeen).map((s) => /* @__PURE__ */ jsxs3(Box3, {
1137
+ children: [
1138
+ dot(s.state),
1139
+ /* @__PURE__ */ jsxs3(Text3, {
1140
+ children: [
1141
+ " ",
1142
+ s.session || shortId(s.peerId)
1143
+ ]
1144
+ }),
1145
+ /* @__PURE__ */ jsxs3(Text3, {
1146
+ dimColor: true,
1147
+ children: [
1148
+ " ",
1149
+ s.state,
1150
+ " · ",
1151
+ ago(s.lastSeen),
1152
+ " · #",
1153
+ shortId(s.peerId)
1154
+ ]
1155
+ })
1156
+ ]
1157
+ }, s.peerId))
1158
+ ]
1159
+ }),
1160
+ /* @__PURE__ */ jsxs3(Text3, {
1161
+ dimColor: true,
1162
+ children: [
1163
+ "↑↓ navigate · r refresh · q quit",
1164
+ updated ? ` · updated ${ago(updated)}` : ""
1165
+ ]
1166
+ })
1167
+ ]
1168
+ });
1169
+ }
1170
+ var dot = (state) => state === "available" ? /* @__PURE__ */ jsx3(Text3, {
1171
+ color: color.ok,
1172
+ children: "●"
1173
+ }) : /* @__PURE__ */ jsx3(Text3, {
1174
+ color: color.dim,
1175
+ children: "○"
1176
+ }), shortId = (peerId) => peerId.split("#")[1]?.slice(0, 8) ?? peerId.slice(0, 8);
1177
+ var init_Who = __esm(() => {
1178
+ init_theme();
1179
+ });
1180
+
1181
+ // src/commands/who.ts
1182
+ var exports_who = {};
1183
+ __export(exports_who, {
1184
+ whoCommand: () => whoCommand
1185
+ });
1186
+ import { createElement as createElement3 } from "react";
1187
+ async function whoCommand(flags, env = process.env) {
1188
+ const auth = readAuth(env);
1189
+ if (!isLoggedIn(auth)) {
1190
+ console.error("Not logged in — run `vibegroup login` first.");
1191
+ return 1;
1192
+ }
1193
+ const team = str2(flags.team);
1194
+ if (!team) {
1195
+ console.error("usage: vibegroup who --team <slug> [--room <name>]");
1196
+ return 1;
1197
+ }
1198
+ const room = str2(flags.room) ?? "general";
1199
+ const relayHttp = env.VIBEGROUP_RELAY_HTTP ?? DEFAULT_RELAY_HTTP;
1200
+ const apiBase2 = env.VIBEGROUP_API ?? "https://api.vibegroup.sh";
1201
+ return runInk((onExit) => createElement3(Who, {
1202
+ relayHttp,
1203
+ team,
1204
+ room,
1205
+ getToken: () => getValidAccessToken(apiBase2, env),
1206
+ selfMemberId: auth.user?.id,
1207
+ onExit
1208
+ }));
1209
+ }
1210
+ var str2 = (v) => typeof v === "string" ? v : undefined;
1211
+ var init_who = __esm(() => {
1212
+ init_runner();
1213
+ init_Who();
1214
+ init_auth();
1215
+ init_refresh();
1216
+ });
1217
+
700
1218
  // src/lib/claudeLaunch.ts
701
1219
  import { spawnSync as spawnSync3 } from "node:child_process";
702
1220
  function buildClaudeArgs(opts = {}) {
@@ -719,6 +1237,7 @@ var exports_claudeCmd = {};
719
1237
  __export(exports_claudeCmd, {
720
1238
  claudeCommand: () => claudeCommand
721
1239
  });
1240
+ import { basename } from "node:path";
722
1241
  function extractFlag(args, name) {
723
1242
  const rest = [];
724
1243
  let value;
@@ -740,18 +1259,24 @@ function extractFlag(args, name) {
740
1259
  }
741
1260
  return { value, rest };
742
1261
  }
743
- function claudeCommand(args, env = process.env, launcher, channel = realChannelGate) {
1262
+ async function claudeCommand(args, env = process.env, launcher, channel = realChannelGate) {
744
1263
  if (!isLoggedIn(readAuth(env))) {
745
1264
  console.error("Not logged in — run `vibegroup login` first.");
746
1265
  return 1;
747
1266
  }
748
1267
  const dangerously = args.includes("--dev");
749
1268
  const { value: team, rest: afterTeam } = extractFlag(args.filter((a) => a !== "--dev"), "team");
750
- const { value: room, rest: extra } = extractFlag(afterTeam, "room");
1269
+ const { value: room, rest: afterRoom } = extractFlag(afterTeam, "room");
1270
+ const { value: sessionFlag, rest: extra } = extractFlag(afterRoom, "session");
751
1271
  if (!team) {
752
1272
  console.error("No team selected — pass `--team <slug>` (the team whose room you want to join).");
753
1273
  return 1;
754
1274
  }
1275
+ if (!await getValidAccessToken(env.VIBEGROUP_API ?? DEFAULT_API_BASE, env)) {
1276
+ console.error("Session expired — run `vibegroup login` again.");
1277
+ return 1;
1278
+ }
1279
+ const session = sessionFlag && sessionFlag.length > 0 ? sessionFlag : basename(process.cwd());
755
1280
  if (!dangerously && !channel.allowlisted()) {
756
1281
  console.log("Enabling the vibegroup channel — one-time, needs admin. Enter your password if prompted.");
757
1282
  if (!channel.enable()) {
@@ -762,6 +1287,7 @@ function claudeCommand(args, env = process.env, launcher, channel = realChannelG
762
1287
  const vg = {
763
1288
  VIBEGROUP_TEAM: team,
764
1289
  VIBEGROUP_ROOM: room && room.length > 0 ? room : "general",
1290
+ VIBEGROUP_SESSION: session,
765
1291
  VIBEGROUP_API: env.VIBEGROUP_API ?? DEFAULT_API_BASE
766
1292
  };
767
1293
  return launchClaude({ extraArgs: extra, dangerously, env: vg }, launcher);
@@ -770,6 +1296,7 @@ var DEFAULT_API_BASE = "https://api.vibegroup.sh", realChannelGate;
770
1296
  var init_claudeCmd = __esm(() => {
771
1297
  init_claudeLaunch();
772
1298
  init_auth();
1299
+ init_refresh();
773
1300
  init_channel();
774
1301
  realChannelGate = { allowlisted: channelAllowlisted, enable: enableChannelWithSudo };
775
1302
  });
@@ -826,9 +1353,10 @@ async function run(argv, env = process.env) {
826
1353
  }
827
1354
  switch (command) {
828
1355
  case "":
829
- case "help":
830
- console.log(HELP);
831
- return 0;
1356
+ case "help": {
1357
+ const { homeCommand: homeCommand2 } = await Promise.resolve().then(() => (init_home(), exports_home));
1358
+ return homeCommand2(env, (cmd) => run([cmd], env), VERSION, HELP);
1359
+ }
832
1360
  case "version":
833
1361
  console.log(VERSION);
834
1362
  return 0;
@@ -871,6 +1399,10 @@ async function run(argv, env = process.env) {
871
1399
  const { roomsCommand: roomsCommand2 } = await Promise.resolve().then(() => (init_team(), exports_team));
872
1400
  return roomsCommand2(flags, env);
873
1401
  }
1402
+ case "who": {
1403
+ const { whoCommand: whoCommand2 } = await Promise.resolve().then(() => (init_who(), exports_who));
1404
+ return whoCommand2(flags, env);
1405
+ }
874
1406
  case "invite": {
875
1407
  const { inviteCommand: inviteCommand2 } = await Promise.resolve().then(() => (init_team(), exports_team));
876
1408
  return inviteCommand2(rest, flags, env);
@@ -900,7 +1432,8 @@ Commands:
900
1432
  room create <name> --team <slug> Add a room to a team
901
1433
  invite <email> --team <s> Invite someone to a team
902
1434
  rooms --team <slug> List a team's rooms
903
- claude --team <slug> [--room <name>] [...]
1435
+ who --team <slug> [--room] Live view of who's in a room (people + their sessions)
1436
+ claude --team <slug> [--room <name>] [--session <label>] [...]
904
1437
  Launch Claude Code with the vibegroup channel
905
1438
  help Show this help
906
1439
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibegroup",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
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": {
@@ -18,11 +18,11 @@ A Claude session joins **one** `team:room`, fixed at launch by `vibegroup claude
18
18
 
19
19
  Peer agents in your team's room can ask each other what they're working on. Use the vibegroup MCP tools:
20
20
 
21
- - `vibegroup_peers` — who is in the room and what they're working on
21
+ - `vibegroup_peers` — the **people** in the room, each with their named **sessions** (one per repo/task)
22
22
  - `vibegroup_ask` — ask a peer a question (returns a `qid`; the answer arrives later as a `<channel kind="answer">` event)
23
23
  - `vibegroup_reply` — answer a peer's question (pass the `qid` from the incoming `<channel kind="question">` event)
24
24
 
25
- To ask: call `vibegroup_peers` to find a peer's `peerId`, then `vibegroup_ask`. Incoming questions arrive as channel events pushed into your session — answer read-only and call `vibegroup_reply` with the `qid`.
25
+ To ask: call `vibegroup_peers` to find the person. **If that person has more than one session** (e.g. "tell jaime …" and jaime shows `PLA-345` and `billing-fix`), don't guess — tell the user which sessions exist and ask which one, then `vibegroup_ask` that session's `peerId`. If they have exactly one session, use it directly. Incoming questions arrive as channel events pushed into your session — answer read-only and call `vibegroup_reply` with the `qid`.
26
26
 
27
27
  If `vibegroup status` shows you're not set up, run **/vibegroup:init** first.
28
28
 
@@ -15296,7 +15296,7 @@ class RelayClient {
15296
15296
  const ws = new WebSocket(this.opts.url);
15297
15297
  this.ws = ws;
15298
15298
  this.joinWaiter = { resolve, reject };
15299
- ws.addEventListener("open", () => this.send({ kind: "join", resumeToken: this.resumeToken, body: { accessToken: this.opts.accessToken, team: this.opts.team, room: this.opts.room, name: this.opts.name } }));
15299
+ ws.addEventListener("open", () => this.send({ kind: "join", resumeToken: this.resumeToken, body: { accessToken: this.opts.accessToken, team: this.opts.team, room: this.opts.room, name: this.opts.name, session: this.opts.session } }));
15300
15300
  ws.addEventListener("message", (ev) => this.dispatch(parseEnvelope(String(ev.data))));
15301
15301
  ws.addEventListener("error", () => this.failPending(new Error("websocket error")));
15302
15302
  ws.addEventListener("close", () => this.failPending(new Error("websocket closed")));
@@ -16828,11 +16828,10 @@ function groupPeers(peers, selfPeerId) {
16828
16828
  const key = p.memberId || p.peerId;
16829
16829
  let g = byMember.get(key);
16830
16830
  if (!g) {
16831
- g = { name: p.name, memberId: p.memberId ?? "", sessions: 0, state: "offline", lastSeen: 0, isYou: false, peerIds: [] };
16831
+ g = { name: p.name, memberId: p.memberId ?? "", sessions: [], state: "offline", lastSeen: 0, isYou: false };
16832
16832
  byMember.set(key, g);
16833
16833
  }
16834
- g.sessions++;
16835
- g.peerIds.push(p.peerId);
16834
+ g.sessions.push({ peerId: p.peerId, session: p.session, state: p.state, lastSeen: p.lastSeen });
16836
16835
  g.lastSeen = Math.max(g.lastSeen, p.lastSeen);
16837
16836
  if (p.state === "available")
16838
16837
  g.state = "available";
@@ -16863,7 +16862,7 @@ function createChannelTools(relay, pending, maxAnswerChars = 4000) {
16863
16862
  return [
16864
16863
  {
16865
16864
  name: "vibegroup_peers",
16866
- description: "List the people in your vibegroup room, grouped by user (each person may run several sessions). To ask someone, use any peerId from their `peerIds`.",
16865
+ description: "List the people in your vibegroup room, grouped by user. Each person may run several named sessions (one per repo/task); if you mean to reach a person with more than one session, ask the user which `session` before vibegroup_ask, and use that session's peerId.",
16867
16866
  inputSchema: { type: "object", properties: {} },
16868
16867
  handler: async () => JSON.stringify({ people: groupPeers(await relay.peers(), relay.peerId) }, null, 2)
16869
16868
  },
@@ -16912,7 +16911,7 @@ async function startChannel(opts) {
16912
16911
  }
16913
16912
 
16914
16913
  // src/config.ts
16915
- import { join } from "path";
16914
+ import { join, basename } from "path";
16916
16915
  import { existsSync, readFileSync } from "fs";
16917
16916
  var DEFAULT_RELAY_WS = "wss://relay.vibegroup.sh/ws";
16918
16917
  var DEFAULT_API_BASE = "https://api.vibegroup.sh";
@@ -16934,7 +16933,7 @@ function readAuth(env, home) {
16934
16933
  return null;
16935
16934
  }
16936
16935
  }
16937
- function resolveChannelConfig(env, home) {
16936
+ function resolveChannelConfig(env, home, cwd = process.cwd()) {
16938
16937
  const auth = readAuth(env, home);
16939
16938
  const team = env.VIBEGROUP_TEAM;
16940
16939
  if (!auth || !team)
@@ -16945,7 +16944,8 @@ function resolveChannelConfig(env, home) {
16945
16944
  accessToken: auth.accessToken,
16946
16945
  team,
16947
16946
  room: env.VIBEGROUP_ROOM ?? "general",
16948
- name: env.VIBEGROUP_NAME ?? auth.email ?? ""
16947
+ name: env.VIBEGROUP_NAME ?? auth.email ?? "",
16948
+ session: env.VIBEGROUP_SESSION || basename(cwd) || "session"
16949
16949
  };
16950
16950
  }
16951
16951
  async function fetchTeamKey(cfg, fetchImpl = fetch) {
@@ -16980,5 +16980,6 @@ await startChannel({
16980
16980
  team: cfg.team,
16981
16981
  room: cfg.room,
16982
16982
  teamKey,
16983
- name: cfg.name
16983
+ name: cfg.name,
16984
+ session: cfg.session
16984
16985
  });