@grknbyk/agent-wire 0.13.2 → 0.13.4
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 +24 -11
- package/bin/agent-wire.mjs +11 -0
- package/package.json +1 -1
- package/src/mcp.mjs +22 -3
- package/src/protocol.mjs +30 -8
- package/src/slack.mjs +5 -0
- package/src/status.mjs +2 -1
package/README.md
CHANGED
|
@@ -20,10 +20,10 @@ produces a conversation nobody can audit. In a channel, the humans who own those
|
|
|
20
20
|
agents read the whole exchange, scroll back through it, and step in by typing.
|
|
21
21
|
|
|
22
22
|
```
|
|
23
|
-
🔥 grkn => mira
|
|
23
|
+
🔥 grkn => @mira
|
|
24
24
|
migration 0042 is on dev now, txn_date is a DATE not a TIMESTAMP
|
|
25
25
|
|
|
26
|
-
⚡ mira => grkn
|
|
26
|
+
⚡ mira => @grkn
|
|
27
27
|
got it, rewriting the report query
|
|
28
28
|
```
|
|
29
29
|
|
|
@@ -254,15 +254,15 @@ Running `agent-wire` with no arguments shows where you stand:
|
|
|
254
254
|
│ agent-hcm ○ off 1 held │
|
|
255
255
|
│ agent-lab ○ off 1 held │
|
|
256
256
|
├────────────────── PEERS ───────────────────┤
|
|
257
|
-
│
|
|
258
|
-
│
|
|
257
|
+
│ + Zoë @ kai @ mira │
|
|
258
|
+
│ @ warehouse-… @ robin ! nox │
|
|
259
259
|
├────────────────── STATE ───────────────────┤
|
|
260
260
|
│ workspace Acme poll 14s ago │
|
|
261
261
|
└────────────────────────────────────────────┘
|
|
262
262
|
```
|
|
263
263
|
|
|
264
|
-
The peers section lists everyone this agent has heard from:
|
|
265
|
-
|
|
264
|
+
The peers section lists everyone this agent has heard from: `@` for an agent,
|
|
265
|
+
`+` for a human typing in the channel, `!` for a name that has been forged.
|
|
266
266
|
Anything too wide for its column ends in `…`, so one long nickname costs its own
|
|
267
267
|
row a character instead of pushing the border out.
|
|
268
268
|
|
|
@@ -398,8 +398,9 @@ do with it:
|
|
|
398
398
|
| `nobody` | A human wrote without naming any agent | Read it as context, stay quiet |
|
|
399
399
|
|
|
400
400
|
Slack has no real mention for an agent, so `@grkn` is ordinary text that
|
|
401
|
-
agent-wire looks for itself.
|
|
402
|
-
|
|
401
|
+
agent-wire looks for itself. `*grkn` calls it too, since that is the marker the
|
|
402
|
+
header uses. The match is literal and case-insensitive, and it stops at a word
|
|
403
|
+
boundary, so neither form fires for `@grknbyk`.
|
|
403
404
|
|
|
404
405
|
Nothing is filtered by this. Every message still arrives, still goes in the
|
|
405
406
|
inbox, and is still readable. It only decides who speaks first. Your own
|
|
@@ -411,11 +412,23 @@ Every message carries a handle at the right edge of its header line, padded to
|
|
|
411
412
|
column 60 so a scrolled channel has one straight edge to read down:
|
|
412
413
|
|
|
413
414
|
```
|
|
414
|
-
🔥 grkn => sinan
|
|
415
|
-
🚀 hakan-akduman =>
|
|
416
|
-
🛰️ mehmet-emin-kaya =>
|
|
415
|
+
🔥 grkn => @sinan wms-agents@k7m2pq
|
|
416
|
+
🚀 hakan-akduman => +Sinan wms-agents@zpbxdf
|
|
417
|
+
🛰️ mehmet-emin-kaya => all wms-agents@8g88zm
|
|
417
418
|
```
|
|
418
419
|
|
|
420
|
+
The recipient carries the same two markers the status panel uses: `@` for an
|
|
421
|
+
agent, `+` for a person. `@` is the character you already type to call an agent,
|
|
422
|
+
so it means the same thing in both places. People get `+` because Slack's
|
|
423
|
+
markdown gives it no meaning, while `*` would open a bold run and `~` a struck
|
|
424
|
+
one on a line the sender does not control.
|
|
425
|
+
|
|
426
|
+
One name often belongs to both, the agent `sinan` and the colleague Sinan, and
|
|
427
|
+
without the marker you cannot tell which one a line was addressed to. A name that
|
|
428
|
+
is neither a pinned agent nor a resolved Slack user stays bare, because a marker
|
|
429
|
+
there would be a guess. The sender is never marked: every one of these lines was
|
|
430
|
+
written by an agent.
|
|
431
|
+
|
|
419
432
|
To point your agent at one line you scrolled past, say "wms-agents@k7m2pq oku".
|
|
420
433
|
It fetches that message whatever channel it came from and whether it was already
|
|
421
434
|
read, which saves copying a Slack timestamp.
|
package/bin/agent-wire.mjs
CHANGED
|
@@ -22,6 +22,7 @@ const USAGE = `agent-wire — message other AI coding agents through Slack
|
|
|
22
22
|
agent-wire off <name> say nothing about it in this session
|
|
23
23
|
agent-wire version print the installed version, which is what a bug report needs
|
|
24
24
|
agent-wire update install the newest published version, cache and all
|
|
25
|
+
agent-wire help print this
|
|
25
26
|
|
|
26
27
|
The three modes belong to one session, identified by the client's session id when
|
|
27
28
|
it publishes one and by the working directory otherwise. The token, the nickname
|
|
@@ -201,8 +202,18 @@ const commands = {
|
|
|
201
202
|
version: showVersion,
|
|
202
203
|
'--version': showVersion,
|
|
203
204
|
'-v': showVersion,
|
|
205
|
+
// Asking for the usage text is not a mistake, so it prints and exits 0. The
|
|
206
|
+
// same text after a wrong command is a complaint and exits 1.
|
|
207
|
+
help: showUsage,
|
|
208
|
+
'--help': showUsage,
|
|
209
|
+
'-h': showUsage,
|
|
204
210
|
};
|
|
205
211
|
|
|
212
|
+
function showUsage() {
|
|
213
|
+
console.log(USAGE);
|
|
214
|
+
return 0;
|
|
215
|
+
}
|
|
216
|
+
|
|
206
217
|
function notConfigured() {
|
|
207
218
|
console.log('not configured yet — run `agent-wire setup`');
|
|
208
219
|
return 1;
|
package/package.json
CHANGED
package/src/mcp.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
7
7
|
import { tmpdir } from 'node:os';
|
|
8
8
|
import { dirname, join } from 'node:path';
|
|
9
9
|
|
|
10
|
-
import { MODES, activeChannels, channelMode, findChannel, loadConfig, paths, pollableChannels, scopeId } from './config.mjs';
|
|
10
|
+
import { MODES, activeChannels, channelMode, findChannel, loadConfig, paths, pollableChannels, readJson, scopeId } from './config.mjs';
|
|
11
11
|
import { DEFAULT_COUNT, appendMessages, archive, findByRef, findByTs, markRead, readCursor, selectMessages, writeCursor } from './inbox.mjs';
|
|
12
12
|
import { FINGERPRINT_CHARS, listPeers, signMessage } from './identity.mjs';
|
|
13
13
|
import { refusalFor } from './manners.mjs';
|
|
@@ -249,6 +249,21 @@ function chainOf(replyTo) {
|
|
|
249
249
|
return { conv: parent.conv ?? parent.ts, hop: (Number(parent.hop) || 1) + 1 };
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
+
// A name is an agent once a key has been pinned to it, a human once Slack has
|
|
253
|
+
// resolved it under that name, and unmarked while it is neither. The agent
|
|
254
|
+
// "sinan" and the person Sinan share a name here, so the header has to say which
|
|
255
|
+
// one a message was addressed to.
|
|
256
|
+
function recipientKind(config, to) {
|
|
257
|
+
if (to === 'all') return 'all';
|
|
258
|
+
|
|
259
|
+
const wanted = String(to).toLowerCase();
|
|
260
|
+
if (wanted === String(config.nickname).toLowerCase()) return 'agent';
|
|
261
|
+
if (listPeers().some((peer) => peer.name.toLowerCase() === wanted)) return 'agent';
|
|
262
|
+
|
|
263
|
+
const humans = Object.values(readJson(paths.users, {}));
|
|
264
|
+
return humans.some((name) => String(name).toLowerCase() === wanted) ? 'human' : 'unknown';
|
|
265
|
+
}
|
|
266
|
+
|
|
252
267
|
async function sendText(config, { to, text, channel, replyTo }) {
|
|
253
268
|
const target = findChannel(config, channel);
|
|
254
269
|
if (!target) return `no such channel: ${channel ?? '(none configured)'}`;
|
|
@@ -262,7 +277,9 @@ async function sendText(config, { to, text, channel, replyTo }) {
|
|
|
262
277
|
|
|
263
278
|
const client = slackClient(config.bot_token);
|
|
264
279
|
const ref = mintRef();
|
|
265
|
-
const rendered = formatMessage({
|
|
280
|
+
const rendered = formatMessage({
|
|
281
|
+
mark: config.mark, from: config.nickname, to, toKind: recipientKind(config, to), text, ref, channel: target.name,
|
|
282
|
+
});
|
|
266
283
|
const signature = signMessage(config.private_key, {
|
|
267
284
|
channel: target.id, from: config.nickname, to, conv: chain.conv, hop: chain.hop, text,
|
|
268
285
|
});
|
|
@@ -319,7 +336,9 @@ async function postFile(config, { to, path, note, target, chain, logText }) {
|
|
|
319
336
|
});
|
|
320
337
|
const posted = await postMessage(client, {
|
|
321
338
|
channel: target.id,
|
|
322
|
-
rendered: formatMessage({
|
|
339
|
+
rendered: formatMessage({
|
|
340
|
+
mark: config.mark, from: config.nickname, to, toKind: recipientKind(config, to), text, ref, channel: target.name,
|
|
341
|
+
}),
|
|
323
342
|
signature,
|
|
324
343
|
publicKey: config.public_key,
|
|
325
344
|
from: config.nickname,
|
package/src/protocol.mjs
CHANGED
|
@@ -68,8 +68,26 @@ export function displayWidth(text) {
|
|
|
68
68
|
// one line costs less than losing a character of somebody's name.
|
|
69
69
|
export const HEADER_WIDTH = 60;
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
// @ is the character a person types to call an agent, so it means agent here too
|
|
72
|
+
// and one name carries one meaning in both places. People get +, which Slack gives
|
|
73
|
+
// no markdown meaning: * would open a bold run and ~ a struck one, on a line the
|
|
74
|
+
// sender does not control.
|
|
75
|
+
//
|
|
76
|
+
// One name often belongs to both, the agent "sinan" and the colleague Sinan, and
|
|
77
|
+
// saying which one a message went to is the whole point of the marker.
|
|
78
|
+
//
|
|
79
|
+
// Only the recipient is marked. Every header line was written by an agent, so a
|
|
80
|
+
// marker on the sender would have been the one field that can never vary.
|
|
81
|
+
//
|
|
82
|
+
// A name nobody has placed stays bare. Guessing "human" for an unknown recipient
|
|
83
|
+
// would put the marker on exactly the messages it is least sure about.
|
|
84
|
+
const RECIPIENT_MARK = { agent: '@', human: '+' };
|
|
85
|
+
|
|
86
|
+
export const addressLine = ({ from, to, toKind }) =>
|
|
87
|
+
`${from} => ${to === 'all' ? 'all' : `${RECIPIENT_MARK[toKind] ?? ''}${to}`}`;
|
|
88
|
+
|
|
89
|
+
export function formatMessage({ mark, from, to, toKind, text, ref, channel }) {
|
|
90
|
+
const left = `${mark ? `${mark} ` : ''}${addressLine({ from, to, toKind })}`;
|
|
73
91
|
if (!ref) return `${left}\n${toSlackText(text)}\n`;
|
|
74
92
|
|
|
75
93
|
// Shipped once without this: a call site forgot the channel, the handle went out
|
|
@@ -81,8 +99,10 @@ export function formatMessage({ mark, from, to, text, ref, channel }) {
|
|
|
81
99
|
return `${left}${' '.repeat(gap)}${handle}\n${toSlackText(text)}\n`;
|
|
82
100
|
}
|
|
83
101
|
|
|
84
|
-
//
|
|
85
|
-
|
|
102
|
+
// A sender is never marked now, but 0.13.3 marked it with a *, so the pattern
|
|
103
|
+
// still allows one, and the recipient still accepts * for the same reason. Markers
|
|
104
|
+
// are stripped rather than kept: routing reads the signed payload, not this line.
|
|
105
|
+
const HEADER = /^(?:(?<mark>\S+)\s+)?\*?(?<from>[^\s=*]+)\s*=>\s*[@*+]?(?<to>\S+?)(?:\s+(?<refChannel>[a-z0-9][\w.-]*)?@(?<ref>[a-z2-9]{4,12}))?$/;
|
|
86
106
|
|
|
87
107
|
export function parseMessage(raw) {
|
|
88
108
|
const lines = fromSlackText(String(raw ?? '').replace(/\r\n/g, '\n')).trim().split('\n');
|
|
@@ -137,10 +157,12 @@ export function addressee(item, myNickname) {
|
|
|
137
157
|
}
|
|
138
158
|
|
|
139
159
|
const text = String(item.text).toLowerCase();
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
160
|
+
for (const prefix of ['@', '*']) {
|
|
161
|
+
const tag = `${prefix}${myNickname.toLowerCase()}`;
|
|
162
|
+
for (let at = text.indexOf(tag); at !== -1; at = text.indexOf(tag, at + 1)) {
|
|
163
|
+
const after = text[at + tag.length];
|
|
164
|
+
if (after === undefined || !CONTINUES.test(after)) return 'you';
|
|
165
|
+
}
|
|
144
166
|
}
|
|
145
167
|
return 'nobody';
|
|
146
168
|
}
|
package/src/slack.mjs
CHANGED
|
@@ -317,6 +317,11 @@ export async function pollChannel(client, channel, { since, myNickname }) {
|
|
|
317
317
|
const history = await client.form('conversations.history', {
|
|
318
318
|
channel: channel.id,
|
|
319
319
|
limit: PAGE_LIMIT,
|
|
320
|
+
// Without this Slack returns no metadata at all, so every message
|
|
321
|
+
// another agent signed arrives looking like a bare bot post and is
|
|
322
|
+
// dropped two lines below. Humans were unaffected, which is why the
|
|
323
|
+
// log looked healthy while no peer was ever seen.
|
|
324
|
+
include_all_metadata: true,
|
|
320
325
|
...(since ? { oldest: since } : {}),
|
|
321
326
|
...(cursor ? { cursor } : {}),
|
|
322
327
|
});
|
package/src/status.mjs
CHANGED
|
@@ -109,7 +109,8 @@ function unreadByChannel() {
|
|
|
109
109
|
// reader already associates with a machine and a person, and `!` is the one that
|
|
110
110
|
// stops the eye. All three are ASCII, so no terminal draws them double-width and
|
|
111
111
|
// tips a row over its border.
|
|
112
|
-
|
|
112
|
+
// The same two the header uses: @ calls an agent, + is a person.
|
|
113
|
+
const peerMark = (peer) => (peer.everForged ? '!' : peer.kind === 'human' ? '+' : '@');
|
|
113
114
|
|
|
114
115
|
export function renderStatus(config) {
|
|
115
116
|
const counts = unreadByChannel();
|