@grknbyk/agent-wire 0.8.0 → 0.8.1

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
@@ -136,7 +136,7 @@ to the machine:
136
136
  `ask` looks like this, and is what a prompt hook prints:
137
137
 
138
138
  ```
139
- Unread messages : Huso(5), Sinan(2)
139
+ Unread messages : mira(5), kai(2)
140
140
  ```
141
141
 
142
142
  Loudest sender first, because five messages from one person is a conversation
@@ -20,6 +20,7 @@ const USAGE = `agent-wire — message other AI coding agents through Slack
20
20
  agent-wire ask <name> name who is waiting and how many; open nothing (default)
21
21
  agent-wire read <name> put the messages themselves into every prompt
22
22
  agent-wire off <name> say nothing about it in this session
23
+ agent-wire version print the installed version, which is what a bug report needs
23
24
 
24
25
  The three modes belong to one session, identified by the client's session id when
25
26
  it publishes one and by the working directory otherwise. The token, the nickname
@@ -113,6 +114,19 @@ function switchChannel(name, mode) {
113
114
 
114
115
  const showStatus = async () => (await import('../src/status.mjs')).runStatus();
115
116
 
117
+ // Everybody types one of these three before they report anything, so all three
118
+ // answer. package.json is read here rather than imported at the top because
119
+ // `drain` runs on every prompt and never needs it.
120
+ async function showVersion() {
121
+ const { readFileSync } = await import('node:fs');
122
+ const { fileURLToPath } = await import('node:url');
123
+ const { dirname, join } = await import('node:path');
124
+
125
+ const packageJson = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
126
+ console.log(JSON.parse(readFileSync(packageJson, 'utf8')).version);
127
+ return 0;
128
+ }
129
+
116
130
  const commands = {
117
131
  status: async () => await showStatus() ?? notConfigured(),
118
132
  setup: async () => (await import('../src/setup.mjs')).runSetup(),
@@ -125,6 +139,9 @@ const commands = {
125
139
  // `on` was the only way to undo `off` before there were three modes, and it
126
140
  // meant "announce it without opening it". That is ask.
127
141
  on: () => switchChannel(process.argv[3], 'ask'),
142
+ version: showVersion,
143
+ '--version': showVersion,
144
+ '-v': showVersion,
128
145
  };
129
146
 
130
147
  function notConfigured() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grknbyk/agent-wire",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
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/drain.mjs CHANGED
@@ -9,7 +9,7 @@ import { renderEnvelope } from './protocol.mjs';
9
9
  // has to scroll is a hook line the user stops reading.
10
10
  const SENDERS_SHOWN = 5;
11
11
 
12
- // "Huso(5), Sinan(2)", loudest first, so the name that matters is the first thing
12
+ // "mira(5), kai(2)", loudest first, so the name that matters is the first thing
13
13
  // on the line. The count per person is the whole point: five messages from one
14
14
  // person is a conversation waiting, one each from five people is a standup.
15
15
  export function senderTally(items) {
package/src/inbox.mjs CHANGED
@@ -80,10 +80,65 @@ export function selectMessages({ state = 'unread', count = DEFAULT_COUNT, channe
80
80
  return picked.reverse();
81
81
  }
82
82
 
83
+ // A session id is minted per client session, so read state stored under one is
84
+ // unreachable the moment that session ends — no later process ever asks under
85
+ // that key again. Left alone the file grows by one key per message per session:
86
+ // measured at 14 MB and 79 ms per mark after a thousand sessions, and marking is
87
+ // something a `read` session does on every prompt.
88
+ //
89
+ // Whole scopes go, oldest first, ranked by the newest message each one has seen.
90
+ // A session still running has recent timestamps and survives; only the dead ones
91
+ // are cheap enough to lose. The current scope is never a candidate.
92
+ const STATE_KEYS_MAX = 8000;
93
+ const STATE_KEYS_KEEP = 6000;
94
+
95
+ const scopeOfKey = (key) => {
96
+ const bar = key.indexOf('|');
97
+ return bar === -1 ? '' : key.slice(0, bar); // written before scopes existed
98
+ };
99
+
100
+ function prunedStates(states) {
101
+ const keys = Object.keys(states);
102
+ if (keys.length <= STATE_KEYS_MAX) return states;
103
+
104
+ // One session that has simply read a lot owns every key here, and none of them
105
+ // can be dropped. Counting them costs a startsWith per key and no allocation,
106
+ // where grouping by scope costs a substring per key: 20k keys went from 0.7 ms
107
+ // to 13 ms of scanning that always freed nothing.
108
+ const mine = `${scopeId()}|`;
109
+ let foreign = 0;
110
+ for (const key of keys) if (!key.startsWith(mine)) foreign++;
111
+ if (foreign === 0) return states;
112
+
113
+ const keysByScope = new Map();
114
+ const newestByScope = new Map();
115
+ for (const key of keys) {
116
+ const scope = scopeOfKey(key);
117
+ const timestamp = Number(key.slice(key.lastIndexOf(':') + 1)) || 0;
118
+ if (!keysByScope.has(scope)) keysByScope.set(scope, []);
119
+ keysByScope.get(scope).push(key);
120
+ if (timestamp > (newestByScope.get(scope) ?? 0)) newestByScope.set(scope, timestamp);
121
+ }
122
+
123
+ const stale = [...newestByScope]
124
+ .filter(([scope]) => scope !== scopeId())
125
+ .sort(([, left], [, right]) => left - right);
126
+
127
+ // Down to the low mark rather than to the cap, or the next write is over it
128
+ // again and pays for the whole scan a second time.
129
+ let remaining = keys.length;
130
+ for (const [scope] of stale) {
131
+ if (remaining <= STATE_KEYS_KEEP) break;
132
+ for (const key of keysByScope.get(scope)) delete states[key];
133
+ remaining -= keysByScope.get(scope).length;
134
+ }
135
+ return states;
136
+ }
137
+
83
138
  export function markRead(items) {
84
139
  const states = readJsonCached(paths.states, {});
85
140
  for (const item of items) states[storageKey(item)] = 'read';
86
- writeJson(paths.states, states);
141
+ writeJson(paths.states, prunedStates(states));
87
142
  }
88
143
 
89
144
  export function archive(ts) {
@@ -92,7 +147,7 @@ export function archive(ts) {
92
147
  ? readInbox().filter((item) => item.ts === ts)
93
148
  : readInbox().filter((item) => stateOf(states, item) === 'read');
94
149
  for (const item of targets) states[storageKey(item)] = 'archived';
95
- writeJson(paths.states, states);
150
+ writeJson(paths.states, prunedStates(states));
96
151
  return targets.length;
97
152
  }
98
153
 
package/src/mcp.mjs CHANGED
@@ -321,7 +321,10 @@ async function call(name, args, session) {
321
321
  if (missing.length) return `missing or empty: ${missing.join(', ')}`;
322
322
 
323
323
  const config = loadConfig();
324
- if (!config) return 'agent-wire is not configured yet run `npx @grknbyk/agent-wire setup`';
324
+ // Said to an agent, which will pass it on. Naming the terminal matters: setup
325
+ // refuses a pipe, so an agent that tries to run it from a tool gets a bare
326
+ // refusal and tells the user the wrong thing.
327
+ 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.';
325
328
 
326
329
  if (name === 'my_id') {
327
330
  const channels = (config.channels ?? []).map((channel) => `#${channel.name}`).join(', ') || 'none';
package/src/setup.mjs CHANGED
@@ -169,7 +169,7 @@ export async function runSetup() {
169
169
  export async function runDoctor() {
170
170
  const config = loadConfig();
171
171
  if (!config?.bot_token) {
172
- console.log('not configured — run `npx @grknbyk/agent-wire setup`');
172
+ console.log('not configured yet — run `agent-wire setup`');
173
173
  return 1;
174
174
  }
175
175