@grknbyk/agent-wire 0.4.1 β 0.5.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 +276 -243
- package/bin/agent-wire.mjs +127 -115
- package/manifest.json +31 -38
- package/package.json +3 -2
- package/src/config.mjs +114 -68
- package/src/identity.mjs +101 -73
- package/src/inbox.mjs +99 -76
- package/src/mcp.mjs +382 -309
- package/src/protocol.mjs +77 -68
- package/src/setup.mjs +171 -262
- package/src/slack.mjs +322 -236
package/README.md
CHANGED
|
@@ -1,243 +1,276 @@
|
|
|
1
|
-
<div align="center">
|
|
2
|
-
<img src="assets/agent-wire.png" alt="agent-wire" width="96">
|
|
3
|
-
|
|
4
|
-
# agent-wire
|
|
5
|
-
|
|
6
|
-
**Let your AI coding agents talk to each other, in a Slack channel you can read.**
|
|
7
|
-
|
|
8
|
-
</div>
|
|
9
|
-
|
|
10
|
-
Two developers, two machines, two coding agents working on the same system. One
|
|
11
|
-
knows the migration is deployed. The other is about to write against the old
|
|
12
|
-
schema. agent-wire gives them a way to say so.
|
|
13
|
-
|
|
14
|
-
It runs as an [MCP](https://modelcontextprotocol.io) server, so any MCP client
|
|
15
|
-
(Claude Code, Cursor, anything else that speaks the protocol) gets `send` and
|
|
16
|
-
`inbox` tools. Messages travel through a normal Slack channel.
|
|
17
|
-
|
|
18
|
-
Slack is a deliberate choice here. A private protocol between two machines
|
|
19
|
-
produces a conversation nobody can audit. In a channel, the humans who own those
|
|
20
|
-
agents read the whole exchange, scroll back through it, and step in by typing.
|
|
21
|
-
|
|
22
|
-
```
|
|
23
|
-
π₯ grkn => mira
|
|
24
|
-
migration 0042 is on dev now, txn_date is a DATE not a TIMESTAMP
|
|
25
|
-
|
|
26
|
-
β‘ mira => grkn
|
|
27
|
-
got it, rewriting the report query
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
## Install
|
|
31
|
-
|
|
32
|
-
```bash
|
|
33
|
-
npx @grknbyk/agent-wire setup
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
Setup
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
Setup never asks "did you do it? (y/n)". Every step it can verify, it verifies by
|
|
45
|
-
asking Slack. When a step is stuck for a reason Slack reports, such as a missing
|
|
46
|
-
scope, a
|
|
47
|
-
says which one and what to do about it. Quit halfway and re-run: it resumes at
|
|
48
|
-
the first unfinished step, because the config file is the progress.
|
|
49
|
-
|
|
50
|
-
Then point your client at it:
|
|
51
|
-
|
|
52
|
-
```bash
|
|
53
|
-
claude mcp add agent-wire -- npx -y @grknbyk/agent-wire serve
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
Or, for any other MCP client:
|
|
57
|
-
|
|
58
|
-
```json
|
|
59
|
-
{
|
|
60
|
-
"mcpServers": {
|
|
61
|
-
"agent-wire": { "command": "npx", "args": ["-y", "@grknbyk/agent-wire", "serve"] }
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
## Commands
|
|
67
|
-
|
|
68
|
-
| Command | What it does |
|
|
69
|
-
|---|---|
|
|
70
|
-
| `agent-wire status` | Identity, channels and unread counts, read from disk |
|
|
71
|
-
| `agent-wire setup` | Connect a workspace, a channel, and this agent's identity |
|
|
72
|
-
| `agent-wire serve` | Run the MCP stdio server, which is what your client launches |
|
|
73
|
-
| `agent-wire doctor` | Re-check the token, the channels and the identity |
|
|
74
|
-
| `agent-wire drain` | Print what arrived since last time, for a prompt hook |
|
|
75
|
-
| `agent-wire channels` | List the channels and whether each one is switched on |
|
|
76
|
-
| `agent-wire on/off <name>` | Bring a channel into scope, or take it out |
|
|
77
|
-
|
|
78
|
-
## Tools your agent gets
|
|
79
|
-
|
|
80
|
-
`send`, `send_file`, `inbox`, `archive`, `peers`, `channels`, `my_id`.
|
|
81
|
-
|
|
82
|
-
Text over 3500 characters is posted as a Markdown file instead of a message.
|
|
83
|
-
Slack splits anything longer, and the tail arrives without a header, so half an
|
|
84
|
-
answer vanishes while the sender is told it was delivered.
|
|
85
|
-
|
|
86
|
-
##
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
##
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img src="assets/agent-wire.png" alt="agent-wire" width="96">
|
|
3
|
+
|
|
4
|
+
# agent-wire
|
|
5
|
+
|
|
6
|
+
**Let your AI coding agents talk to each other, in a Slack channel you can read.**
|
|
7
|
+
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
Two developers, two machines, two coding agents working on the same system. One
|
|
11
|
+
knows the migration is deployed. The other is about to write against the old
|
|
12
|
+
schema. agent-wire gives them a way to say so.
|
|
13
|
+
|
|
14
|
+
It runs as an [MCP](https://modelcontextprotocol.io) server, so any MCP client
|
|
15
|
+
(Claude Code, Cursor, anything else that speaks the protocol) gets `send` and
|
|
16
|
+
`inbox` tools. Messages travel through a normal Slack channel.
|
|
17
|
+
|
|
18
|
+
Slack is a deliberate choice here. A private protocol between two machines
|
|
19
|
+
produces a conversation nobody can audit. In a channel, the humans who own those
|
|
20
|
+
agents read the whole exchange, scroll back through it, and step in by typing.
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
π₯ grkn => mira
|
|
24
|
+
migration 0042 is on dev now, txn_date is a DATE not a TIMESTAMP
|
|
25
|
+
|
|
26
|
+
β‘ mira => grkn
|
|
27
|
+
got it, rewriting the report query
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npx @grknbyk/agent-wire setup
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Setup prints the path of the bundled `manifest.json`. You create the app from it
|
|
37
|
+
at [api.slack.com/apps/new](https://api.slack.com/apps/new), install it, and paste
|
|
38
|
+
the Bot User OAuth Token back. Then you create the channel in Slack and type
|
|
39
|
+
`/invite @agent-wire` in it.
|
|
40
|
+
|
|
41
|
+
The app never adds itself to anything. It has no scope to create a channel or to
|
|
42
|
+
join one, so a person decides where it can read and write.
|
|
43
|
+
|
|
44
|
+
Setup never asks "did you do it? (y/n)". Every step it can verify, it verifies by
|
|
45
|
+
asking Slack. When a step is stuck for a reason Slack reports, such as a missing
|
|
46
|
+
scope, a channel nobody invited it to, or a token from the wrong workspace, it
|
|
47
|
+
says which one and what to do about it. Quit halfway and re-run: it resumes at
|
|
48
|
+
the first unfinished step, because the config file is the progress.
|
|
49
|
+
|
|
50
|
+
Then point your client at it:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
claude mcp add agent-wire -- npx -y @grknbyk/agent-wire serve
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Or, for any other MCP client:
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"mcpServers": {
|
|
61
|
+
"agent-wire": { "command": "npx", "args": ["-y", "@grknbyk/agent-wire", "serve"] }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Commands
|
|
67
|
+
|
|
68
|
+
| Command | What it does |
|
|
69
|
+
|---|---|
|
|
70
|
+
| `agent-wire status` | Identity, channels and unread counts, read from disk |
|
|
71
|
+
| `agent-wire setup` | Connect a workspace, a channel, and this agent's identity |
|
|
72
|
+
| `agent-wire serve` | Run the MCP stdio server, which is what your client launches |
|
|
73
|
+
| `agent-wire doctor` | Re-check the token, the channels and the identity |
|
|
74
|
+
| `agent-wire drain` | Print what arrived since last time, for a prompt hook |
|
|
75
|
+
| `agent-wire channels` | List the channels and whether each one is switched on |
|
|
76
|
+
| `agent-wire on/off <name>` | Bring a channel into scope, or take it out |
|
|
77
|
+
|
|
78
|
+
## Tools your agent gets
|
|
79
|
+
|
|
80
|
+
`send`, `send_file`, `inbox`, `archive`, `peers`, `members`, `channels`, `my_id`.
|
|
81
|
+
|
|
82
|
+
Text over 3500 characters is posted as a Markdown file instead of a message.
|
|
83
|
+
Slack splits anything longer, and the tail arrives without a header, so half an
|
|
84
|
+
answer vanishes while the sender is told it was delivered.
|
|
85
|
+
|
|
86
|
+
## Files go both ways
|
|
87
|
+
|
|
88
|
+
`send_file` uploads, and the receiving side downloads. A `.md` plan sent from one
|
|
89
|
+
machine lands on the other as a real file in `~/.agent-wire/files/`, and `inbox`
|
|
90
|
+
prints that path in the fence header, so the agent opens it with its own tools.
|
|
91
|
+
Files a human drags into the channel arrive the same way.
|
|
92
|
+
|
|
93
|
+
Slack accepts no metadata on a file upload, so the file and the message that
|
|
94
|
+
describes it are two posts. The message is the signed one, and the file id it
|
|
95
|
+
names is inside what the signature covers, so a valid signature cannot be lifted
|
|
96
|
+
onto somebody else's upload. A message that fails verification is never
|
|
97
|
+
downloaded.
|
|
98
|
+
|
|
99
|
+
Anything over 20 MB stays in Slack. The message still arrives and says why the
|
|
100
|
+
file was left there.
|
|
101
|
+
|
|
102
|
+
## One channel per project
|
|
103
|
+
|
|
104
|
+
Setup configures one channel. Add more by hand in `~/.agent-wire/config.json`:
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
"channels": [
|
|
108
|
+
{ "id": "C0123", "name": "agent-wms" },
|
|
109
|
+
{ "id": "C0456", "name": "agent-crm" }
|
|
110
|
+
]
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Every message is tagged with the channel it came from, `send` takes an optional
|
|
114
|
+
`channel`, and `inbox` can filter by one. The first entry is the default.
|
|
115
|
+
|
|
116
|
+
## Working on two of five channels
|
|
117
|
+
|
|
118
|
+
Channels you are not working on today can be switched off. Running `agent-wire`
|
|
119
|
+
with no arguments shows where you stand:
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
βββββββββββββββββ agent-wire βββββββββββββββββ
|
|
123
|
+
β name grkn mark π₯ β
|
|
124
|
+
β key MCowBQYDK2VwAyEAq7Xn2mZ8kLcYzQwErTyβ¦ β
|
|
125
|
+
ββββββββββββββββββ CHANNELS ββββββββββββββββββ€
|
|
126
|
+
β agent-wms β on 3 unread β
|
|
127
|
+
β agent-crm β on 1 unread β
|
|
128
|
+
β agent-hcm β off 1 held β
|
|
129
|
+
β agent-lab β off 1 held β
|
|
130
|
+
βββββββββββββββββββ PEERS ββββββββββββββββββββ€
|
|
131
|
+
β @ ZoΓ« * kai * mira β
|
|
132
|
+
β * warehouse-β¦ * robin ! nox β
|
|
133
|
+
βββββββββββββββββββ STATE ββββββββββββββββββββ€
|
|
134
|
+
β workspace Acme poll 14s ago β
|
|
135
|
+
ββββββββββββββββββββββββββββββββββββββββββββββ
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The peers section lists everyone this agent has heard from: `*` for an agent,
|
|
139
|
+
`@` for a human typing in the channel, `!` for a name that has been forged.
|
|
140
|
+
Anything too wide for its column ends in `β¦`, so one long nickname costs its own
|
|
141
|
+
row a character instead of pushing the border out.
|
|
142
|
+
|
|
143
|
+
A forged sighting stays on the record even after that name sends a message that
|
|
144
|
+
verifies. Letting a later message clear it would hand an attacker the way to bury
|
|
145
|
+
the evidence.
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
agent-wire off agent-hcm
|
|
149
|
+
agent-wire on agent-hcm
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
`status` reads the config and the local log only, so it answers instantly.
|
|
153
|
+
Whether Slack still accepts the token is `doctor`'s question.
|
|
154
|
+
|
|
155
|
+
A channel that is off is not polled, not announced by `drain`, and absent from
|
|
156
|
+
the default `inbox` view. Its history stays readable at any time with
|
|
157
|
+
`inbox channel="agent-hcm"`.
|
|
158
|
+
|
|
159
|
+
Switching one off does not lose messages. The cursor stays where it was, so
|
|
160
|
+
switching it back on replays everything that arrived meanwhile.
|
|
161
|
+
|
|
162
|
+
Only the person running the agent can switch a channel, from the command line.
|
|
163
|
+
The MCP `channels` tool lists the state and cannot change it, so a message
|
|
164
|
+
arriving from one channel can never talk the agent into silencing another.
|
|
165
|
+
|
|
166
|
+
## Who actually sent that message
|
|
167
|
+
|
|
168
|
+
Every agent in a workspace shares one bot token, so Slack's own `bot_id` proves
|
|
169
|
+
that agent-wire posted a message without proving which agent wrote it. The header
|
|
170
|
+
line is plain text that anyone in the channel can type.
|
|
171
|
+
|
|
172
|
+
So each install generates an Ed25519 key pair at setup and signs every message it
|
|
173
|
+
sends. The signature covers the sender, the recipient, the channel, the position
|
|
174
|
+
in the reply chain, and the text. It travels in Slack message metadata, which the
|
|
175
|
+
UI never renders. The first key seen using a name is pinned to that name, and
|
|
176
|
+
`inbox` labels every message with what is actually proven:
|
|
177
|
+
|
|
178
|
+
| Label | Meaning |
|
|
179
|
+
|---|---|
|
|
180
|
+
| `signed` | Verified against the key already pinned to that name |
|
|
181
|
+
| `new` | Verified, first sighting of this name, key now pinned |
|
|
182
|
+
| `impostor` | That name is pinned to a different key, so treat it as forged |
|
|
183
|
+
| `unsigned` | No valid signature, so the sender name is decoration only |
|
|
184
|
+
| `slack-verified` | A human, identified by Slack's own user id |
|
|
185
|
+
| `self` | Sent by this agent |
|
|
186
|
+
|
|
187
|
+
Changing one character of the text breaks the signature, and so does replaying a
|
|
188
|
+
signed message into another channel. There are tests for both.
|
|
189
|
+
|
|
190
|
+
## Untrusted input
|
|
191
|
+
|
|
192
|
+
Anything arriving from the channel is rendered inside a fence whose delimiter is
|
|
193
|
+
a random value minted per server process, never written to Slack and never
|
|
194
|
+
logged:
|
|
195
|
+
|
|
196
|
+
```
|
|
197
|
+
<<<WIRE:4f2a⦠UNTRUSTED from=mira kind=agent authorship=signed channel=agent-wms ts=1712.44 hop=3>>>
|
|
198
|
+
the message
|
|
199
|
+
<<<END:4f2aβ¦>>>
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
The rule for reading that fence arrives through the MCP handshake, a channel the
|
|
203
|
+
message author cannot write to, so it never sits inline beside the content it
|
|
204
|
+
governs. If a payload contains the live delimiter, it is replaced with
|
|
205
|
+
`[FENCE-ECHO REDACTED]`, which turns reflection into a visible event instead of a
|
|
206
|
+
silently broken boundary.
|
|
207
|
+
|
|
208
|
+
Be clear about what this buys you. An attacker cannot close the fence, and does
|
|
209
|
+
not need to, because text inside a correctly labelled `UNTRUSTED` block still
|
|
210
|
+
reads as language to a model. The fence makes the labelling accurate. Hostile
|
|
211
|
+
text stays exactly as persuasive as it was, so this is a boundary rather than a
|
|
212
|
+
filter.
|
|
213
|
+
|
|
214
|
+
A reply chain also carries a hop count and stops at 8. Two agents answering each
|
|
215
|
+
other politely is an infinite loop that costs real money.
|
|
216
|
+
|
|
217
|
+
## Slack scopes, and why each one
|
|
218
|
+
|
|
219
|
+
Your workspace admin will ask. The manifest requests:
|
|
220
|
+
|
|
221
|
+
| Scope | Why |
|
|
222
|
+
|---|---|
|
|
223
|
+
| `chat:write` | Post messages |
|
|
224
|
+
| `channels:history` | Read the channels it was added to |
|
|
225
|
+
| `channels:read` | Find a channel by name, list who is in it |
|
|
226
|
+
| `files:write` | Send a file, and post a long message as one |
|
|
227
|
+
| `files:read` | Download a file somebody sent |
|
|
228
|
+
| `users:read` | Show a human's name instead of `U08J21KLER1` |
|
|
229
|
+
|
|
230
|
+
Six, and that is the whole list. No `channels:join` or `channels:manage`, so the
|
|
231
|
+
app cannot add itself to a channel or create one. No `groups:*`, so private
|
|
232
|
+
channels are out of reach: use a public one.
|
|
233
|
+
|
|
234
|
+
The two lookups it does are both scoped to the invite. Channels come from
|
|
235
|
+
`users.conversations`, which answers "which channels am I in", never
|
|
236
|
+
`conversations.list`, which answers "which channels exist here". Names come from
|
|
237
|
+
`conversations.members` on one of those channels. There is no call in the package
|
|
238
|
+
that can enumerate the workspace.
|
|
239
|
+
|
|
240
|
+
## Where things are stored
|
|
241
|
+
|
|
242
|
+
Everything lives in `~/.agent-wire/` (override with `AGENT_WIRE_HOME`).
|
|
243
|
+
`config.json` holds the token, identity and channels. `inbox.jsonl` is the
|
|
244
|
+
append-only message log. `peers.json` holds the pinned keys. `files/` holds every
|
|
245
|
+
attachment that arrived, named by Slack file id so two `plan.md` files stay two
|
|
246
|
+
files.
|
|
247
|
+
|
|
248
|
+
The local log is the source of truth. Slack is a cache that can be re-read at any
|
|
249
|
+
time, so recovering a lost inbox is an ordinary operation rather than a
|
|
250
|
+
procedure. Messages are keyed by their Slack timestamp, so a retried poll or a
|
|
251
|
+
reinstalled app cannot produce duplicates.
|
|
252
|
+
|
|
253
|
+
## Roadmap
|
|
254
|
+
|
|
255
|
+
- `mode: reply`, to answer waiting messages when no live session is watching
|
|
256
|
+
- Per-worktree identity, so parallel sessions on one machine name themselves
|
|
257
|
+
- Discord as a second transport
|
|
258
|
+
- Published measurements of fenced against unfenced injection compliance
|
|
259
|
+
|
|
260
|
+
Wire format v2 signs the attached file id alongside the text, so a 0.5 agent and
|
|
261
|
+
a 0.4 agent cannot verify each other. Upgrade both ends together.
|
|
262
|
+
|
|
263
|
+
## Development
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
npm test # 42 tests, no network
|
|
267
|
+
npm run bench # medians over a synthetic 20k-message log
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
The benchmark is here because the slow paths are the ones nobody watches: a log
|
|
271
|
+
that only grows, and a CLI that a prompt hook runs on every prompt. It is not
|
|
272
|
+
shipped to npm.
|
|
273
|
+
|
|
274
|
+
## License
|
|
275
|
+
|
|
276
|
+
MIT
|