benignswarm 0.7.0 → 0.7.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.
@@ -151,6 +151,8 @@ export async function read(p, out) {
151
151
  qs.set("limit", str(p.flags, "limit") ?? "100");
152
152
  const r = await client.call("GET", `/projects/${project}/channels/${channel}/messages?${qs}`);
153
153
  const text = r.messages.length ? r.messages.map(fmtMessage).join("\n") : "(no messages)";
154
+ const st = readState(creds.agent);
155
+ writeState(creds.agent, { ...st, channel_read: { ...(st.channel_read ?? {}), [`${project}/${channel}`]: r.cursor } });
154
156
  out.print(`${text}\ncursor: ${r.cursor}`, r);
155
157
  }
156
158
  function follow(client, path, out) {
@@ -270,3 +272,26 @@ export async function projects(p, out) {
270
272
  const list = r.scope === "account" ? r.projects : [r.project];
271
273
  out.print(list.map((x) => x.name).join("\n") || "(none)", { projects: list });
272
274
  }
275
+ /** Follow a channel: the hook then injects its new messages, not just a digest line. */
276
+ export async function followCmd(p, out) {
277
+ const channel = p.positionals[1];
278
+ const { creds, client } = agentClient(p);
279
+ const unfollow = p.positionals[0] === "unfollow";
280
+ const st = readState(creds.agent);
281
+ const follows = new Set(st.follows ?? []);
282
+ if (p.positionals[0] === "following" || !channel) {
283
+ out.print(follows.size ? [...follows].join("\n") : "(following no channels; the hook shows a digest line only)", { follows: [...follows] });
284
+ return;
285
+ }
286
+ const project = projectOf(p, creds);
287
+ const key = `${project}/${channel}`;
288
+ if (unfollow) {
289
+ follows.delete(key);
290
+ }
291
+ else {
292
+ await client.call("GET", `/projects/${project}/channels/${channel}/messages?limit=1`); // validates access
293
+ follows.add(key);
294
+ }
295
+ writeState(creds.agent, { ...st, follows: [...follows] });
296
+ out.print(`${unfollow ? "unfollowed" : "following"} #${channel} in ${project}`, { follows: [...follows] });
297
+ }
@@ -71,17 +71,65 @@ export async function hook(p) {
71
71
  const next = { ...state, last_check: Date.now() };
72
72
  if (messages.length)
73
73
  next.inbox_after = messages[messages.length - 1].created_at;
74
+ // Channel awareness: a digest line for unread channels, full bodies for followed ones.
75
+ const channelRead = { ...(state.channel_read ?? {}) };
76
+ const follows = new Set(state.follows ?? []);
77
+ const digest = [];
78
+ const followed = [];
79
+ try {
80
+ const client = Client.forAgent(creds, resolveModel(str(p.flags, "model"), process.env, process.cwd(), name));
81
+ const projects = creds.project ? [creds.project] : ((await client.call("GET", "/me")).projects ?? []).map((x) => x.name);
82
+ for (const project of projects) {
83
+ const { cursors } = await client.call("GET", `/projects/${project}/channels/cursors`);
84
+ for (const ch of cursors) {
85
+ const key = `${project}/${ch.name}`;
86
+ const last = channelRead[key];
87
+ if (last === undefined) {
88
+ // Never read on this machine: baseline to now rather than reporting all history as unread.
89
+ channelRead[key] = ch.cursor;
90
+ continue;
91
+ }
92
+ const unread = ch.cursor - last;
93
+ if (unread <= 0)
94
+ continue;
95
+ if (follows.has(key)) {
96
+ const r = await client.call("GET", `/projects/${project}/channels/${ch.name}/messages?since=${last}&limit=5`);
97
+ for (const m of r.messages)
98
+ followed.push(`${creds.project ? "" : `[${project}] `}${fmtMessage(m)}`);
99
+ const shown = r.messages.length ? r.messages[r.messages.length - 1].seq : last;
100
+ if (ch.cursor > shown)
101
+ followed.push(` (+${ch.cursor - shown} more in #${ch.name}: bschat read ${ch.name} --since ${shown})`);
102
+ channelRead[key] = shown;
103
+ }
104
+ else {
105
+ digest.push(`#${ch.name}${creds.project ? "" : ` [${project}]`}: ${unread} unread (bschat read ${ch.name} --since ${last})`);
106
+ }
107
+ }
108
+ }
109
+ }
110
+ catch (e) {
111
+ process.stderr.write(`bschat hook: channels: ${e.message}\n`);
112
+ }
113
+ next.channel_read = channelRead;
74
114
  writeState(name, next);
75
- if (!messages.length && !items.length)
115
+ if (!messages.length && !items.length && !digest.length && !followed.length)
76
116
  return;
77
117
  const lines = messages.map((m) => `${creds.project ? "" : `[${m.project}] `}${fmtMessage(m)}${m.reply_to_seq ? ` (reply to #${m.reply_to_seq})` : ""}`);
78
118
  const itemLines = items.map((i) => ` list "${i.list}"${i.project && !creds.project ? ` [${i.project}]` : ""}: [${i.status}] ${i.title} (item ${i.id}, by ${i.updated_by})`);
119
+ const head = [
120
+ messages.length ? `${messages.length} message${messages.length === 1 ? "" : "s"} for you` : null,
121
+ items.length ? `${items.length} list item${items.length === 1 ? "" : "s"} assigned to you` : null,
122
+ followed.length ? `new messages in followed channels` : null,
123
+ digest.length ? `unread channels` : null,
124
+ ].filter(Boolean).join(", ");
79
125
  const text = [
80
- `[bschat] ${messages.length} new message${messages.length === 1 ? "" : "s"}${items.length ? ` and ${items.length} list item${items.length === 1 ? "" : "s"} assigned to you` : ""} (${name}) on BenignSwarm:`,
126
+ `[bschat] ${head} (${name}) on BenignSwarm:`,
81
127
  ...lines,
82
128
  ...(items.length ? [`Assigned list items (bschat item show <id>; bschat item set <id> --status ...):`, ...itemLines] : []),
83
- `Reply with: bschat send ${creds.project ? "" : "--project <project> "}<channel> --reply-to <seq> "..." (or just continue if no answer is needed)`,
84
- ].join("\n");
129
+ ...(followed.length ? [`Followed channels:`, ...followed] : []),
130
+ ...(digest.length ? [`Unread elsewhere: ${digest.join("; ")}`] : []),
131
+ messages.length || followed.length ? `Reply with: bschat send ${creds.project ? "" : "--project <project> "}<channel> --reply-to <seq> "..." (or just continue if no answer is needed)` : null,
132
+ ].filter((l) => l !== null).join("\n");
85
133
  if (harness === "claude-code") {
86
134
  process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: event, additionalContext: text } }) + "\n");
87
135
  }
package/dist/help.js CHANGED
@@ -23,6 +23,8 @@ Agent commands
23
23
  bschat send <channel> <message...> [--reply-to <seq>] @name, @role:name, @role:* are resolved and validated
24
24
  bschat read <channel> [--since <seq>] [--limit n] [--follow]
25
25
  bschat inbox [--unread] [--after <iso-time>] [--limit n] mentions of you and replies to your messages
26
+ bschat follow <channel> | unfollow <channel> | following followed channels' new messages are injected by the hook;
27
+ other channels appear as a one-line unread digest
26
28
  bschat hook claude-code [--event E] harness hook: prints new inbox items as hook JSON (installed by join)
27
29
  bschat activity [--account] [--after <iso>] [--limit n] merged timeline: messages, status, evidence
28
30
  bschat projects projects you can access
package/dist/index.js CHANGED
@@ -37,6 +37,9 @@ const COMMANDS = {
37
37
  lists,
38
38
  list,
39
39
  item,
40
+ follow: a.followCmd,
41
+ unfollow: a.followCmd,
42
+ following: a.followCmd,
40
43
  admin,
41
44
  };
42
45
  async function main(argv) {
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const CLI_VERSION = "0.7.0";
1
+ export const CLI_VERSION = "0.7.1";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "benignswarm",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "bschat: chat, status board and evidence store for coding agents (benignswarm.org)",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {