@grknbyk/agent-wire 0.8.1 → 0.8.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 CHANGED
@@ -30,14 +30,28 @@ got it, rewriting the report query
30
30
  ## Install
31
31
 
32
32
  ```bash
33
- npx @grknbyk/agent-wire setup
33
+ npm i -g @grknbyk/agent-wire
34
+ agent-wire setup
34
35
  ```
35
36
 
37
+ Run `setup` in a real terminal window. It asks questions, so it refuses a pipe, a
38
+ script, and an editor task — an agent that tries to run it from a tool gets a
39
+ one-line refusal and usually tells you the wrong thing about why.
40
+
36
41
  Setup prints the path of the bundled `manifest.json`. You create the app from it
37
42
  at [api.slack.com/apps/new](https://api.slack.com/apps/new), install it, and paste
38
43
  the Bot User OAuth Token back. Then you create the channel in Slack and type
39
44
  `/invite @agent-wire` in it.
40
45
 
46
+ **Give every install its own nickname.** The first key seen under a name is pinned
47
+ to it, so a second install answering to the same name is reported as `impostor` by
48
+ everyone who already heard from the first — and a forged sighting stays on the
49
+ record even after a later message verifies.
50
+
51
+ The whole team shares one Slack app and one bot token. Only the first person
52
+ creates the app; everybody after that pastes the same token and picks their own
53
+ name, and nobody needs to invite the bot again.
54
+
41
55
  Setup never asks which channel. The invite is the answer: whatever the bot has
42
56
  been added to, public or private, is what it works in. Invite it somewhere new and
43
57
  `agent-wire doctor` picks the channel up on the next run.
@@ -54,19 +68,43 @@ the first unfinished step, because the config file is the progress.
54
68
  Then point your client at it:
55
69
 
56
70
  ```bash
57
- claude mcp add agent-wire -- npx -y @grknbyk/agent-wire serve
71
+ claude mcp add -s user agent-wire -- agent-wire serve
58
72
  ```
59
73
 
74
+ `-s user` registers it once for every project. The modes are per session anyway,
75
+ so a per-project registration only means adding it again in the next folder.
76
+
60
77
  Or, for any other MCP client:
61
78
 
62
79
  ```json
63
80
  {
64
81
  "mcpServers": {
65
- "agent-wire": { "command": "npx", "args": ["-y", "@grknbyk/agent-wire", "serve"] }
82
+ "agent-wire": { "command": "agent-wire", "args": ["serve"] }
66
83
  }
67
84
  }
68
85
  ```
69
86
 
87
+ ### Without installing it
88
+
89
+ Every command works through `npx` instead, which is the way to try it before
90
+ putting a binary on the machine:
91
+
92
+ ```bash
93
+ npx @grknbyk/agent-wire setup
94
+ claude mcp add -s user agent-wire -- npx -y @grknbyk/agent-wire serve
95
+ ```
96
+
97
+ Nothing behaves differently. It is slower: measured at 800 ms against 215 ms for
98
+ the global binary, on a warm cache, and the difference is paid on every call. The
99
+ prompt hook runs on every prompt, so that is where it is felt.
100
+
101
+ `npx` also serves whatever it has cached. Add `@latest` when a version you just
102
+ published does not show up.
103
+
104
+ An install that reports an old version after `npm i -g` is reading the same stale
105
+ cache rather than a publish that failed: `npm cache clean --force`, then install
106
+ again.
107
+
70
108
  ## Commands
71
109
 
72
110
  | Command | What it does |
@@ -83,7 +121,13 @@ Or, for any other MCP client:
83
121
 
84
122
  ## Tools your agent gets
85
123
 
86
- `send`, `send_file`, `inbox`, `archive`, `peers`, `members`, `channels`, `my_id`.
124
+ `send`, `send_file`, `inbox`, `archive`, `peers`, `members`, `channels`, `my_id`,
125
+ `status`.
126
+
127
+ `status` returns the same card the CLI draws, already fenced. It exists because a
128
+ shell result gets read, understood and then retyped as prose, and a drawn box does
129
+ not survive that — two installs reporting the same state should not produce two
130
+ different-looking answers.
87
131
 
88
132
  The mode of a channel is a command the user runs, never a tool. A message
89
133
  arriving from the channel must not be able to talk the agent into silencing
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grknbyk/agent-wire",
3
- "version": "0.8.1",
3
+ "version": "0.8.3",
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/inbox.mjs CHANGED
@@ -38,7 +38,12 @@ export function readInbox() {
38
38
  // per appended message is the whole cost of appending a message.
39
39
  const inboxKeys = () => derivedFromFile(paths.inbox, 'keys', () => new Set(readInbox().map(logKey)));
40
40
 
41
- export const stateOf = (states, item) => states[storageKey(item)] ?? 'unread';
41
+ // A message this agent sent is kept in the log so the conversation reads back
42
+ // whole, but it was never waiting on anybody. Left as unread it joined the tally
43
+ // of who needs an answer, and the agent was told ten people were waiting when one
44
+ // of them was itself.
45
+ export const stateOf = (states, item) =>
46
+ (item.authorship === 'self' ? 'read' : states[storageKey(item)] ?? 'unread');
42
47
 
43
48
  // The Slack timestamp is the idempotency key: a retried poll, an overlapping
44
49
  // window, or a re-installed app all replay the same ts, and a duplicate the human
package/src/mcp.mjs CHANGED
@@ -64,6 +64,11 @@ const TOOLS = [
64
64
  description: 'This agent\'s nickname, emoji, key fingerprint and channels.',
65
65
  inputSchema: { type: 'object', properties: {} },
66
66
  },
67
+ {
68
+ name: 'status',
69
+ description: 'The status card: identity, channels with their modes, who has written, and when the last poll ran. Print what this returns exactly as it arrives, inside a code block. It is a drawn box, so retyping the fields loses it.',
70
+ inputSchema: { type: 'object', properties: {} },
71
+ },
67
72
  {
68
73
  name: 'peers',
69
74
  description: 'Agent names seen in the channels so far, with the key pinned to each.',
@@ -154,11 +159,26 @@ const MODE_SUMMARY = {
154
159
  read: 'the messages themselves, in every prompt',
155
160
  };
156
161
 
157
- const PROMPTS = MODES.map((mode) => ({
158
- name: mode,
159
- description: `Set a channel to ${mode} for this session — ${MODE_SUMMARY[mode]}`,
160
- arguments: [{ name: 'channel', description: 'Channel name. Omit it when only one is configured.', required: false }],
161
- }));
162
+ const PROMPTS = [
163
+ ...MODES.map((mode) => ({
164
+ name: mode,
165
+ description: `Set a channel to ${mode} for this session ${MODE_SUMMARY[mode]}`,
166
+ arguments: [{ name: 'channel', description: 'Channel name. Omit it when only one is configured.', required: false }],
167
+ })),
168
+ { name: 'status', description: 'Show the agent-wire status card', arguments: [] },
169
+ ];
170
+
171
+ const STATUS_INSTRUCTION = {
172
+ description: 'Show the agent-wire status card',
173
+ messages: [{
174
+ role: 'user',
175
+ content: {
176
+ type: 'text',
177
+ text: 'Call the agent-wire status tool and print what it returns verbatim, inside a code block.'
178
+ + ' Do not summarise it, do not retype the fields, do not reformat the box. The drawing is the answer.',
179
+ },
180
+ }],
181
+ };
162
182
 
163
183
  function modeInstruction(mode, channel) {
164
184
  const command = `agent-wire ${mode}${channel ? ` ${channel}` : ''}`;
@@ -326,6 +346,15 @@ async function call(name, args, session) {
326
346
  // refusal and tells the user the wrong thing.
327
347
  if (!config) return 'agent-wire is not configured yet. Tell the user to run `agent-wire setup` in a real terminal window — it asks questions, so it will not run from a tool. Install it first with `npm i -g @grknbyk/agent-wire` if the command is missing.';
328
348
 
349
+ // The card reaches the user through a tool rather than a shell, because a
350
+ // shell result gets read, understood and then retyped as prose — and the box
351
+ // does not survive that. Fenced here so it arrives ready to pass on.
352
+ if (name === 'status') {
353
+ const { renderStatus } = await import('./status.mjs');
354
+ return 'Show this to the user exactly as it is, in a code block. Do not summarise it and do not retype the numbers.\n\n'
355
+ + `\`\`\`\n${renderStatus(config).trim()}\n\`\`\``;
356
+ }
357
+
329
358
  if (name === 'my_id') {
330
359
  const channels = (config.channels ?? []).map((channel) => `#${channel.name}`).join(', ') || 'none';
331
360
  return `${config.mark} ${config.nickname} — key ${config.public_key.slice(0, FINGERPRINT_CHARS)}… — channels: ${channels}`;
@@ -430,7 +459,10 @@ export function serve() {
430
459
  if (message.method === 'prompts/get') {
431
460
  const asked = PROMPTS.find((prompt) => prompt.name === message.params.name);
432
461
  if (!asked) return write({ jsonrpc: '2.0', id: message.id, error: { code: -32602, message: `no prompt named ${message.params.name}` } });
433
- return write({ jsonrpc: '2.0', id: message.id, result: modeInstruction(asked.name, message.params.arguments?.channel) });
462
+ const answer = asked.name === 'status'
463
+ ? STATUS_INSTRUCTION
464
+ : modeInstruction(asked.name, message.params.arguments?.channel);
465
+ return write({ jsonrpc: '2.0', id: message.id, result: answer });
434
466
  }
435
467
  if (message.method === 'ping') return write({ jsonrpc: '2.0', id: message.id, result: {} });
436
468
  if (message.method === 'tools/call') {