@grknbyk/agent-wire 0.13.2 → 0.13.3
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 +15 -7
- package/bin/agent-wire.mjs +11 -0
- package/package.json +1 -1
- package/src/mcp.mjs +22 -3
- package/src/protocol.mjs +27 -8
- package/src/slack.mjs +5 -0
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
|
|
|
@@ -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,18 @@ 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. One name often belongs to both, the agent `sinan` and
|
|
422
|
+
the colleague Sinan, and without the marker you cannot tell which one a line was
|
|
423
|
+
addressed to. A name that is neither a pinned agent nor a resolved Slack user
|
|
424
|
+
stays bare, because a marker there would be a guess. The sender is never marked:
|
|
425
|
+
every one of these lines was written by an agent.
|
|
426
|
+
|
|
419
427
|
To point your agent at one line you scrolled past, say "wms-agents@k7m2pq oku".
|
|
420
428
|
It fetches that message whatever channel it came from and whether it was already
|
|
421
429
|
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,23 @@ 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
|
+
// The status panel already reads * for an agent and @ for a human, so the header
|
|
72
|
+
// borrows the same two characters rather than inventing a third vocabulary. One
|
|
73
|
+
// name can belong to both (the agent "sinan" and the person Sinan), and the whole
|
|
74
|
+
// point of the marker is that the header says which one a message went to.
|
|
75
|
+
//
|
|
76
|
+
// Only the recipient is marked. Every header line was written by an agent, so a
|
|
77
|
+
// marker on the sender would have been the one field that can never vary.
|
|
78
|
+
//
|
|
79
|
+
// A name nobody has placed stays bare. Guessing "human" for an unknown recipient
|
|
80
|
+
// would put the marker on exactly the messages it is least sure about.
|
|
81
|
+
const RECIPIENT_MARK = { agent: '*', human: '@' };
|
|
82
|
+
|
|
83
|
+
export const addressLine = ({ from, to, toKind }) =>
|
|
84
|
+
`${from} => ${to === 'all' ? 'all' : `${RECIPIENT_MARK[toKind] ?? ''}${to}`}`;
|
|
85
|
+
|
|
86
|
+
export function formatMessage({ mark, from, to, toKind, text, ref, channel }) {
|
|
87
|
+
const left = `${mark ? `${mark} ` : ''}${addressLine({ from, to, toKind })}`;
|
|
73
88
|
if (!ref) return `${left}\n${toSlackText(text)}\n`;
|
|
74
89
|
|
|
75
90
|
// Shipped once without this: a call site forgot the channel, the handle went out
|
|
@@ -81,8 +96,10 @@ export function formatMessage({ mark, from, to, text, ref, channel }) {
|
|
|
81
96
|
return `${left}${' '.repeat(gap)}${handle}\n${toSlackText(text)}\n`;
|
|
82
97
|
}
|
|
83
98
|
|
|
84
|
-
//
|
|
85
|
-
|
|
99
|
+
// The leading * on a sender is ours; a *bold* line is not, which is why the name
|
|
100
|
+
// after it still cannot contain one. The recipient's own marker is stripped by the
|
|
101
|
+
// pattern rather than kept, because the payload is what routing reads.
|
|
102
|
+
const HEADER = /^(?:(?<mark>\S+)\s+)?\*?(?<from>[^\s=*]+)\s*=>\s*[@*]?(?<to>\S+?)(?:\s+(?<refChannel>[a-z0-9][\w.-]*)?@(?<ref>[a-z2-9]{4,12}))?$/;
|
|
86
103
|
|
|
87
104
|
export function parseMessage(raw) {
|
|
88
105
|
const lines = fromSlackText(String(raw ?? '').replace(/\r\n/g, '\n')).trim().split('\n');
|
|
@@ -137,10 +154,12 @@ export function addressee(item, myNickname) {
|
|
|
137
154
|
}
|
|
138
155
|
|
|
139
156
|
const text = String(item.text).toLowerCase();
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
157
|
+
for (const prefix of ['@', '*']) {
|
|
158
|
+
const tag = `${prefix}${myNickname.toLowerCase()}`;
|
|
159
|
+
for (let at = text.indexOf(tag); at !== -1; at = text.indexOf(tag, at + 1)) {
|
|
160
|
+
const after = text[at + tag.length];
|
|
161
|
+
if (after === undefined || !CONTINUES.test(after)) return 'you';
|
|
162
|
+
}
|
|
144
163
|
}
|
|
145
164
|
return 'nobody';
|
|
146
165
|
}
|
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
|
});
|