@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 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
- @ Zoë * kai * mira │
258
- * warehouse-… * robin ! nox │
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: `*` for an agent,
265
- `@` for a human typing in the channel, `!` for a name that has been forged.
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. The match is literal and case-insensitive, and it
402
- stops at a word boundary, so `@grkn` does not fire for `@grknbyk`.
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 wms-agents@k7m2pq
415
- 🚀 hakan-akduman => all wms-agents@zpbxdf
416
- 🛰️ mehmet-emin-kaya => hakan-akduman wms-agents@8g88zm
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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grknbyk/agent-wire",
3
- "version": "0.13.2",
3
+ "version": "0.13.4",
4
4
  "description": "Let AI coding agents message each other through a shared Slack channel, over MCP.",
5
5
  "type": "module",
6
6
  "license": "MIT",
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({ mark: config.mark, from: config.nickname, to, text, ref, channel: target.name });
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({ mark: config.mark, from: config.nickname, to, text, ref, channel: target.name }),
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
- export function formatMessage({ mark, from, to, text, ref, channel }) {
72
- const left = `${mark ? `${mark} ` : ''}${from} => ${to}`;
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
- // Rejects "*" as a sender so a bold-wrapped line cannot file a message under "*".
85
- const HEADER = /^(?:(?<mark>\S+)\s+)?(?<from>[^\s=*]+)\s*=>\s*(?<to>\S+?)(?:\s+(?<refChannel>[a-z0-9][\w.-]*)?@(?<ref>[a-z2-9]{4,12}))?$/;
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 tag = `@${myNickname.toLowerCase()}`;
141
- for (let at = text.indexOf(tag); at !== -1; at = text.indexOf(tag, at + 1)) {
142
- const after = text[at + tag.length];
143
- if (after === undefined || !CONTINUES.test(after)) return 'you';
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
- const peerMark = (peer) => (peer.everForged ? '!' : peer.kind === 'human' ? '@' : '*');
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();