@kvsm/blether 0.1.0 → 0.2.0
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 +69 -0
- package/package.json +5 -3
- package/plugin/.claude-plugin/plugin.json +5 -10
- package/plugin/dist/bridge.js +253 -27
- package/plugin/dist/cli.js +128 -9
- package/plugin/skills/blether/SKILL.md +3 -1
- package/plugin/skills/connect/SKILL.md +14 -0
- package/plugin/skills/disconnect/SKILL.md +9 -0
- package/plugin/skills/setup/SKILL.md +10 -7
package/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
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.
|
|
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
|
+
blether init --name <name> # your identity, once per developer
|
|
26
|
+
blether join <invite> # prints "Joined <team>."
|
|
27
|
+
blether agent create <team> web # an agent you own: a named mailbox
|
|
28
|
+
|
|
29
|
+
cd ~/code/web-app # each project an agent works in
|
|
30
|
+
blether use <team> web # sessions started here act as "web"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Start a new Claude Code session in the project and run `/blether:connect`. Claude can now message your teammates' agents, and is told when messages arrive. 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
|
+
To start a team or run a relay, see [Getting started](https://github.com/kvsm/blether#getting-started).
|
|
38
|
+
|
|
39
|
+
## Other agents
|
|
40
|
+
|
|
41
|
+
Any agent that speaks MCP can use the bridge. Add it to the agent's MCP config:
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"mcpServers": {
|
|
46
|
+
"blether": { "command": "blether-bridge" }
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
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`.
|
|
52
|
+
|
|
53
|
+
## Updating
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
npm install -g @kvsm/blether@latest
|
|
57
|
+
blether --version # shows the version now installed
|
|
58
|
+
blether claude install # if you use Claude Code: updates the plugin to match
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Then start new agent sessions: a running session keeps the bridge it started with. Your identity, teams and projects are left as they are. See [Updating](https://github.com/kvsm/blether#updating) for the details.
|
|
62
|
+
|
|
63
|
+
## Documentation
|
|
64
|
+
|
|
65
|
+
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.
|
|
66
|
+
|
|
67
|
+
## License
|
|
68
|
+
|
|
69
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kvsm/blether",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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.
|
|
33
|
-
"@blether/relay": "0.
|
|
34
|
+
"@blether/bridge": "0.2.0",
|
|
35
|
+
"@blether/relay": "0.2.0"
|
|
34
36
|
},
|
|
35
37
|
"publishConfig": {
|
|
36
38
|
"access": "public"
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blether",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"displayName": "Blether",
|
|
5
|
-
"description": "Message your teammates' coding agents through Blether: the bridge,
|
|
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
|
}
|
package/plugin/dist/bridge.js
CHANGED
|
@@ -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.0";
|
|
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
|
|
37578
|
-
import {
|
|
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 =
|
|
37591
|
-
const path2 =
|
|
37592
|
-
if (
|
|
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 (
|
|
37674
|
+
if (existsSync6(join6(dir, ".git")) || parent === dir)
|
|
37597
37675
|
return void 0;
|
|
37598
37676
|
dir = parent;
|
|
37599
37677
|
}
|
|
@@ -37601,7 +37679,7 @@ function findSessionFile(start, bletherHome) {
|
|
|
37601
37679
|
function readSessionFile(path2) {
|
|
37602
37680
|
let raw;
|
|
37603
37681
|
try {
|
|
37604
|
-
raw = JSON.parse(
|
|
37682
|
+
raw = JSON.parse(readFileSync6(path2, "utf8"));
|
|
37605
37683
|
} catch (error62) {
|
|
37606
37684
|
throw new SessionFileError(`${path2} isn't valid JSON (${error62.message}). Write it again with \`blether use <team> <agent>\`.`);
|
|
37607
37685
|
}
|
|
@@ -37613,7 +37691,7 @@ function readSessionFile(path2) {
|
|
|
37613
37691
|
}
|
|
37614
37692
|
|
|
37615
37693
|
// ../bridge/dist/relay-connection.js
|
|
37616
|
-
import { randomUUID as
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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:
|
|
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 asks you to connect to Blether."
|
|
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 or asks you to."
|
|
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:
|
|
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 =
|
|
51283
|
+
const fanout = randomUUID4();
|
|
51148
51284
|
const sendOne = async (recipient) => {
|
|
51149
51285
|
const receipt = await relay.send(recipient, body, {
|
|
51150
51286
|
audience,
|
|
@@ -51466,6 +51602,8 @@ var REFUSAL_FIXES = {
|
|
|
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
|
-
|
|
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 {
|
|
@@ -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,
|
|
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
|
|
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
|
|
51573
|
-
sentLog: new SentLog(store.home, team.id, agent)
|
|
51574
|
-
|
|
51575
|
-
|
|
51576
|
-
|
|
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
|
package/plugin/dist/cli.js
CHANGED
|
@@ -27617,6 +27617,9 @@ function parseDeviceGrant(text) {
|
|
|
27617
27617
|
}
|
|
27618
27618
|
}
|
|
27619
27619
|
|
|
27620
|
+
// ../protocol/dist/version.js
|
|
27621
|
+
var BLETHER_VERSION = "0.2.0";
|
|
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
|
-
|
|
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
|
|
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 (
|
|
28824
|
+
if (existsSync6(join5(dir, ".claude-plugin", "marketplace.json")))
|
|
28741
28825
|
return dir;
|
|
28742
28826
|
const parent = dirname2(dir);
|
|
28743
28827
|
if (parent === dir)
|
|
@@ -28801,6 +28885,9 @@ Commands:
|
|
|
28801
28885
|
agent delete <team> <name> Delete one of your agents (or any, as Team Admin)
|
|
28802
28886
|
use <team> <agent> [--dir <path>] Make sessions started in this project (or <path>) act as <agent>
|
|
28803
28887
|
claude install Install (or update) the Blether plugin in Claude Code
|
|
28888
|
+
watch --inbox <path> --session <id>
|
|
28889
|
+
Print a line whenever a connected session's agent gets mail
|
|
28890
|
+
(the bridge gives its agent this command when it connects)
|
|
28804
28891
|
escalations List messages your agents are holding for your decision
|
|
28805
28892
|
status One line for your Claude Code status line: escalations waiting
|
|
28806
28893
|
policy Show your Approval Policy on this device
|
|
@@ -28810,6 +28897,7 @@ Commands:
|
|
|
28810
28897
|
outgoing: ask | ask-others | free
|
|
28811
28898
|
incoming: ask | ask-impactful | free
|
|
28812
28899
|
limits: most messages an agent sends in the window
|
|
28900
|
+
--version Show which version of Blether is installed
|
|
28813
28901
|
|
|
28814
28902
|
Set BLETHER_HOME to keep Blether's files somewhere other than ~/.blether.`;
|
|
28815
28903
|
var CliError = class extends Error {
|
|
@@ -28917,6 +29005,8 @@ ${USAGE}`);
|
|
|
28917
29005
|
}
|
|
28918
29006
|
case "use":
|
|
28919
29007
|
return use(rest, ctx);
|
|
29008
|
+
case "watch":
|
|
29009
|
+
return watch(rest, ctx);
|
|
28920
29010
|
case "claude": {
|
|
28921
29011
|
const [sub] = rest;
|
|
28922
29012
|
if (sub === "install")
|
|
@@ -28937,6 +29027,11 @@ ${USAGE}`);
|
|
|
28937
29027
|
case "-h":
|
|
28938
29028
|
ctx.io.out(USAGE);
|
|
28939
29029
|
return command === void 0 ? 1 : 0;
|
|
29030
|
+
case "version":
|
|
29031
|
+
case "--version":
|
|
29032
|
+
case "-v":
|
|
29033
|
+
ctx.io.out(BLETHER_VERSION);
|
|
29034
|
+
return 0;
|
|
28940
29035
|
default:
|
|
28941
29036
|
throw new CliError(`Unknown command: ${command}
|
|
28942
29037
|
|
|
@@ -28950,7 +29045,7 @@ function init(args, { store, io }) {
|
|
|
28950
29045
|
});
|
|
28951
29046
|
const name = DeveloperName.safeParse(values.name);
|
|
28952
29047
|
if (!name.success) {
|
|
28953
|
-
throw new CliError(
|
|
29048
|
+
throw new CliError("Give your name, as teammates will see it: blether init --name <name>");
|
|
28954
29049
|
}
|
|
28955
29050
|
if (store.exists()) {
|
|
28956
29051
|
throw new CliError(`There is already a Blether identity in ${store.home}. Run \`blether whoami\` to see it.`);
|
|
@@ -29427,6 +29522,27 @@ async function agentDelete(args, ctx) {
|
|
|
29427
29522
|
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
29523
|
return 0;
|
|
29429
29524
|
}
|
|
29525
|
+
async function watch(args, ctx) {
|
|
29526
|
+
const { values } = parseArgs({
|
|
29527
|
+
args,
|
|
29528
|
+
options: { inbox: { type: "string" }, session: { type: "string" } }
|
|
29529
|
+
});
|
|
29530
|
+
if (!values.inbox || !values.session) {
|
|
29531
|
+
throw new CliError("Usage: blether watch --inbox <path> --session <id>");
|
|
29532
|
+
}
|
|
29533
|
+
const ended = await new Promise((resolve3) => {
|
|
29534
|
+
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."));
|
|
29535
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
29536
|
+
process.once(signal, () => {
|
|
29537
|
+
stop();
|
|
29538
|
+
resolve3("");
|
|
29539
|
+
});
|
|
29540
|
+
}
|
|
29541
|
+
});
|
|
29542
|
+
if (ended)
|
|
29543
|
+
ctx.io.out(ended);
|
|
29544
|
+
return 0;
|
|
29545
|
+
}
|
|
29430
29546
|
async function claudeInstall(ctx) {
|
|
29431
29547
|
const root = "packageRoot" in ctx ? ctx.packageRoot : packageRoot();
|
|
29432
29548
|
if (!root) {
|
|
@@ -29439,7 +29555,6 @@ async function claudeInstall(ctx) {
|
|
|
29439
29555
|
throw new CliError(missing ? "Couldn't run `claude`. Install Claude Code, or check it's on your PATH." : error62.message);
|
|
29440
29556
|
}
|
|
29441
29557
|
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
29558
|
return 0;
|
|
29444
29559
|
}
|
|
29445
29560
|
async function use(args, ctx) {
|
|
@@ -29463,10 +29578,14 @@ async function use(args, ctx) {
|
|
|
29463
29578
|
if (agent.owner !== me2) {
|
|
29464
29579
|
throw new CliError(`${name} belongs to another developer. Use one of yours (blether agent list ${record2.name}), or create one.`);
|
|
29465
29580
|
}
|
|
29466
|
-
|
|
29467
|
-
|
|
29468
|
-
agent: name
|
|
29469
|
-
})
|
|
29581
|
+
let path;
|
|
29582
|
+
try {
|
|
29583
|
+
path = writeSessionFile(values.dir ?? ctx.cwd ?? process.cwd(), { team: record2.name, agent: name }, ctx.store.home);
|
|
29584
|
+
} catch (error62) {
|
|
29585
|
+
if (error62 instanceof SessionFileError)
|
|
29586
|
+
throw new CliError(error62.message);
|
|
29587
|
+
throw error62;
|
|
29588
|
+
}
|
|
29470
29589
|
ctx.io.out(`Sessions started in this project will act as ${name} in ${record2.name}.`);
|
|
29471
29590
|
ctx.io.out(`Wrote ${path}; it's ignored by git, since the agent is yours. Restart any session already running here.`);
|
|
29472
29591
|
return 0;
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: blether
|
|
3
|
-
description: Message teammates' agents through Blether.
|
|
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,14 @@
|
|
|
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. Call the bridge's `connect` tool (`mcp__plugin_blether_blether__connect`; load it with ToolSearch first if it's deferred).
|
|
12
|
+
2. If it fails, tell the developer what it says. If no agent is chosen for this project, offer `/blether:setup`.
|
|
13
|
+
3. If it connects, do what its result says: read the mailbox, then start the watch with the Monitor tool, exactly as given.
|
|
14
|
+
4. 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. If another session was using the agent, it has been disconnected.
|
|
@@ -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
|
|
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
|
|
|
@@ -37,17 +37,20 @@ Then run `use <team> <agent>` in the project root. It writes `.blether/session.j
|
|
|
37
37
|
|
|
38
38
|
Done when `use` reports the agent for this project.
|
|
39
39
|
|
|
40
|
-
## 4.
|
|
40
|
+
## 4. Connect and check
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
Setting Blether up is a request to use it, so connect this session: 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.
|
|
43
43
|
|
|
44
|
-
Done when
|
|
44
|
+
Done when it connects and its result's instructions are followed (read the mailbox, start the watch). If it fails, fix what it says.
|
|
45
45
|
|
|
46
|
-
## 5.
|
|
46
|
+
## 5. How it works, and visibility
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
Tell them how Blether works from now on:
|
|
49
|
+
|
|
50
|
+
- **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.
|
|
51
|
+
|
|
52
|
+
Offer this option and set it up if they want it:
|
|
49
53
|
|
|
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
54
|
- **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.
|
|
52
55
|
|
|
53
56
|
Finish with a one-paragraph summary: who they are, which team, which agent this project acts as, and what's enabled.
|