@kvsm/blether 0.1.0 → 0.2.1

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/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # Blether
2
+
3
+ Agent-to-agent communication for distributed teams of human developers.
4
+
5
+ Each developer's coding agent can message the agents of their teammates, on other machines, to give a heads-up before a change, ask the owner of some code instead of guessing, or say that something they were waiting on has landed. Messages are asynchronous: they wait in a mailbox until the receiving agent reads them. They're signed by the sending device and end-to-end encrypted, so the relay that carries them never sees what was said.
6
+
7
+ > **Status:** early development. Expect breaking changes, and read [Safety](https://github.com/kvsm/blether#safety) before letting agents act on what they receive.
8
+
9
+ This package contains:
10
+
11
+ - **`blether`**: the CLI developers use to manage their identity, teams, agents and Approval Policy. Agents can't change any of those.
12
+ - **`blether-bridge`**: a local MCP server that gives an agent its Blether tools.
13
+ - **The Claude Code plugin**: the bridge, new-mail notices, and the `/blether:connect` and `/blether:setup` skills.
14
+
15
+ Teams also need a relay, which holds each agent's mailbox. It's published as the Docker image [`ghcr.io/kvsm/blether-relay`](https://github.com/kvsm/blether/blob/main/docs/self-hosting.md).
16
+
17
+ ## Quickstart
18
+
19
+ You need Node 24, and an invite link from a teammate whose team already has a relay. To start a team instead, you need a relay: see [Run a relay](https://github.com/kvsm/blether#2-run-a-relay).
20
+
21
+ ```sh
22
+ npm install -g @kvsm/blether
23
+ blether claude install # adds the plugin to Claude Code (the CLI and VS Code)
24
+ ```
25
+
26
+ Then open Claude Code in a project, with `claude` in the terminal or in VS Code, and run `/blether:setup`. Claude walks you through the rest, running the commands for you:
27
+
28
+ 1. **Your identity:** your name, as teammates will see it. Once per developer.
29
+ 2. **Your team:** joins it with your invite, or creates a new one.
30
+ 3. **This project's agent:** the named mailbox your sessions here act as. Teammates' agents message it by name, so pick one that says what it works on, such as `web-app` or `payments-api`. You can give it roles, such as `frontend` or `reviewer`, so teammates can message every agent covering an area at once.
31
+ 4. **Connecting:** the session connects to Blether, and is told whenever a message arrives.
32
+
33
+ Claude can now message your teammates' agents. In later sessions, run `/blether:connect` when you want to work with your team. Sessions you don't connect leave Blether out entirely.
34
+
35
+ By default Claude asks you before it sends any message, and before it acts on any request it receives. `blether policy` shows your Approval Policy, and `blether help` lists every command.
36
+
37
+ ### By hand
38
+
39
+ To set up without the skill, or for an agent other than Claude Code, run the steps yourself:
40
+
41
+ ```sh
42
+ blether init --name <name> # your identity, once per developer
43
+ blether join <invite> # prints "Joined <team>."
44
+ blether role list <team> # the team's roles, such as frontend or reviewer
45
+ blether role add <team> <role> # add one if what your agent does isn't listed
46
+ blether agent create <team> <agent name> --role <role> # an agent you own; --role is optional, and can repeat
47
+
48
+ cd <project> # each project an agent works in
49
+ blether use <team> <agent name> # sessions started here act as <agent name>
50
+ ```
51
+
52
+ In Claude Code, start a new session in the project and run `/blether:connect`. For other agents, see [Other agents](#other-agents) below. To start a team or run a relay, see [Getting started](https://github.com/kvsm/blether#getting-started).
53
+
54
+ ## Other agents
55
+
56
+ Any agent that speaks MCP can use the bridge. Add it to the agent's MCP config:
57
+
58
+ ```json
59
+ {
60
+ "mcpServers": {
61
+ "blether": { "command": "blether-bridge" }
62
+ }
63
+ }
64
+ ```
65
+
66
+ The bridge acts as the agent named in the project's `.blether/session.json`, which `blether use` writes. For guidance on when and how to message teammates, give the agent the skill at `$(npm root -g)/@kvsm/blether/plugin/skills/blether/SKILL.md`.
67
+
68
+ ## Updating
69
+
70
+ ```sh
71
+ npm install -g @kvsm/blether@latest
72
+ blether --version # shows the version now installed
73
+ blether claude install # if you use Claude Code: updates the plugin to match
74
+ ```
75
+
76
+ Then restart your agent sessions. Your identity, teams and projects are left as they are. See [Updating](https://github.com/kvsm/blether#updating) for the details.
77
+
78
+ ## Documentation
79
+
80
+ The [README on GitHub](https://github.com/kvsm/blether#readme) covers how Blether works, what agents can do, the safety controls (Approval Policy, sending limits, the secret check and escalations), managing devices and teams, and configuration. [Hosting a relay](https://github.com/kvsm/blether/blob/main/docs/self-hosting.md) covers running one for your team.
81
+
82
+ ## License
83
+
84
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kvsm/blether",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Agent-to-agent communication for distributed teams of human developers: the blether CLI, the bridge (an MCP server), and the Claude Code plugin.",
5
5
  "keywords": [
6
6
  "agents",
@@ -10,6 +10,8 @@
10
10
  "messaging"
11
11
  ],
12
12
  "license": "MIT",
13
+ "homepage": "https://github.com/kvsm/blether#readme",
14
+ "bugs": "https://github.com/kvsm/blether/issues",
13
15
  "repository": {
14
16
  "type": "git",
15
17
  "url": "git+https://github.com/kvsm/blether.git"
@@ -29,8 +31,8 @@
29
31
  "devDependencies": {
30
32
  "@modelcontextprotocol/sdk": "^1.31.0",
31
33
  "esbuild": "^0.28.2",
32
- "@blether/bridge": "0.0.0",
33
- "@blether/relay": "0.0.0"
34
+ "@blether/bridge": "0.2.1",
35
+ "@blether/relay": "0.2.1"
34
36
  },
35
37
  "publishConfig": {
36
38
  "access": "public"
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "blether",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "displayName": "Blether",
5
- "description": "Message your teammates' coding agents through Blether: the bridge, push delivery, and skills for when and how to reach out.",
5
+ "description": "Message your teammates' coding agents through Blether: the bridge, new-mail notices, and skills for when and how to reach out.",
6
6
  "author": {
7
7
  "name": "Kev Smith"
8
8
  },
@@ -14,14 +14,9 @@
14
14
  "command": "node",
15
15
  "args": ["${CLAUDE_PLUGIN_ROOT}/dist/bridge.js"],
16
16
  "env": {
17
- "BLETHER_PROJECT_DIR": "${CLAUDE_PROJECT_DIR}"
17
+ "BLETHER_PROJECT_DIR": "${CLAUDE_PROJECT_DIR}",
18
+ "BLETHER_CONNECT": "manual"
18
19
  }
19
20
  }
20
- },
21
- "channels": [
22
- {
23
- "server": "blether",
24
- "displayName": "Blether"
25
- }
26
- ]
21
+ }
27
22
  }
@@ -36959,6 +36959,9 @@ var DeviceGrant = external_exports.object({
36959
36959
  teams: external_exports.array(external_exports.object({ name: TeamName, id: external_exports.string(), relayUrl: external_exports.url() }))
36960
36960
  });
36961
36961
 
36962
+ // ../protocol/dist/version.js
36963
+ var BLETHER_VERSION = "0.2.1";
36964
+
36962
36965
  // ../protocol/dist/wire.js
36963
36966
  var AudienceHint = external_exports.object({
36964
36967
  audience: external_exports.union([external_exports.object({ kind: external_exports.literal("agent") }), Audience]),
@@ -37573,9 +37576,79 @@ var SentLog = class {
37573
37576
  }
37574
37577
  };
37575
37578
 
37579
+ // ../bridge/dist/inbox-file.js
37580
+ import { randomUUID as randomUUID2 } from "node:crypto";
37581
+ import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync5, renameSync, writeFileSync as writeFileSync5 } from "node:fs";
37582
+ import { join as join5 } from "node:path";
37583
+ var InboxState = external_exports.object({
37584
+ /** Changes each time a bridge starts, so a watcher can tell a restart from new mail. */
37585
+ session: external_exports.string(),
37586
+ /** Messages that have arrived since this bridge started, counting up. */
37587
+ arrivals: external_exports.number().int().min(0),
37588
+ unread: external_exports.number().int().min(0),
37589
+ /** Who the waiting messages are from, oldest first, each once. */
37590
+ from: external_exports.array(external_exports.string()),
37591
+ /** The latest arrival's sender, if any has arrived this session. */
37592
+ lastFrom: external_exports.string().optional(),
37593
+ updatedAt: external_exports.string(),
37594
+ /** Set when the bridge disconnected this session from the relay. */
37595
+ closed: external_exports.boolean().optional()
37596
+ });
37597
+ function inboxPath(home, team, agent) {
37598
+ if (!/^[A-Za-z0-9_-]+$/.test(team + agent)) {
37599
+ throw new Error("Bad team or agent name.");
37600
+ }
37601
+ return join5(home, "inbox", `${team}.${agent}.json`);
37602
+ }
37603
+ var InboxFile = class {
37604
+ path;
37605
+ now;
37606
+ /** Identifies this connection, so a watch can tell when it has ended. */
37607
+ session = randomUUID2();
37608
+ arrivals = 0;
37609
+ lastFrom;
37610
+ closed = false;
37611
+ constructor(path2, now = () => /* @__PURE__ */ new Date()) {
37612
+ this.path = path2;
37613
+ this.now = now;
37614
+ }
37615
+ /** Records an arrival (from `from`), then the mailbox as it now stands. */
37616
+ arrived(from, unread, senders) {
37617
+ this.arrivals++;
37618
+ if (from)
37619
+ this.lastFrom = from;
37620
+ this.write(unread, senders);
37621
+ }
37622
+ /** Records that this session has disconnected, so its watch stops. */
37623
+ close() {
37624
+ this.write(0, [], true);
37625
+ this.closed = true;
37626
+ }
37627
+ /** Records the mailbox as it now stands, after a read or at start-up. */
37628
+ write(unread, senders, closed = false) {
37629
+ if (this.closed)
37630
+ return;
37631
+ const state = {
37632
+ session: this.session,
37633
+ arrivals: this.arrivals,
37634
+ unread,
37635
+ from: senders,
37636
+ ...this.lastFrom ? { lastFrom: this.lastFrom } : {},
37637
+ updatedAt: this.now().toISOString(),
37638
+ ...closed ? { closed } : {}
37639
+ };
37640
+ mkdirSync5(join5(this.path, ".."), { recursive: true });
37641
+ const temp = `${this.path}.${process.pid}.tmp`;
37642
+ writeFileSync5(temp, `${JSON.stringify(state)}
37643
+ `);
37644
+ renameSync(temp, this.path);
37645
+ }
37646
+ };
37647
+
37576
37648
  // ../bridge/dist/session-file.js
37577
- import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "node:fs";
37578
- import { dirname, join as join5, resolve } from "node:path";
37649
+ import { existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "node:fs";
37650
+ import { homedir as homedir2 } from "node:os";
37651
+ import { dirname, join as join6, resolve } from "node:path";
37579
37652
  var SessionFileContents = external_exports.object({
37580
37653
  team: external_exports.string().min(1),
37581
37654
  agent: AgentName
@@ -37584,16 +37657,21 @@ var DIR = ".blether";
37584
37657
  var FILE = "session.json";
37585
37658
  var SessionFileError = class extends Error {
37586
37659
  };
37660
+ function isBletherHome(dir, bletherHome) {
37661
+ const same = (a2, b2) => process.platform === "win32" ? resolve(a2).toLowerCase() === resolve(b2).toLowerCase() : resolve(a2) === resolve(b2);
37662
+ const candidate = join6(dir, DIR);
37663
+ return same(candidate, bletherHome) || same(candidate, join6(homedir2(), DIR));
37664
+ }
37587
37665
  function findSessionFile(start, bletherHome) {
37588
37666
  let dir = resolve(start);
37589
37667
  for (; ; ) {
37590
- const candidate = join5(dir, DIR);
37591
- const path2 = join5(candidate, FILE);
37592
- if (resolve(candidate) !== resolve(bletherHome) && existsSync5(path2)) {
37668
+ const candidate = join6(dir, DIR);
37669
+ const path2 = join6(candidate, FILE);
37670
+ if (!isBletherHome(dir, bletherHome) && existsSync6(path2)) {
37593
37671
  return { path: path2, contents: readSessionFile(path2) };
37594
37672
  }
37595
37673
  const parent = dirname(dir);
37596
- if (existsSync5(join5(dir, ".git")) || parent === dir)
37674
+ if (existsSync6(join6(dir, ".git")) || parent === dir)
37597
37675
  return void 0;
37598
37676
  dir = parent;
37599
37677
  }
@@ -37601,19 +37679,19 @@ function findSessionFile(start, bletherHome) {
37601
37679
  function readSessionFile(path2) {
37602
37680
  let raw;
37603
37681
  try {
37604
- raw = JSON.parse(readFileSync5(path2, "utf8"));
37682
+ raw = JSON.parse(readFileSync6(path2, "utf8"));
37605
37683
  } catch (error62) {
37606
- throw new SessionFileError(`${path2} isn't valid JSON (${error62.message}). Write it again with \`blether use <team> <agent>\`.`);
37684
+ throw new SessionFileError(`${path2} isn't valid JSON (${error62.message}). Write it again with \`blether use <team> <agent name>\`.`);
37607
37685
  }
37608
37686
  const parsed = SessionFileContents.safeParse(raw);
37609
37687
  if (!parsed.success) {
37610
- throw new SessionFileError(`${path2} doesn't name a team and agent. Write it again with \`blether use <team> <agent>\`.`);
37688
+ throw new SessionFileError(`${path2} doesn't name a team and agent. Write it again with \`blether use <team> <agent name>\`.`);
37611
37689
  }
37612
37690
  return parsed.data;
37613
37691
  }
37614
37692
 
37615
37693
  // ../bridge/dist/relay-connection.js
37616
- import { randomUUID as randomUUID2 } from "node:crypto";
37694
+ import { randomUUID as randomUUID3 } from "node:crypto";
37617
37695
 
37618
37696
  // ../../node_modules/.pnpm/ws@8.22.0/node_modules/ws/wrapper.mjs
37619
37697
  var import_stream = __toESM(require_stream(), 1);
@@ -37676,6 +37754,7 @@ var RelayConnection = class _RelayConnection {
37676
37754
  */
37677
37755
  identity;
37678
37756
  arrivalListeners = /* @__PURE__ */ new Set();
37757
+ readListeners = /* @__PURE__ */ new Set();
37679
37758
  constructor(socket, credentials, scope, witness, readMessages) {
37680
37759
  this.socket = socket;
37681
37760
  this.credentials = credentials;
@@ -37774,7 +37853,7 @@ var RelayConnection = class _RelayConnection {
37774
37853
  if (devices.length === 0) {
37775
37854
  throw new RelayError("untrusted-reply", `Couldn't find the devices of ${to}'s developer to encrypt for.`);
37776
37855
  }
37777
- const id = randomUUID2();
37856
+ const id = randomUUID3();
37778
37857
  const envelope = sealMessage({
37779
37858
  id,
37780
37859
  team: scope.team,
@@ -37825,7 +37904,7 @@ var RelayConnection = class _RelayConnection {
37825
37904
  }
37826
37905
  /** The `limit` messages this agent sent most recently, newest first. */
37827
37906
  listSent(limit = 20) {
37828
- const requestId = randomUUID2();
37907
+ const requestId = randomUUID3();
37829
37908
  return this.request(this.listings, requestId, {
37830
37909
  type: "list-sent",
37831
37910
  requestId,
@@ -37841,6 +37920,11 @@ var RelayConnection = class _RelayConnection {
37841
37920
  const from = this.unread.map((item) => item.kind === "lost" ? "lost-message notices" : item.from);
37842
37921
  return [...new Set(from)];
37843
37922
  }
37923
+ /** Calls `listener` after each mailbox read. Returns a function that unsubscribes. */
37924
+ onRead(listener) {
37925
+ this.readListeners.add(listener);
37926
+ return () => this.readListeners.delete(listener);
37927
+ }
37844
37928
  /** Calls `listener` whenever a new message arrives. Returns a function that unsubscribes. */
37845
37929
  onArrival(listener) {
37846
37930
  this.arrivalListeners.add(listener);
@@ -37866,6 +37950,8 @@ var RelayConnection = class _RelayConnection {
37866
37950
  if (read.length > 0 && this.socket.readyState === import_websocket.default.OPEN) {
37867
37951
  this.write({ type: "read", ids: read });
37868
37952
  }
37953
+ for (const listener of this.readListeners)
37954
+ listener();
37869
37955
  return items;
37870
37956
  }
37871
37957
  /** A message read earlier in this session, by id. */
@@ -37887,7 +37973,7 @@ var RelayConnection = class _RelayConnection {
37887
37973
  }
37888
37974
  /** Starts a team whose log is `log`, returning it as the relay stored it. */
37889
37975
  async createTeam(log) {
37890
- const requestId = randomUUID2();
37976
+ const requestId = randomUUID3();
37891
37977
  const reply = await this.request(this.teamRequests, requestId, {
37892
37978
  type: "create-team",
37893
37979
  requestId,
@@ -37897,7 +37983,7 @@ var RelayConnection = class _RelayConnection {
37897
37983
  }
37898
37984
  /** The names of team `id`'s agents that have a session connected. */
37899
37985
  getPresence(id) {
37900
- const requestId = randomUUID2();
37986
+ const requestId = randomUUID3();
37901
37987
  return this.request(this.presenceRequests, requestId, {
37902
37988
  type: "get-presence",
37903
37989
  requestId,
@@ -37921,7 +38007,7 @@ var RelayConnection = class _RelayConnection {
37921
38007
  }
37922
38008
  /** Fetches team `id`'s log and verifies it. */
37923
38009
  async getTeam(id) {
37924
- const requestId = randomUUID2();
38010
+ const requestId = randomUUID3();
37925
38011
  const reply = await this.request(this.teamRequests, requestId, {
37926
38012
  type: "get-team",
37927
38013
  requestId,
@@ -37931,7 +38017,7 @@ var RelayConnection = class _RelayConnection {
37931
38017
  }
37932
38018
  /** Appends `entry` to team `id`'s log, returning the verified result. */
37933
38019
  async appendTeam(id, entry) {
37934
- const requestId = randomUUID2();
38020
+ const requestId = randomUUID3();
37935
38021
  const reply = await this.request(this.teamRequests, requestId, {
37936
38022
  type: "append-team",
37937
38023
  requestId,
@@ -38197,7 +38283,7 @@ function take(map2, key) {
38197
38283
  }
38198
38284
 
38199
38285
  // ../bridge/dist/server.js
38200
- import { randomUUID as randomUUID3 } from "node:crypto";
38286
+ import { randomUUID as randomUUID4 } from "node:crypto";
38201
38287
 
38202
38288
  // ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v3/helpers/util.js
38203
38289
  var util;
@@ -51027,7 +51113,7 @@ var CLAUDE_CHANNEL = "claude/channel";
51027
51113
  var CLAUDE_CHANNEL_NOTIFICATION = "notifications/claude/channel";
51028
51114
  function createSetupProblemServer(problem, { takeOver } = {}) {
51029
51115
  const explanation = `Blether isn't working in this session: ${problem}`;
51030
- const server2 = new McpServer({ name: "blether", version: "0.0.0" }, {
51116
+ const server2 = new McpServer({ name: "blether", version: BLETHER_VERSION }, {
51031
51117
  instructions: `${explanation} Blether's messaging tools are unavailable until this is fixed. If your developer asks about Blether, or you need to message another agent, tell them this and suggest the fix.`,
51032
51118
  // Declared up front so a session that takes its agent over can still
51033
51119
  // receive channel notices.
@@ -51057,9 +51143,59 @@ function createSetupProblemServer(problem, { takeOver } = {}) {
51057
51143
  }
51058
51144
  return server2;
51059
51145
  }
51146
+ function createDormantServer(connect2) {
51147
+ const server2 = new McpServer({ name: "blether", version: BLETHER_VERSION }, { capabilities: { experimental: { [CLAUDE_CHANNEL]: {} } } });
51148
+ const offerConnect = () => {
51149
+ const connectTool = server2.registerTool("connect", {
51150
+ title: "Connect to Blether",
51151
+ description: "Connects this session to Blether, so you can message the agents of other developers on your team. Call it ONLY when your developer runs /blether:connect or /blether:setup. Never call it otherwise, even if they ask you to connect: tell them to run /blether:connect instead."
51152
+ }, async () => {
51153
+ let connected;
51154
+ try {
51155
+ connected = await connect2(server2);
51156
+ } catch (error62) {
51157
+ return {
51158
+ ...text(`Couldn't connect to Blether: ${error62.message}`),
51159
+ isError: true
51160
+ };
51161
+ }
51162
+ connectTool.remove();
51163
+ const disconnectTool = server2.registerTool("disconnect", {
51164
+ title: "Disconnect from Blether",
51165
+ description: "Disconnects this session from Blether. Call it ONLY when your developer runs /blether:disconnect. Never call it otherwise, even if they ask you to disconnect: tell them to run /blether:disconnect instead."
51166
+ }, async () => {
51167
+ const result = await connected.disconnect();
51168
+ disconnectTool.remove();
51169
+ offerConnect();
51170
+ return text(result);
51171
+ });
51172
+ return text(connected.result);
51173
+ });
51174
+ };
51175
+ offerConnect();
51176
+ return server2;
51177
+ }
51178
+ function removableTools(server2, install) {
51179
+ const added = [];
51180
+ const register = server2.registerTool.bind(server2);
51181
+ server2.registerTool = ((...args) => {
51182
+ const tool = register(...args);
51183
+ added.push(tool);
51184
+ return tool;
51185
+ });
51186
+ try {
51187
+ install();
51188
+ } finally {
51189
+ delete server2.registerTool;
51190
+ }
51191
+ return () => {
51192
+ for (const tool of added.splice(0))
51193
+ tool.remove();
51194
+ };
51195
+ }
51060
51196
  function createBridgeServer(relay, { policy = STRICTEST_POLICY, escalations, now = () => /* @__PURE__ */ new Date(), scanSecrets = scanForSecrets, sentLog, server: existing } = {}) {
51061
51197
  const pendingAtStart = escalations?.pending().length ?? 0;
51062
- const server2 = existing ?? new McpServer({ name: "blether", version: "0.0.0" }, {
51198
+ const server2 = existing ?? new McpServer({ name: "blether", version: BLETHER_VERSION }, {
51063
51199
  instructions: [
51064
51200
  INSTRUCTIONS,
51065
51201
  ...escalations ? [
@@ -51144,7 +51280,7 @@ function createBridgeServer(relay, { policy = STRICTEST_POLICY, escalations, now
51144
51280
  if (approval !== "approved") {
51145
51281
  return respond(`Not sent: ${approval}`, true);
51146
51282
  }
51147
- const fanout = randomUUID3();
51283
+ const fanout = randomUUID4();
51148
51284
  const sendOne = async (recipient) => {
51149
51285
  const receipt = await relay.send(recipient, body, {
51150
51286
  audience,
@@ -51459,13 +51595,15 @@ var SetupProblem = class extends Error {
51459
51595
  };
51460
51596
  var REFUSAL_FIXES = {
51461
51597
  "agent-in-use": (agent) => `Only one session can act as ${agent} at a time. If the other one is still in use, close it (it may be in another terminal, or on another of your devices) or choose a different agent for this project with \`blether use\`. If this session should be ${agent} instead, use take_over_agent.`,
51462
- "agent-owned-by-another": (agent, team) => `${agent} is another developer's agent. Use one of yours (\`blether agent list ${team}\`) or create one with \`blether agent create ${team} <name>\`, then restart this session.`,
51598
+ "agent-owned-by-another": (agent, team) => `${agent} is another developer's agent. Use one of yours (\`blether agent list ${team}\`) or create one with \`blether agent create ${team} <agent name>\`, then restart this session.`,
51463
51599
  "not-a-member": () => "Ask a teammate for an invite and run `blether join <invite>`, then restart this session.",
51464
51600
  "unknown-team": (_agent, team) => `The relay doesn't know ${team}; it may have been reset. Check the relay is the right one, or create the team again with \`blether team create\`.`,
51465
51601
  "authentication-failed": () => "This device's identity didn't verify. Check `blether whoami`; if it's damaged, set this device up again.",
51466
51602
  "identity-conflict": () => "The relay holds a different history for your identity than this device does. Don't add devices from two places at once; ask for help before going further."
51467
51603
  };
51468
51604
  async function startBridge(env = process.env, log = (line) => console.error(line)) {
51605
+ if (env.BLETHER_CONNECT === "manual")
51606
+ return startDormant(env, log);
51469
51607
  try {
51470
51608
  const { server: server2, connection } = await connect(env, log);
51471
51609
  return {
@@ -51500,7 +51638,65 @@ async function startBridge(env = process.env, log = (line) => console.error(line
51500
51638
  };
51501
51639
  }
51502
51640
  }
51503
- async function connect(env, log) {
51641
+ function startDormant(env, log) {
51642
+ let connected;
51643
+ const disconnect = async () => {
51644
+ const opened = connected;
51645
+ connected = void 0;
51646
+ if (!opened)
51647
+ return;
51648
+ opened.removeTools();
51649
+ try {
51650
+ opened.inbox.close();
51651
+ } catch {
51652
+ }
51653
+ await opened.connection.close();
51654
+ };
51655
+ const server2 = createDormantServer(async (server3) => {
51656
+ let opened;
51657
+ try {
51658
+ opened = await connect(env, log, server3);
51659
+ } catch (error62) {
51660
+ if (error62 instanceof SetupProblem) {
51661
+ throw new Error(error62.message, { cause: error62 });
51662
+ }
51663
+ throw error62;
51664
+ }
51665
+ connected = opened;
51666
+ log(`blether bridge connected as ${opened.agent}`);
51667
+ return {
51668
+ result: connectedGuidance(opened),
51669
+ disconnect: async () => {
51670
+ await disconnect();
51671
+ log("blether bridge disconnected");
51672
+ return "Disconnected from Blether. Your watch stops by itself, and Blether's tools are gone until your developer runs /blether:connect again.";
51673
+ }
51674
+ };
51675
+ });
51676
+ return {
51677
+ server: server2,
51678
+ close: async () => {
51679
+ await server2.close();
51680
+ await disconnect();
51681
+ }
51682
+ };
51683
+ }
51684
+ function connectedGuidance({ agent, team, inbox, pendingEscalations }) {
51685
+ const path2 = inbox.path.split("\\").join("/");
51686
+ const watch = `blether watch --inbox "${path2}" --session ${inbox.session}`;
51687
+ return [
51688
+ `Connected to Blether: you are ${agent} in team ${team.name}.`,
51689
+ INSTRUCTIONS,
51690
+ ...pendingEscalations > 0 ? [
51691
+ `${pendingEscalations} escalation(s) from earlier sessions are waiting for your developer: call list_escalations and raise them with your developer.`
51692
+ ] : [],
51693
+ "",
51694
+ "Now:",
51695
+ "1. Call read_mailbox.",
51696
+ `2. Start a watch, so you hear about new mail between prompts: use the Monitor tool (load it with ToolSearch first if it's deferred) with the command: ${watch} \u2014 the description "Blether mail for ${agent}", and timeout_ms 1800000. Each line it prints means new mail: call read_mailbox. When the watch expires, start it again. If it stops by itself (this session disconnected, or another session took the agent over), don't.`
51697
+ ].join("\n");
51698
+ }
51699
+ async function connect(env, log, into) {
51504
51700
  const home = env.BLETHER_HOME ?? defaultBletherHome();
51505
51701
  let session;
51506
51702
  try {
@@ -51512,11 +51708,11 @@ async function connect(env, log) {
51512
51708
  }
51513
51709
  const agentName = env.BLETHER_AGENT ?? session?.contents.agent;
51514
51710
  if (agentName === void 0) {
51515
- throw new SetupProblem("No agent is chosen for this project. Run `blether use <team> <agent>` in the project (`blether agent list <team>` shows the agents), then restart this session.");
51711
+ throw new SetupProblem("No agent is chosen for this project. Run `blether use <team> <agent name>` in the project (`blether agent list <team>` shows the agents), then restart this session.");
51516
51712
  }
51517
51713
  const agent = AgentName.safeParse(agentName);
51518
51714
  if (!agent.success) {
51519
- throw new SetupProblem(`BLETHER_AGENT is set to "${agentName}", which isn't an agent name (lowercase letters, digits and hyphens). Fix it in this agent's MCP config, or remove it and run \`blether use <team> <agent>\` in the project.`);
51715
+ throw new SetupProblem(`BLETHER_AGENT is set to "${agentName}", which isn't an agent name (lowercase letters, digits and hyphens). Fix it in this agent's MCP config, or remove it and run \`blether use <team> <agent name>\` in the project.`);
51520
51716
  }
51521
51717
  if (session)
51522
51718
  log(`blether bridge using ${session.path}`);
@@ -51532,7 +51728,7 @@ async function connect(env, log) {
51532
51728
  }
51533
51729
  const teamName = env.BLETHER_TEAM ?? session?.contents.team;
51534
51730
  if (!teamName) {
51535
- throw new SetupProblem("No team is chosen for this project. Run `blether use <team> <agent>` in the project (`blether team list` shows your teams), then restart this session.");
51731
+ throw new SetupProblem("No team is chosen for this project. Run `blether use <team> <agent name>` in the project (`blether team list` shows your teams), then restart this session.");
51536
51732
  }
51537
51733
  let team;
51538
51734
  try {
@@ -51547,13 +51743,13 @@ async function connect(env, log) {
51547
51743
  Messages are end-to-end encrypted; the relay sees only who messaged whom, and when.`);
51548
51744
  const found = { store, credentials, team, agent: agent.data, log };
51549
51745
  try {
51550
- return await open2(found, false);
51746
+ return await open2(found, into !== void 0, into);
51551
51747
  } catch (error62) {
51552
51748
  if (!(error62 instanceof RelayError)) {
51553
51749
  throw new SetupProblem(`Couldn't connect to the relay at ${team.relayUrl}: ${error62.message}. Is it running? Start it, then restart this session.`);
51554
51750
  }
51555
51751
  const fix = REFUSAL_FIXES[error62.code]?.(agent.data, team.name);
51556
- throw new SetupProblem(`The relay refused this session (${error62.code}): ${error62.message}${fix ? ` ${fix}` : ""}`, error62.code === "agent-in-use" ? async (server2) => (await open2(found, true, server2)).connection : void 0);
51752
+ throw new SetupProblem(`The relay refused this session (${error62.code}): ${error62.message}${fix ? ` ${fix}` : ""}`, error62.code === "agent-in-use" && !into ? async (server2) => (await open2(found, true, server2)).connection : void 0);
51557
51753
  }
51558
51754
  }
51559
51755
  async function open2({ store, credentials, team, agent, log }, takeover, server2) {
@@ -51567,13 +51763,43 @@ async function open2({ store, credentials, team, agent, log }, takeover, server2
51567
51763
  if (relay.identity && relay.identity.length > credentials.identity.length) {
51568
51764
  store.saveIdentity(relay.identity);
51569
51765
  }
51570
- const bridge = createBridgeServer(relay, {
51766
+ const inbox = new InboxFile(inboxPath(store.home, team.id, agent));
51767
+ keepInboxFile(relay, inbox);
51768
+ const escalations = new EscalationStore(store.home, team.id, agent);
51769
+ const options = {
51571
51770
  policy: new PolicyStore(store.home).load(),
51572
- escalations: new EscalationStore(store.home, team.id, agent),
51573
- sentLog: new SentLog(store.home, team.id, agent),
51574
- ...server2 ? { server: server2 } : {}
51575
- });
51576
- return { server: bridge, connection: relay };
51771
+ escalations,
51772
+ sentLog: new SentLog(store.home, team.id, agent)
51773
+ };
51774
+ let bridge;
51775
+ let removeTools = () => {
51776
+ };
51777
+ if (server2) {
51778
+ bridge = server2;
51779
+ removeTools = removableTools(server2, () => createBridgeServer(relay, { ...options, server: server2 }));
51780
+ } else {
51781
+ bridge = createBridgeServer(relay, options);
51782
+ }
51783
+ return {
51784
+ server: bridge,
51785
+ connection: relay,
51786
+ agent,
51787
+ team,
51788
+ inbox,
51789
+ removeTools,
51790
+ pendingEscalations: escalations.pending().length
51791
+ };
51792
+ }
51793
+ function keepInboxFile(relay, inbox) {
51794
+ const attempt = (write) => {
51795
+ try {
51796
+ write();
51797
+ } catch {
51798
+ }
51799
+ };
51800
+ relay.onArrival((item) => attempt(() => inbox.arrived(item.kind === "lost" ? void 0 : item.from, relay.unreadCount, relay.unreadFrom())));
51801
+ relay.onRead(() => attempt(() => inbox.write(relay.unreadCount, relay.unreadFrom())));
51802
+ void relay.settled().then(() => attempt(() => inbox.write(relay.unreadCount, relay.unreadFrom())));
51577
51803
  }
51578
51804
 
51579
51805
  // ../bridge/dist/bin.js
@@ -27617,6 +27617,9 @@ function parseDeviceGrant(text) {
27617
27617
  }
27618
27618
  }
27619
27619
 
27620
+ // ../protocol/dist/version.js
27621
+ var BLETHER_VERSION = "0.2.1";
27622
+
27620
27623
  // ../protocol/dist/wire.js
27621
27624
  var AudienceHint = external_exports.object({
27622
27625
  audience: external_exports.union([external_exports.object({ kind: external_exports.literal("agent") }), Audience]),
@@ -28007,6 +28010,7 @@ var RelayConnection = class _RelayConnection {
28007
28010
  */
28008
28011
  identity;
28009
28012
  arrivalListeners = /* @__PURE__ */ new Set();
28013
+ readListeners = /* @__PURE__ */ new Set();
28010
28014
  constructor(socket, credentials, scope, witness, readMessages) {
28011
28015
  this.socket = socket;
28012
28016
  this.credentials = credentials;
@@ -28172,6 +28176,11 @@ var RelayConnection = class _RelayConnection {
28172
28176
  const from = this.unread.map((item) => item.kind === "lost" ? "lost-message notices" : item.from);
28173
28177
  return [...new Set(from)];
28174
28178
  }
28179
+ /** Calls `listener` after each mailbox read. Returns a function that unsubscribes. */
28180
+ onRead(listener) {
28181
+ this.readListeners.add(listener);
28182
+ return () => this.readListeners.delete(listener);
28183
+ }
28175
28184
  /** Calls `listener` whenever a new message arrives. Returns a function that unsubscribes. */
28176
28185
  onArrival(listener) {
28177
28186
  this.arrivalListeners.add(listener);
@@ -28197,6 +28206,8 @@ var RelayConnection = class _RelayConnection {
28197
28206
  if (read.length > 0 && this.socket.readyState === import_websocket.default.OPEN) {
28198
28207
  this.write({ type: "read", ids: read });
28199
28208
  }
28209
+ for (const listener of this.readListeners)
28210
+ listener();
28200
28211
  return items;
28201
28212
  }
28202
28213
  /** A message read earlier in this session, by id. */
@@ -28695,6 +28706,7 @@ var PolicyStore = class {
28695
28706
 
28696
28707
  // ../bridge/dist/session-file.js
28697
28708
  import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
28709
+ import { homedir as homedir2 } from "node:os";
28698
28710
  import { dirname, join as join4, resolve } from "node:path";
28699
28711
  var SessionFileContents = external_exports.object({
28700
28712
  team: external_exports.string().min(1),
@@ -28703,7 +28715,17 @@ var SessionFileContents = external_exports.object({
28703
28715
  var DIR = ".blether";
28704
28716
  var FILE = "session.json";
28705
28717
  var GITIGNORE = "# Written by `blether use`: this developer's own agent for the project.\n*\n";
28706
- function writeSessionFile(projectDir, contents) {
28718
+ var SessionFileError = class extends Error {
28719
+ };
28720
+ function isBletherHome(dir, bletherHome) {
28721
+ const same = (a2, b2) => process.platform === "win32" ? resolve(a2).toLowerCase() === resolve(b2).toLowerCase() : resolve(a2) === resolve(b2);
28722
+ const candidate = join4(dir, DIR);
28723
+ return same(candidate, bletherHome) || same(candidate, join4(homedir2(), DIR));
28724
+ }
28725
+ function writeSessionFile(projectDir, contents, bletherHome) {
28726
+ if (isBletherHome(projectDir, bletherHome)) {
28727
+ throw new SessionFileError(`${resolve(projectDir)} is your home directory, where Blether keeps your identity, not a project. Run "blether use" from the project's root, or give it --dir <project>.`);
28728
+ }
28707
28729
  const dir = join4(projectDir, DIR);
28708
28730
  mkdirSync4(dir, { recursive: true });
28709
28731
  const ignore = join4(dir, ".gitignore");
@@ -28715,9 +28737,71 @@ function writeSessionFile(projectDir, contents) {
28715
28737
  return path;
28716
28738
  }
28717
28739
 
28740
+ // ../bridge/dist/inbox-file.js
28741
+ import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync5, renameSync, writeFileSync as writeFileSync5 } from "node:fs";
28742
+ var InboxState = external_exports.object({
28743
+ /** Changes each time a bridge starts, so a watcher can tell a restart from new mail. */
28744
+ session: external_exports.string(),
28745
+ /** Messages that have arrived since this bridge started, counting up. */
28746
+ arrivals: external_exports.number().int().min(0),
28747
+ unread: external_exports.number().int().min(0),
28748
+ /** Who the waiting messages are from, oldest first, each once. */
28749
+ from: external_exports.array(external_exports.string()),
28750
+ /** The latest arrival's sender, if any has arrived this session. */
28751
+ lastFrom: external_exports.string().optional(),
28752
+ updatedAt: external_exports.string(),
28753
+ /** Set when the bridge disconnected this session from the relay. */
28754
+ closed: external_exports.boolean().optional()
28755
+ });
28756
+ function readInboxState(path) {
28757
+ if (!existsSync5(path))
28758
+ return void 0;
28759
+ try {
28760
+ return InboxState.parse(JSON.parse(readFileSync5(path, "utf8")));
28761
+ } catch {
28762
+ return void 0;
28763
+ }
28764
+ }
28765
+ function unreadSummary(state) {
28766
+ if (!state || state.unread === 0)
28767
+ return void 0;
28768
+ return `\u{1F4EC} ${state.unread} unread Blether message${state.unread === 1 ? "" : "s"} (from ${state.from.join(", ")}). Call read_mailbox to read them.`;
28769
+ }
28770
+
28771
+ // ../bridge/dist/watch.js
28772
+ function watchInbox(path, session, emit, end, { intervalMs = 1e3 } = {}) {
28773
+ let arrivals;
28774
+ let timer;
28775
+ let stopped = false;
28776
+ const stop = () => {
28777
+ stopped = true;
28778
+ clearInterval(timer);
28779
+ };
28780
+ const check2 = () => {
28781
+ const state = readInboxState(path);
28782
+ if (!state)
28783
+ return;
28784
+ if (state.session !== session || state.closed) {
28785
+ stop();
28786
+ end(state.session === session ? "disconnected" : "taken-over");
28787
+ return;
28788
+ }
28789
+ const summary = unreadSummary(state);
28790
+ const arrived = arrivals !== void 0 && state.arrivals > arrivals;
28791
+ if (summary && (arrivals === void 0 || arrived)) {
28792
+ emit(arrived && state.lastFrom ? `New Blether message from ${state.lastFrom}. ${summary}` : summary);
28793
+ }
28794
+ arrivals = state.arrivals;
28795
+ };
28796
+ check2();
28797
+ if (!stopped)
28798
+ timer = setInterval(check2, intervalMs);
28799
+ return stop;
28800
+ }
28801
+
28718
28802
  // ../bridge/dist/claude-install.js
28719
28803
  import { spawn } from "node:child_process";
28720
- import { existsSync as existsSync5 } from "node:fs";
28804
+ import { existsSync as existsSync6 } from "node:fs";
28721
28805
  import { dirname as dirname2, join as join5, resolve as resolve2 } from "node:path";
28722
28806
  import { fileURLToPath } from "node:url";
28723
28807
  var PLUGIN_ID = "blether@blether";
@@ -28737,7 +28821,7 @@ var runClaude = (args) => new Promise((resolvePromise, reject) => {
28737
28821
  function packageRoot(from = dirname2(fileURLToPath(import.meta.url))) {
28738
28822
  let dir = resolve2(from);
28739
28823
  for (; ; ) {
28740
- if (existsSync5(join5(dir, ".claude-plugin", "marketplace.json")))
28824
+ if (existsSync6(join5(dir, ".claude-plugin", "marketplace.json")))
28741
28825
  return dir;
28742
28826
  const parent = dirname2(dir);
28743
28827
  if (parent === dir)
@@ -28793,14 +28877,20 @@ Commands:
28793
28877
  device list List your identity's devices
28794
28878
  device revoke <fingerprint> Revoke a lost or stolen device, from another of yours
28795
28879
  role add <team> <role> Add a role to the team's agreed list
28796
- agent create <team> <name> [--role <r>]...
28880
+ role list <team> List the team's roles
28881
+ agent create <team> <agent name> [--role <r>]...
28797
28882
  Create an agent you own, with roles from the team's list
28798
- agent roles <team> <name> [--role <r>]...
28883
+ agent roles <team> <agent name> [--role <r>]...
28799
28884
  Replace the roles of one of your agents
28800
28885
  agent list <team> Show the team's agents and who is online
28801
- agent delete <team> <name> Delete one of your agents (or any, as Team Admin)
28802
- use <team> <agent> [--dir <path>] Make sessions started in this project (or <path>) act as <agent>
28886
+ agent delete <team> <agent name>
28887
+ Delete one of your agents (or any, as Team Admin)
28888
+ use <team> <agent name> [--dir <path>]
28889
+ Make sessions started in this project (or <path>) act as the agent
28803
28890
  claude install Install (or update) the Blether plugin in Claude Code
28891
+ watch --inbox <path> --session <id>
28892
+ Print a line whenever a connected session's agent gets mail
28893
+ (the bridge gives its agent this command when it connects)
28804
28894
  escalations List messages your agents are holding for your decision
28805
28895
  status One line for your Claude Code status line: escalations waiting
28806
28896
  policy Show your Approval Policy on this device
@@ -28810,6 +28900,7 @@ Commands:
28810
28900
  outgoing: ask | ask-others | free
28811
28901
  incoming: ask | ask-impactful | free
28812
28902
  limits: most messages an agent sends in the window
28903
+ --version Show which version of Blether is installed
28813
28904
 
28814
28905
  Set BLETHER_HOME to keep Blether's files somewhere other than ~/.blether.`;
28815
28906
  var CliError = class extends Error {
@@ -28881,6 +28972,8 @@ ${USAGE}`);
28881
28972
  const [sub, ...args] = rest;
28882
28973
  if (sub === "add")
28883
28974
  return roleAdd(args, ctx);
28975
+ if (sub === "list")
28976
+ return roleList(args, ctx);
28884
28977
  throw new CliError(`Unknown role command: ${sub ?? "(none)"}
28885
28978
 
28886
28979
  ${USAGE}`);
@@ -28917,6 +29010,8 @@ ${USAGE}`);
28917
29010
  }
28918
29011
  case "use":
28919
29012
  return use(rest, ctx);
29013
+ case "watch":
29014
+ return watch(rest, ctx);
28920
29015
  case "claude": {
28921
29016
  const [sub] = rest;
28922
29017
  if (sub === "install")
@@ -28937,6 +29032,11 @@ ${USAGE}`);
28937
29032
  case "-h":
28938
29033
  ctx.io.out(USAGE);
28939
29034
  return command === void 0 ? 1 : 0;
29035
+ case "version":
29036
+ case "--version":
29037
+ case "-v":
29038
+ ctx.io.out(BLETHER_VERSION);
29039
+ return 0;
28940
29040
  default:
28941
29041
  throw new CliError(`Unknown command: ${command}
28942
29042
 
@@ -28950,7 +29050,7 @@ function init(args, { store, io }) {
28950
29050
  });
28951
29051
  const name = DeveloperName.safeParse(values.name);
28952
29052
  if (!name.success) {
28953
- throw new CliError('Give your name, as teammates will see it: blether init --name "Kev"');
29053
+ throw new CliError("Give your name, as teammates will see it: blether init --name <name>");
28954
29054
  }
28955
29055
  if (store.exists()) {
28956
29056
  throw new CliError(`There is already a Blether identity in ${store.home}. Run \`blether whoami\` to see it.`);
@@ -29329,6 +29429,19 @@ async function roleAdd(args, ctx) {
29329
29429
  ctx.io.out(`Added the ${role} role to ${record2.name}.`);
29330
29430
  return 0;
29331
29431
  }
29432
+ async function roleList(args, ctx) {
29433
+ const record2 = loadTeam(ctx.teams, args[0]);
29434
+ const credentials = loadCredentials(ctx.store);
29435
+ const { team } = await withRelay(ctx, record2.relayUrl, credentials, (relay) => relay.getTeam(record2.id));
29436
+ if (team.roles.length === 0) {
29437
+ ctx.io.out(`${record2.name} has no roles yet. Add one with: blether role add ${record2.name} <role>`);
29438
+ return 0;
29439
+ }
29440
+ ctx.io.out(`Roles in ${record2.name}:`);
29441
+ for (const role of team.roles)
29442
+ ctx.io.out(` ${role}`);
29443
+ return 0;
29444
+ }
29332
29445
  async function agentCreate(args, ctx) {
29333
29446
  const { values, positionals } = parseArgs({
29334
29447
  args,
@@ -29336,7 +29449,7 @@ async function agentCreate(args, ctx) {
29336
29449
  options: { role: { type: "string", multiple: true } }
29337
29450
  });
29338
29451
  const record2 = loadTeam(ctx.teams, positionals[0]);
29339
- const name = parseName(AgentName, positionals[1], "Usage: blether agent create <team> <name> [--role <role>]...");
29452
+ const name = parseName(AgentName, positionals[1], "Usage: blether agent create <team> <agent name> [--role <role>]...");
29340
29453
  const roles = values.role ?? [];
29341
29454
  const credentials = loadCredentials(ctx.store);
29342
29455
  await withRelay(ctx, record2.relayUrl, credentials, async (relay) => {
@@ -29358,7 +29471,7 @@ async function agentRoles(args, ctx) {
29358
29471
  options: { role: { type: "string", multiple: true } }
29359
29472
  });
29360
29473
  const record2 = loadTeam(ctx.teams, positionals[0]);
29361
- const name = parseName(AgentName, positionals[1], "Usage: blether agent roles <team> <name> [--role <role>]...");
29474
+ const name = parseName(AgentName, positionals[1], "Usage: blether agent roles <team> <agent name> [--role <role>]...");
29362
29475
  const roles = values.role ?? [];
29363
29476
  const credentials = loadCredentials(ctx.store);
29364
29477
  const me2 = verifyIdentityLog(credentials.identity).id;
@@ -29411,7 +29524,7 @@ async function teamRemove(args, ctx) {
29411
29524
  }
29412
29525
  async function agentDelete(args, ctx) {
29413
29526
  const record2 = loadTeam(ctx.teams, args[0]);
29414
- const name = parseName(AgentName, args[1], "Usage: blether agent delete <team> <name>");
29527
+ const name = parseName(AgentName, args[1], "Usage: blether agent delete <team> <agent name>");
29415
29528
  const credentials = loadCredentials(ctx.store);
29416
29529
  const me2 = verifyIdentityLog(credentials.identity).id;
29417
29530
  await withRelay(ctx, record2.relayUrl, credentials, async (relay) => {
@@ -29427,6 +29540,27 @@ async function agentDelete(args, ctx) {
29427
29540
  ctx.io.out(`Deleted agent ${name} from ${record2.name}. Unread messages to it are lost, and their senders will be told. The name can be used again.`);
29428
29541
  return 0;
29429
29542
  }
29543
+ async function watch(args, ctx) {
29544
+ const { values } = parseArgs({
29545
+ args,
29546
+ options: { inbox: { type: "string" }, session: { type: "string" } }
29547
+ });
29548
+ if (!values.inbox || !values.session) {
29549
+ throw new CliError("Usage: blether watch --inbox <path> --session <id>");
29550
+ }
29551
+ const ended = await new Promise((resolve3) => {
29552
+ const stop = watchInbox(values.inbox, values.session, ctx.io.out, (why) => resolve3(why === "taken-over" ? "Blether watch stopped: another session took over this agent." : "Blether watch stopped: this session disconnected from Blether."));
29553
+ for (const signal of ["SIGINT", "SIGTERM"]) {
29554
+ process.once(signal, () => {
29555
+ stop();
29556
+ resolve3("");
29557
+ });
29558
+ }
29559
+ });
29560
+ if (ended)
29561
+ ctx.io.out(ended);
29562
+ return 0;
29563
+ }
29430
29564
  async function claudeInstall(ctx) {
29431
29565
  const root = "packageRoot" in ctx ? ctx.packageRoot : packageRoot();
29432
29566
  if (!root) {
@@ -29439,7 +29573,6 @@ async function claudeInstall(ctx) {
29439
29573
  throw new CliError(missing ? "Couldn't run `claude`. Install Claude Code, or check it's on your PATH." : error62.message);
29440
29574
  }
29441
29575
  ctx.io.out("Start a new Claude Code session to use it, and run /blether:setup in each project.");
29442
- ctx.io.out(`For push delivery, start sessions with: claude --dangerously-load-development-channels plugin:${PLUGIN_ID}`);
29443
29576
  return 0;
29444
29577
  }
29445
29578
  async function use(args, ctx) {
@@ -29448,7 +29581,7 @@ async function use(args, ctx) {
29448
29581
  allowPositionals: true,
29449
29582
  options: { dir: { type: "string" } }
29450
29583
  });
29451
- const usage = "Usage: blether use <team> <agent> [--dir <path>]";
29584
+ const usage = "Usage: blether use <team> <agent name> [--dir <path>]";
29452
29585
  if (positionals.length === 0)
29453
29586
  throw new CliError(usage);
29454
29587
  const record2 = loadTeam(ctx.teams, positionals[0]);
@@ -29463,10 +29596,14 @@ async function use(args, ctx) {
29463
29596
  if (agent.owner !== me2) {
29464
29597
  throw new CliError(`${name} belongs to another developer. Use one of yours (blether agent list ${record2.name}), or create one.`);
29465
29598
  }
29466
- const path = writeSessionFile(values.dir ?? ctx.cwd ?? process.cwd(), {
29467
- team: record2.name,
29468
- agent: name
29469
- });
29599
+ let path;
29600
+ try {
29601
+ path = writeSessionFile(values.dir ?? ctx.cwd ?? process.cwd(), { team: record2.name, agent: name }, ctx.store.home);
29602
+ } catch (error62) {
29603
+ if (error62 instanceof SessionFileError)
29604
+ throw new CliError(error62.message);
29605
+ throw error62;
29606
+ }
29470
29607
  ctx.io.out(`Sessions started in this project will act as ${name} in ${record2.name}.`);
29471
29608
  ctx.io.out(`Wrote ${path}; it's ignored by git, since the agent is yours. Restart any session already running here.`);
29472
29609
  return 0;
@@ -29479,15 +29616,15 @@ async function agentList(args, ctx) {
29479
29616
  online: await relay.getPresence(record2.id)
29480
29617
  }));
29481
29618
  if (team.agents.length === 0) {
29482
- ctx.io.out(`${record2.name} has no agents yet. Create one with: blether agent create ${record2.name} <name>`);
29483
- return 0;
29484
- }
29485
- ctx.io.out(`Agents in ${record2.name}:`);
29486
- for (const agent of team.agents) {
29487
- const owner = identities.get(agent.owner)?.name ?? "(unknown)";
29488
- const roles = agent.roles.length > 0 ? agent.roles.join(", ") : "no roles";
29489
- const presence = online.includes(agent.name) ? "online" : "offline";
29490
- ctx.io.out(` ${agent.name} ${owner}, ${roles}, ${presence}`);
29619
+ ctx.io.out(`${record2.name} has no agents yet. Create one with: blether agent create ${record2.name} <agent name>`);
29620
+ } else {
29621
+ ctx.io.out(`Agents in ${record2.name}:`);
29622
+ for (const agent of team.agents) {
29623
+ const owner = identities.get(agent.owner)?.name ?? "(unknown)";
29624
+ const roles = agent.roles.length > 0 ? agent.roles.join(", ") : "no roles";
29625
+ const presence = online.includes(agent.name) ? "online" : "offline";
29626
+ ctx.io.out(` ${agent.name} ${owner}, ${roles}, ${presence}`);
29627
+ }
29491
29628
  }
29492
29629
  if (team.roles.length > 0) {
29493
29630
  ctx.io.out(`Roles: ${team.roles.join(", ")}`);
@@ -29560,7 +29697,7 @@ async function join6(args, ctx) {
29560
29697
  });
29561
29698
  ctx.teams.save(record2);
29562
29699
  ctx.io.out(`Joined ${record2.name}.`);
29563
- ctx.io.out(`Create an agent with: blether agent create ${record2.name} <name>, then run blether use ${record2.name} <name> in your project.`);
29700
+ ctx.io.out(`Create an agent with: blether agent create ${record2.name} <agent name>, then run blether use ${record2.name} <agent name> in your project.`);
29564
29701
  return 0;
29565
29702
  }
29566
29703
  function now(ctx) {
@@ -1,12 +1,14 @@
1
1
  ---
2
2
  name: blether
3
- description: Message teammates' agents through Blether. Use before a change that affects code or interfaces another developer owns, when you need an answer only another developer's agent has, when your work blocks or unblocks someone, or when a Blether message arrives that needs a reply.
3
+ description: Message teammates' agents through Blether, in a session the developer has connected with /blether:connect. Once connected, use before a change that affects code or interfaces another developer owns, when you need an answer only another developer's agent has, when your work blocks or unblocks someone, or when a Blether message arrives that needs a reply.
4
4
  ---
5
5
 
6
6
  # Working with your team through Blether
7
7
 
8
8
  Blether connects you to the agents of the other developers on your team. Each agent works for its own developer, in its own project. The bridge's tools (`list_agents`, `send_message`, `read_mailbox`, `sent_messages`) do the carrying, and each tool result says how to handle what it returns. This skill is about **when** to reach out and **how to write** so the other agent can act without coming back to you.
9
9
 
10
+ Blether is used only in sessions the developer connects (`/blether:connect`). If the bridge's messaging tools aren't available, this session isn't connected: carry on without Blether, don't connect on your own, and don't bring it up.
11
+
10
12
  ## When to send
11
13
 
12
14
  - **Heads-up before you change something others depend on**: an API shape, a shared schema, a config key, a library version, a file layout. Send it before you commit, saying what changes and when, so their agent can adapt or object.
@@ -0,0 +1,17 @@
1
+ ---
2
+ name: connect
3
+ description: Connect this session to Blether, so it can message teammates' agents and hear about new mail.
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ # Connect this session to Blether
8
+
9
+ The developer wants this session to use Blether. Until now the bridge has stayed out of it.
10
+
11
+ 1. Check whether another session is using the agent. Connecting takes the agent over, and the other session is disconnected without warning.
12
+ - Read `.blether/session.json` in the project root for the team and agent. If there isn't one, skip this check: the `connect` tool says what's missing.
13
+ - Run `blether agent list <team>` (the plugin puts `blether` on your Bash `PATH`). If the agent shows as `online`, another session is using it, perhaps in another terminal or on another device. Tell the developer, and go on only if they say to take it over.
14
+ 2. Call the bridge's `connect` tool (`mcp__plugin_blether_blether__connect`; load it with ToolSearch first if it's deferred).
15
+ 3. If it fails, tell the developer what it says. If no agent is chosen for this project, offer `/blether:setup`.
16
+ 4. If it connects, do what its result says: read the mailbox, then start the watch with the Monitor tool, exactly as given.
17
+ 5. Tell the developer in two or three lines: which agent this session is, what's waiting in the mailbox, and that you'll hear about new mail while connected.
@@ -0,0 +1,9 @@
1
+ ---
2
+ name: disconnect
3
+ description: Disconnect this session from Blether, so it stops messaging teammates' agents and hearing about new mail.
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ # Disconnect this session from Blether
8
+
9
+ Call the bridge's `disconnect` tool (`mcp__plugin_blether_blether__disconnect`; load it with ToolSearch first if it's deferred). The watch stops by itself. Tell the developer in one line that the session is disconnected, and that `/blether:connect` connects it again.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: setup
3
- description: Set up Blether for this project - identity, team, agent, push delivery and status line.
3
+ description: Set up Blether for this project - identity, team, agent and status line - and connect this session.
4
4
  disable-model-invocation: true
5
5
  ---
6
6
 
@@ -31,23 +31,32 @@ Done when `team list` shows the team.
31
31
 
32
32
  ## 3. Agent for this project
33
33
 
34
- Run `agent list <team>` and ask which of **their** agents this project should act as, or what to call a new one. Agents are named for what they work on (`web`, `api`, `docs`), lowercase with hyphens. To create one, run `agent create <team> <name>`, adding `--role <role>` for roles the team already lists.
34
+ First read `.blether/session.json` in the project root, if it exists: it names the team and agent the project already acts as. If so, tell them, and ask whether to keep it. Keeping it: go on to step 4, without running `use`.
35
35
 
36
- Then run `use <team> <agent>` in the project root. It writes `.blether/session.json`, which git ignores.
36
+ Otherwise, run `agent list <team>` and ask which of **their** agents this project should act as, or what to call a new one. Teammates' agents message it by name, so suggest one that says what it works on (such as `web-app` or `payments-api`), lowercase with hyphens.
37
37
 
38
- Done when `use` reports the agent for this project.
38
+ For a new agent, run `role list <team>` and ask which roles describe what it does. Roles let teammates message every agent covering an area at once, and are optional. If one they want isn't listed, run `role add <team> <role>`. Then run `agent create <team> <agent name>`, with `--role <role>` for each role.
39
39
 
40
- ## 4. Reload and check
40
+ Then run `use <team> <agent name>` in the project root. It writes `.blether/session.json`, which git ignores.
41
41
 
42
- Tell the developer to restart this session (or run `/mcp` and reconnect the `blether` server) so the bridge picks up the project's agent.
42
+ Done when the project has the agent they want, kept or set with `use`.
43
43
 
44
- Done when the bridge's `list_agents` shows the roster. If the bridge offers only `blether_status`, it couldn't start: call it, and fix what it says.
44
+ ## 4. Connect and check
45
45
 
46
- ## 5. Push delivery and visibility
46
+ Setting Blether up is a request to use it, so connect this session. Connecting takes the agent over from any other session connected as it, which is then disconnected without warning. So first run `agent list <team>`: if the project's agent shows as `online`, another session (perhaps in another terminal, or on another device) is using it. Tell them, and connect only if they say to take it over.
47
47
 
48
- Explain these two options and set up the ones they want:
48
+ To connect, call the bridge's `connect` tool (`mcp__plugin_blether_blether__connect`; load it with ToolSearch first if it's deferred). It reads the project's agent when called, so there's no need to restart.
49
49
 
50
- - **Push delivery (channels)**: new messages can wake the session instead of waiting for the next mailbox check. Channels are a research preview, so Claude Code needs starting with `claude --dangerously-load-development-channels plugin:blether@blether`. Without it, nothing breaks; messages wait in the mailbox.
51
- - **Status line**: `blether status` prints a count of messages agents are holding for the developer's decision. Offer to add it to `statusLine` in `~/.claude/settings.json`, merging with any status line they already have.
50
+ Done when it connects and its result's instructions are followed (read the mailbox, start the watch). If it fails, fix what it says.
51
+
52
+ ## 5. How it works, and visibility
53
+
54
+ Tell them how Blether works from now on:
55
+
56
+ - **Sessions start without Blether.** The bridge stays out of every session until they run `/blether:connect`. A connected session can message teammates' agents and hears about new mail through a watch; `/blether:disconnect` ends that. Only one session can be connected as an agent at a time: connecting another takes the agent over.
57
+
58
+ Offer this option and set it up if they want it:
59
+
60
+ - **Status line**: `blether status` prints a count of messages agents are holding for the developer's decision. Read `statusLine` in `~/.claude/settings.json` first: if it already runs `blether status`, say so and skip this. Otherwise, offer to add it, merging with any status line they already have.
52
61
 
53
62
  Finish with a one-paragraph summary: who they are, which team, which agent this project acts as, and what's enabled.