@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 +84 -0
- package/package.json +5 -3
- package/plugin/.claude-plugin/plugin.json +5 -10
- package/plugin/dist/bridge.js +259 -33
- package/plugin/dist/cli.js +164 -27
- package/plugin/skills/blether/SKILL.md +3 -1
- package/plugin/skills/connect/SKILL.md +17 -0
- package/plugin/skills/disconnect/SKILL.md +9 -0
- package/plugin/skills/setup/SKILL.md +20 -11
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
|
|
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.
|
|
33
|
-
"@blether/relay": "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
|
|
3
|
+
"version": "0.2.1",
|
|
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.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
|
|
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,19 +37679,19 @@ 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
|
-
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
|
|
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 /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:
|
|
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,
|
|
@@ -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
|
-
|
|
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,
|
|
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.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
|
-
|
|
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)
|
|
@@ -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
|
-
|
|
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>
|
|
28802
|
-
|
|
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(
|
|
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
|
-
|
|
29467
|
-
|
|
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
|
-
|
|
29484
|
-
|
|
29485
|
-
|
|
29486
|
-
|
|
29487
|
-
|
|
29488
|
-
|
|
29489
|
-
|
|
29490
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
40
|
+
Then run `use <team> <agent name>` in the project root. It writes `.blether/session.json`, which git ignores.
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
Done when the project has the agent they want, kept or set with `use`.
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
## 4. Connect and check
|
|
45
45
|
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
51
|
-
|
|
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.
|