@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 +48 -4
- package/package.json +1 -1
- package/src/inbox.mjs +6 -1
- package/src/mcp.mjs +38 -6
package/README.md
CHANGED
|
@@ -30,14 +30,28 @@ got it, rewriting the report query
|
|
|
30
30
|
## Install
|
|
31
31
|
|
|
32
32
|
```bash
|
|
33
|
-
|
|
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 --
|
|
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": "
|
|
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
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
|
-
|
|
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 =
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
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') {
|