vibegroup 0.1.7 → 0.1.8

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.8",
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: {
@@ -697,6 +697,280 @@ var init_team = __esm(() => {
697
697
  init_cli();
698
698
  });
699
699
 
700
+ // src/lib/presence.ts
701
+ async function fetchPresence(relayHttp, team, room, token, f = fetch) {
702
+ const url = `${relayHttp.replace(/\/+$/, "")}/presence?team=${encodeURIComponent(team)}&room=${encodeURIComponent(room)}`;
703
+ const res = await f(url, { headers: { authorization: `Bearer ${token}` } });
704
+ if (!res.ok)
705
+ throw new Error(`presence request failed (HTTP ${res.status})`);
706
+ const data = await res.json();
707
+ return data.peers ?? [];
708
+ }
709
+ function groupPeers(peers, selfMemberId) {
710
+ const byMember = new Map;
711
+ for (const p of peers) {
712
+ const key = p.memberId || p.peerId;
713
+ let g = byMember.get(key);
714
+ if (!g) {
715
+ g = { name: p.name, memberId: p.memberId ?? "", sessions: [], state: "offline", lastSeen: 0, isYou: false };
716
+ byMember.set(key, g);
717
+ }
718
+ g.sessions.push(p);
719
+ g.lastSeen = Math.max(g.lastSeen, p.lastSeen);
720
+ if (p.state === "available")
721
+ g.state = "available";
722
+ if (p.name && (!g.name || g.name === g.memberId))
723
+ g.name = p.name;
724
+ if (selfMemberId && p.memberId === selfMemberId)
725
+ g.isYou = true;
726
+ }
727
+ return [...byMember.values()].sort((a, b) => b.lastSeen - a.lastSeen);
728
+ }
729
+ var DEFAULT_RELAY_HTTP = "https://relay.vibegroup.sh";
730
+
731
+ // src/ui/Who.tsx
732
+ import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
733
+ import { Box as Box2, Text as Text2, useInput } from "ink";
734
+ import { Spinner as Spinner2 } from "@inkjs/ui";
735
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
736
+ function ago(ms) {
737
+ if (!ms)
738
+ return "—";
739
+ const s = Math.max(0, Math.round((Date.now() - ms) / 1000));
740
+ if (s < 5)
741
+ return "now";
742
+ if (s < 60)
743
+ return `${s}s ago`;
744
+ if (s < 3600)
745
+ return `${Math.round(s / 60)}m ago`;
746
+ return `${Math.round(s / 3600)}h ago`;
747
+ }
748
+ function Who({
749
+ relayHttp,
750
+ team,
751
+ room,
752
+ token,
753
+ selfMemberId,
754
+ onExit,
755
+ intervalMs = 3000
756
+ }) {
757
+ const [people, setPeople] = useState2(null);
758
+ const [selected, setSelected] = useState2(0);
759
+ const [error, setError] = useState2("");
760
+ const [updated, setUpdated] = useState2(0);
761
+ const selRef = useRef2(0);
762
+ selRef.current = selected;
763
+ const refresh = async () => {
764
+ try {
765
+ const peers = await fetchPresence(relayHttp, team, room, token, fetch);
766
+ const grouped = groupPeers(peers, selfMemberId);
767
+ setPeople(grouped);
768
+ setSelected((s) => Math.min(s, Math.max(0, grouped.length - 1)));
769
+ setUpdated(Date.now());
770
+ setError("");
771
+ } catch (e) {
772
+ setError(e?.message ?? String(e));
773
+ }
774
+ };
775
+ useEffect2(() => {
776
+ refresh();
777
+ const t = setInterval(() => void refresh(), intervalMs);
778
+ return () => clearInterval(t);
779
+ }, []);
780
+ useInput((input, key) => {
781
+ if (input === "q" || key.escape || key.ctrl && input === "c")
782
+ return onExit(0);
783
+ if (input === "r")
784
+ return void refresh();
785
+ const n = people?.length ?? 0;
786
+ if (n === 0)
787
+ return;
788
+ if (key.upArrow)
789
+ setSelected((s) => (s - 1 + n) % n);
790
+ if (key.downArrow)
791
+ setSelected((s) => (s + 1) % n);
792
+ });
793
+ const sel = people && people.length > 0 ? people[Math.min(selected, people.length - 1)] : undefined;
794
+ return /* @__PURE__ */ jsxs2(Box2, {
795
+ flexDirection: "column",
796
+ paddingX: 1,
797
+ paddingY: 1,
798
+ gap: 1,
799
+ children: [
800
+ /* @__PURE__ */ jsxs2(Box2, {
801
+ borderStyle: "round",
802
+ borderColor: color.brand,
803
+ paddingX: 1,
804
+ justifyContent: "space-between",
805
+ children: [
806
+ /* @__PURE__ */ jsxs2(Text2, {
807
+ children: [
808
+ /* @__PURE__ */ jsx2(Text2, {
809
+ color: color.brand,
810
+ bold: true,
811
+ children: "vibegroup"
812
+ }),
813
+ /* @__PURE__ */ jsx2(Text2, {
814
+ dimColor: true,
815
+ children: " · "
816
+ }),
817
+ /* @__PURE__ */ jsxs2(Text2, {
818
+ color: color.accent,
819
+ children: [
820
+ team,
821
+ "/",
822
+ room
823
+ ]
824
+ })
825
+ ]
826
+ }),
827
+ /* @__PURE__ */ jsx2(Text2, {
828
+ dimColor: true,
829
+ children: people ? `${people.length} ${people.length === 1 ? "person" : "people"}` : ""
830
+ })
831
+ ]
832
+ }),
833
+ !people && !error && /* @__PURE__ */ jsx2(Spinner2, {
834
+ label: "Loading who's here…"
835
+ }),
836
+ error && /* @__PURE__ */ jsxs2(Text2, {
837
+ color: color.err,
838
+ children: [
839
+ "Couldn't read presence: ",
840
+ error
841
+ ]
842
+ }),
843
+ people && people.length === 0 && /* @__PURE__ */ jsx2(Text2, {
844
+ dimColor: true,
845
+ children: "No one's in this room yet."
846
+ }),
847
+ people && people.length > 0 && /* @__PURE__ */ jsx2(Box2, {
848
+ flexDirection: "column",
849
+ children: people.map((p, i) => {
850
+ const active = i === selected;
851
+ const n = p.sessions.length;
852
+ return /* @__PURE__ */ jsxs2(Box2, {
853
+ children: [
854
+ /* @__PURE__ */ jsx2(Text2, {
855
+ color: active ? color.accent : undefined,
856
+ children: active ? "❯ " : " "
857
+ }),
858
+ dot(p.state),
859
+ /* @__PURE__ */ jsxs2(Text2, {
860
+ bold: active,
861
+ children: [
862
+ " ",
863
+ p.name || "unknown"
864
+ ]
865
+ }),
866
+ p.isYou && /* @__PURE__ */ jsx2(Text2, {
867
+ color: color.dim,
868
+ children: " (you)"
869
+ }),
870
+ /* @__PURE__ */ jsxs2(Text2, {
871
+ dimColor: true,
872
+ children: [
873
+ " ",
874
+ n,
875
+ " ",
876
+ n === 1 ? "session" : "sessions",
877
+ " · ",
878
+ ago(p.lastSeen)
879
+ ]
880
+ })
881
+ ]
882
+ }, p.memberId || p.name);
883
+ })
884
+ }),
885
+ sel && /* @__PURE__ */ jsxs2(Box2, {
886
+ flexDirection: "column",
887
+ borderStyle: "round",
888
+ borderColor: color.dim,
889
+ paddingX: 1,
890
+ children: [
891
+ /* @__PURE__ */ jsxs2(Text2, {
892
+ dimColor: true,
893
+ children: [
894
+ sel.name,
895
+ " · ",
896
+ sel.sessions.length,
897
+ " ",
898
+ sel.sessions.length === 1 ? "session" : "sessions"
899
+ ]
900
+ }),
901
+ sel.sessions.slice().sort((a, b) => b.lastSeen - a.lastSeen).map((s) => /* @__PURE__ */ jsxs2(Box2, {
902
+ children: [
903
+ dot(s.state),
904
+ /* @__PURE__ */ jsxs2(Text2, {
905
+ children: [
906
+ " ",
907
+ s.session || shortId(s.peerId)
908
+ ]
909
+ }),
910
+ /* @__PURE__ */ jsxs2(Text2, {
911
+ dimColor: true,
912
+ children: [
913
+ " ",
914
+ s.state,
915
+ " · ",
916
+ ago(s.lastSeen),
917
+ " · #",
918
+ shortId(s.peerId)
919
+ ]
920
+ })
921
+ ]
922
+ }, s.peerId))
923
+ ]
924
+ }),
925
+ /* @__PURE__ */ jsxs2(Text2, {
926
+ dimColor: true,
927
+ children: [
928
+ "↑↓ navigate · r refresh · q quit",
929
+ updated ? ` · updated ${ago(updated)}` : ""
930
+ ]
931
+ })
932
+ ]
933
+ });
934
+ }
935
+ var dot = (state) => state === "available" ? /* @__PURE__ */ jsx2(Text2, {
936
+ color: color.ok,
937
+ children: "●"
938
+ }) : /* @__PURE__ */ jsx2(Text2, {
939
+ color: color.dim,
940
+ children: "○"
941
+ }), shortId = (peerId) => peerId.split("#")[1]?.slice(0, 8) ?? peerId.slice(0, 8);
942
+ var init_Who = __esm(() => {
943
+ init_theme();
944
+ });
945
+
946
+ // src/commands/who.ts
947
+ var exports_who = {};
948
+ __export(exports_who, {
949
+ whoCommand: () => whoCommand
950
+ });
951
+ import { createElement as createElement2 } from "react";
952
+ async function whoCommand(flags, env = process.env) {
953
+ const auth = readAuth(env);
954
+ if (!isLoggedIn(auth)) {
955
+ console.error("Not logged in — run `vibegroup login` first.");
956
+ return 1;
957
+ }
958
+ const team = str2(flags.team);
959
+ if (!team) {
960
+ console.error("usage: vibegroup who --team <slug> [--room <name>]");
961
+ return 1;
962
+ }
963
+ const room = str2(flags.room) ?? "general";
964
+ const relayHttp = env.VIBEGROUP_RELAY_HTTP ?? DEFAULT_RELAY_HTTP;
965
+ return runInk((onExit) => createElement2(Who, { relayHttp, team, room, token: auth.accessToken, selfMemberId: auth.user?.id, onExit }));
966
+ }
967
+ var str2 = (v) => typeof v === "string" ? v : undefined;
968
+ var init_who = __esm(() => {
969
+ init_runner();
970
+ init_Who();
971
+ init_auth();
972
+ });
973
+
700
974
  // src/lib/claudeLaunch.ts
701
975
  import { spawnSync as spawnSync3 } from "node:child_process";
702
976
  function buildClaudeArgs(opts = {}) {
@@ -719,6 +993,7 @@ var exports_claudeCmd = {};
719
993
  __export(exports_claudeCmd, {
720
994
  claudeCommand: () => claudeCommand
721
995
  });
996
+ import { basename } from "node:path";
722
997
  function extractFlag(args, name) {
723
998
  const rest = [];
724
999
  let value;
@@ -747,11 +1022,13 @@ function claudeCommand(args, env = process.env, launcher, channel = realChannelG
747
1022
  }
748
1023
  const dangerously = args.includes("--dev");
749
1024
  const { value: team, rest: afterTeam } = extractFlag(args.filter((a) => a !== "--dev"), "team");
750
- const { value: room, rest: extra } = extractFlag(afterTeam, "room");
1025
+ const { value: room, rest: afterRoom } = extractFlag(afterTeam, "room");
1026
+ const { value: sessionFlag, rest: extra } = extractFlag(afterRoom, "session");
751
1027
  if (!team) {
752
1028
  console.error("No team selected — pass `--team <slug>` (the team whose room you want to join).");
753
1029
  return 1;
754
1030
  }
1031
+ const session = sessionFlag && sessionFlag.length > 0 ? sessionFlag : basename(process.cwd());
755
1032
  if (!dangerously && !channel.allowlisted()) {
756
1033
  console.log("Enabling the vibegroup channel — one-time, needs admin. Enter your password if prompted.");
757
1034
  if (!channel.enable()) {
@@ -762,6 +1039,7 @@ function claudeCommand(args, env = process.env, launcher, channel = realChannelG
762
1039
  const vg = {
763
1040
  VIBEGROUP_TEAM: team,
764
1041
  VIBEGROUP_ROOM: room && room.length > 0 ? room : "general",
1042
+ VIBEGROUP_SESSION: session,
765
1043
  VIBEGROUP_API: env.VIBEGROUP_API ?? DEFAULT_API_BASE
766
1044
  };
767
1045
  return launchClaude({ extraArgs: extra, dangerously, env: vg }, launcher);
@@ -871,6 +1149,10 @@ async function run(argv, env = process.env) {
871
1149
  const { roomsCommand: roomsCommand2 } = await Promise.resolve().then(() => (init_team(), exports_team));
872
1150
  return roomsCommand2(flags, env);
873
1151
  }
1152
+ case "who": {
1153
+ const { whoCommand: whoCommand2 } = await Promise.resolve().then(() => (init_who(), exports_who));
1154
+ return whoCommand2(flags, env);
1155
+ }
874
1156
  case "invite": {
875
1157
  const { inviteCommand: inviteCommand2 } = await Promise.resolve().then(() => (init_team(), exports_team));
876
1158
  return inviteCommand2(rest, flags, env);
@@ -900,7 +1182,8 @@ Commands:
900
1182
  room create <name> --team <slug> Add a room to a team
901
1183
  invite <email> --team <s> Invite someone to a team
902
1184
  rooms --team <slug> List a team's rooms
903
- claude --team <slug> [--room <name>] [...]
1185
+ who --team <slug> [--room] Live view of who's in a room (people + their sessions)
1186
+ claude --team <slug> [--room <name>] [--session <label>] [...]
904
1187
  Launch Claude Code with the vibegroup channel
905
1188
  help Show this help
906
1189
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibegroup",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
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
  });