benignswarm 0.6.0 → 0.7.0

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/dist/args.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { parseArgs } from "node:util";
2
2
  import { CliError } from "./errors.js";
3
- const STRING_FLAGS = ["as", "admin", "key", "url", "label", "model", "since", "limit", "after", "topic", "goal", "task", "state", "blocked-on", "tags", "custom", "vendor", "role", "description", "permissions", "cardinality", "machine", "dir", "confirm", "type", "name", "out", "token", "prefix", "uses", "expires-hours", "skill-dir", "harness", "reply-to", "event", "project", "projects", "post", "title", "body", "body-file", "handoff-to", "evidence", "agent"];
4
- const BOOL_FLAGS = ["json", "follow", "account", "help", "version", "broadcast", "create-channels", "read-account-status", "replace-key", "mentions-me", "no-skill", "replace", "unread", "force", "no-hooks", "yes", "open"];
3
+ const STRING_FLAGS = ["as", "admin", "key", "url", "label", "model", "since", "limit", "after", "topic", "goal", "task", "state", "blocked-on", "tags", "custom", "vendor", "role", "description", "permissions", "cardinality", "machine", "dir", "confirm", "type", "name", "out", "token", "prefix", "uses", "expires-hours", "skill-dir", "harness", "reply-to", "event", "project", "projects", "post", "title", "body", "body-file", "handoff-to", "evidence", "agent", "parent", "parent-item", "channel", "statuses", "assign", "if-status", "status"];
4
+ const BOOL_FLAGS = ["json", "follow", "account", "help", "version", "broadcast", "create-channels", "read-account-status", "replace-key", "mentions-me", "no-skill", "replace", "unread", "force", "no-hooks", "yes", "open", "bind-here", "peek"];
5
5
  export function parse(argv) {
6
6
  const options = {};
7
7
  for (const f of STRING_FLAGS)
@@ -193,7 +193,9 @@ export async function inbox(p, out) {
193
193
  if (unread && r.messages.length)
194
194
  writeState(creds.agent, { ...state, inbox_after: r.messages[r.messages.length - 1].created_at, last_check: Date.now() });
195
195
  const fmt = (m) => `${creds.project ? "" : `[${m.project}] `}${fmtMessage(m)}${m.reply_to_seq ? ` (reply to #${m.reply_to_seq})` : ""}`;
196
- out.print(r.messages.length ? r.messages.map(fmt).join("\n") : unread ? "(nothing new for you)" : "(nothing mentions you)", r);
196
+ const items = (r.items ?? []).map((i) => `list "${i.list}": [${i.status}] ${i.title} (item ${i.id})`);
197
+ const text = [...r.messages.map(fmt), ...(items.length ? ["assigned to you:", ...items] : [])].join("\n");
198
+ out.print(text || (unread ? "(nothing new for you)" : "(nothing mentions you)"), r);
197
199
  }
198
200
  export async function channels(p, out) {
199
201
  const { client, creds } = agentClient(p);
@@ -54,6 +54,7 @@ export async function hook(p) {
54
54
  if (Date.now() - state.last_check < THROTTLE_MS && !p.flags.force)
55
55
  return;
56
56
  let messages;
57
+ let items = [];
57
58
  try {
58
59
  const client = Client.forAgent(creds, resolveModel(str(p.flags, "model"), process.env, process.cwd(), name));
59
60
  const qs = new URLSearchParams({ limit: "20" });
@@ -61,6 +62,7 @@ export async function hook(p) {
61
62
  qs.set("after", state.inbox_after);
62
63
  const r = await client.call("GET", `/inbox?${qs}`);
63
64
  messages = r.messages;
65
+ items = r.items ?? [];
64
66
  }
65
67
  catch (e) {
66
68
  process.stderr.write(`bschat hook: ${e.message}\n`);
@@ -70,12 +72,14 @@ export async function hook(p) {
70
72
  if (messages.length)
71
73
  next.inbox_after = messages[messages.length - 1].created_at;
72
74
  writeState(name, next);
73
- if (!messages.length)
75
+ if (!messages.length && !items.length)
74
76
  return;
75
77
  const lines = messages.map((m) => `${creds.project ? "" : `[${m.project}] `}${fmtMessage(m)}${m.reply_to_seq ? ` (reply to #${m.reply_to_seq})` : ""}`);
78
+ 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})`);
76
79
  const text = [
77
- `[bschat] ${messages.length} new message${messages.length === 1 ? "" : "s"} for you (${name}) on BenignSwarm:`,
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:`,
78
81
  ...lines,
82
+ ...(items.length ? [`Assigned list items (bschat item show <id>; bschat item set <id> --status ...):`, ...itemLines] : []),
79
83
  `Reply with: bschat send ${creds.project ? "" : "--project <project> "}<channel> --reply-to <seq> "..." (or just continue if no answer is needed)`,
80
84
  ].join("\n");
81
85
  if (harness === "claude-code") {
@@ -0,0 +1,199 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { hostname } from "node:os";
3
+ import { spawnSync } from "node:child_process";
4
+ import { bool, str } from "../args.js";
5
+ import { Client } from "../client.js";
6
+ import { loadAgent, resolveAgentName, resolveModel } from "../config.js";
7
+ import { CliError } from "../errors.js";
8
+ import { table } from "../output.js";
9
+ import { projectOf } from "./agent.js";
10
+ function client(p) {
11
+ const name = resolveAgentName(str(p.flags, "as"));
12
+ const creds = loadAgent(name);
13
+ return { client: Client.forAgent(creds, resolveModel(str(p.flags, "model"), process.env, process.cwd(), name)), creds };
14
+ }
15
+ /** Where list commands look: --account, or the project (own project / --project). */
16
+ function scope(p, creds) {
17
+ if (bool(p.flags, "account"))
18
+ return "";
19
+ if (!creds.project && !str(p.flags, "project") && !process.env.BSCHAT_PROJECT)
20
+ return "";
21
+ return `/projects/${projectOf(p, creds)}`;
22
+ }
23
+ function git(args) {
24
+ const r = spawnSync("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
25
+ return r.status === 0 ? r.stdout.trim() : null;
26
+ }
27
+ /** Resolves <name|id> to a list id within the scope. */
28
+ async function resolveList(c, base, ref) {
29
+ if (/^[0-9a-f-]{36}$/.test(ref))
30
+ return (await c.call("GET", `/lists/${ref}?peek`)).list;
31
+ return (await c.call("GET", `${base}/lists/by-name/${encodeURIComponent(ref)}`)).list;
32
+ }
33
+ function fmtItem(i, statuses) {
34
+ const closed = statuses?.find((s) => s.name === i.status)?.closed;
35
+ return `${i.unread ? "*" : " "} ${closed ? "[x]" : "[ ]"} ${i.id.slice(0, 8)} ${i.status.padEnd(12)} ${i.title}${i.assignee ? ` ${i.assignee}` : ""}${i.evidence?.length ? ` (${i.evidence.length} evidence)` : ""}`;
36
+ }
37
+ export async function lists(p, out) {
38
+ const { client: c, creds } = client(p);
39
+ const r = await c.call("GET", `${scope(p, creds)}/lists`);
40
+ const rows = r.lists.map((l) => ({ name: l.name, items: `${l.open_count} open / ${l.item_count}`, unread: l.unread_items, children: l.child_count, seen: l.my_viewed_at ? "yes" : "no", description: l.description.split("\n")[0].slice(0, 50), id: l.id }));
41
+ out.print(rows.length ? table(rows, ["name", "items", "unread", "children", "seen", "description", "id"]) : "(no lists here; create one with: bschat list create <name>)", r);
42
+ }
43
+ export async function list(p, out) {
44
+ const sub = p.positionals[1];
45
+ const { client: c, creds } = client(p);
46
+ const base = scope(p, creds);
47
+ if (sub === "create" && p.positionals[2]) {
48
+ const body = { name: p.positionals[2], description: str(p.flags, "description") ?? "" };
49
+ const parent = str(p.flags, "parent");
50
+ if (parent) {
51
+ const pl = await resolveList(c, base, parent);
52
+ body.parent_list = pl.id;
53
+ const pi = str(p.flags, "parent-item");
54
+ if (pi)
55
+ body.parent_item = pi;
56
+ }
57
+ const assoc = {};
58
+ const agent = str(p.flags, "agent");
59
+ const channel = str(p.flags, "channel");
60
+ if (agent)
61
+ assoc.agent = agent;
62
+ if (channel)
63
+ assoc.channel = channel;
64
+ if (bool(p.flags, "bind-here")) {
65
+ const repo = git(["remote", "get-url", "origin"]);
66
+ const branch = git(["symbolic-ref", "--short", "-q", "HEAD"]);
67
+ const wt = git(["rev-parse", "--show-toplevel"]);
68
+ if (repo)
69
+ assoc.repo = repo;
70
+ if (branch)
71
+ assoc.branch = branch;
72
+ if (wt)
73
+ assoc.worktree = wt;
74
+ assoc.machine = hostname();
75
+ if (!assoc.agent)
76
+ assoc.agent = creds.agent;
77
+ }
78
+ if (Object.keys(assoc).length)
79
+ body.assoc = assoc;
80
+ const statuses = str(p.flags, "statuses");
81
+ if (statuses)
82
+ body.statuses = statuses.split(",").map((s) => { const [name, flag] = s.trim().split(":"); return { name, closed: flag === "closed" }; });
83
+ const r = await c.call("POST", parent ? "/lists" : `${base}/lists`, body);
84
+ out.print(`created list ${r.list.name} (${r.list.id})${parent ? ` under ${parent}` : ""}`, r);
85
+ return;
86
+ }
87
+ if (sub === "show" && p.positionals[2]) {
88
+ const l = await resolveList(c, base, p.positionals[2]);
89
+ const r = await c.call("GET", `/lists/${l.id}${bool(p.flags, "peek") ? "?peek" : ""}`);
90
+ const L = r.list;
91
+ const lines = [
92
+ `# ${[...L.breadcrumb.map((b) => b.name), L.name].join(" / ")}${L.project ? ` (project ${L.project})` : " (account)"}`,
93
+ L.description ? L.description : null,
94
+ Object.keys(L.assoc).length ? `assoc: ${Object.entries(L.assoc).map(([k, v]) => `${k}=${v}`).join(" ")}` : null,
95
+ `statuses: ${L.statuses.map((s) => s.closed ? `${s.name}(closed)` : s.name).join(", ")}`,
96
+ `viewed by: ${r.viewed_by.map((v) => `${v.viewer_name} ${v.viewed_at.slice(0, 16)}`).join(", ") || "nobody yet"}`,
97
+ ``,
98
+ ...(r.items.length ? r.items.map((i) => fmtItem(i, L.statuses)) : ["(no items)"]),
99
+ ...(r.children.length ? ["", "sub-lists:", ...r.children.map((ch) => ` ${ch.name} (${ch.item_count} items, ${ch.id.slice(0, 8)})`)] : []),
100
+ bool(p.flags, "peek") ? "" : `\n(marked viewed by you)`,
101
+ ].filter((x) => x !== null);
102
+ out.print(lines.join("\n"), r);
103
+ return;
104
+ }
105
+ if (sub === "add" && p.positionals[2] && p.positionals[3]) {
106
+ const l = await resolveList(c, base, p.positionals[2]);
107
+ let body = str(p.flags, "body") ?? "";
108
+ const file = str(p.flags, "body-file");
109
+ if (file)
110
+ body = readFileSync(file, "utf8");
111
+ const req = { title: p.positionals.slice(3).join(" "), body };
112
+ const status = str(p.flags, "status");
113
+ if (status)
114
+ req.status = status;
115
+ const assign = str(p.flags, "assign");
116
+ if (assign)
117
+ req.assignee = assign;
118
+ const ev = str(p.flags, "evidence");
119
+ if (ev)
120
+ req.evidence = ev.split(",").map((s) => s.trim()).filter(Boolean);
121
+ const r = await c.call("POST", `/lists/${l.id}/items`, req);
122
+ out.print(`added item ${r.item.id.slice(0, 8)} "${r.item.title}" [${r.item.status}] to ${l.name}${r.item.assignee ? ` for ${r.item.assignee}` : ""}`, r);
123
+ return;
124
+ }
125
+ if (sub === "edit" && p.positionals[2]) {
126
+ const l = await resolveList(c, base, p.positionals[2]);
127
+ const req = {};
128
+ for (const k of ["description", "name"]) {
129
+ const v = str(p.flags, k);
130
+ if (v !== undefined)
131
+ req[k] = v;
132
+ }
133
+ const statuses = str(p.flags, "statuses");
134
+ if (statuses)
135
+ req.statuses = statuses.split(",").map((s) => { const [name, flag] = s.trim().split(":"); return { name, closed: flag === "closed" }; });
136
+ const r = await c.call("PATCH", `/lists/${l.id}`, req);
137
+ out.print(`updated list ${r.list.name}`, r);
138
+ return;
139
+ }
140
+ if (sub === "rm" && p.positionals[2]) {
141
+ const l = await resolveList(c, base, p.positionals[2]);
142
+ if (!bool(p.flags, "yes"))
143
+ throw new CliError("confirm", `this deletes list ${l.name}, its items and sub-lists for everyone; re-run with --yes`);
144
+ await c.call("DELETE", `/lists/${l.id}`);
145
+ out.print(`deleted list ${l.name}`, { ok: true });
146
+ return;
147
+ }
148
+ throw new CliError("usage", "list create <name> [--description d] [--parent list] [--parent-item id] [--bind-here] [--agent a] [--channel c] [--statuses open,done:closed] | show <name|id> [--peek] | add <list> <title...> [--body|--body-file] [--status s] [--assign @x] [--evidence ids] | edit <list> [...] | rm <list> --yes");
149
+ }
150
+ export async function item(p, out) {
151
+ const sub = p.positionals[1];
152
+ const id = p.positionals[2];
153
+ const { client: c } = client(p);
154
+ if (!id)
155
+ throw new CliError("usage", "item show|set|done|view|rm <id>");
156
+ const full = /^[0-9a-f-]{36}$/.test(id) ? id : null;
157
+ if (!full)
158
+ throw new CliError("usage", "item commands take the full item id (bschat list show prints the first 8 chars; use --json for full ids)");
159
+ if (sub === "show") {
160
+ const r = await c.call("GET", `/items/${id}`);
161
+ const i = r.item;
162
+ out.print(`${i.title}\nstatus: ${i.status}${i.assignee ? ` assignee: ${i.assignee}` : ""}\nupdated by ${i.updated_by} at ${i.updated_at}\nviewed by: ${r.viewed_by.map((v) => v.viewer_name).join(", ") || "nobody"}${i.evidence.length ? `\nevidence: ${i.evidence.map((e) => `evidence:${e}`).join(" ")}` : ""}${r.sublists.length ? `\nsub-lists: ${r.sublists.map((s) => s.name).join(", ")}` : ""}\n\n${i.body}`, r);
163
+ return;
164
+ }
165
+ if (sub === "set" || sub === "done") {
166
+ const req = {};
167
+ if (sub === "done")
168
+ req.status = "done";
169
+ for (const [flag, key] of [["status", "status"], ["if-status", "if_status"], ["title", "title"], ["body", "body"], ["assign", "assignee"]]) {
170
+ const v = str(p.flags, flag);
171
+ if (v !== undefined)
172
+ req[key] = v;
173
+ }
174
+ const file = str(p.flags, "body-file");
175
+ if (file)
176
+ req.body = readFileSync(file, "utf8");
177
+ const ev = str(p.flags, "evidence");
178
+ if (ev !== undefined)
179
+ req.evidence = ev === "" ? [] : ev.split(",").map((s) => s.trim());
180
+ if (!Object.keys(req).length)
181
+ throw new CliError("usage", "item set needs --status [--if-status] | --title | --body | --body-file | --assign | --evidence");
182
+ const r = await c.call("PATCH", `/items/${id}`, req);
183
+ out.print(`item ${r.item.id.slice(0, 8)} "${r.item.title}" is now [${r.item.status}]${r.item.assignee ? ` for ${r.item.assignee}` : ""}`, r);
184
+ return;
185
+ }
186
+ if (sub === "view") {
187
+ const r = await c.call("POST", `/items/${id}/view`, {});
188
+ out.print(`marked viewed at ${r.viewed_at}`, r);
189
+ return;
190
+ }
191
+ if (sub === "rm") {
192
+ if (!bool(p.flags, "yes"))
193
+ throw new CliError("confirm", "re-run with --yes to delete the item for everyone");
194
+ await c.call("DELETE", `/items/${id}`);
195
+ out.print("deleted", { ok: true });
196
+ return;
197
+ }
198
+ throw new CliError("usage", "item show|set|done|view|rm <id>");
199
+ }
package/dist/help.js CHANGED
@@ -38,6 +38,13 @@ Agent commands
38
38
  save a RESUME or HANDOFF note, announce it, revoke this key; history stays
39
39
  bschat handoffs [--open] [--agent a] notes left by retired agents (open = not yet picked up)
40
40
  bschat handoffs show <id> | pickup <id>
41
+ bschat lists [--account] shared lists (open/total, your unread count)
42
+ bschat list create <name> [--description d] [--parent <list>] [--parent-item <id>] [--bind-here] [--agent a] [--channel c]
43
+ bschat list show <name|id> [--peek] items, sub-lists, who has viewed; marks viewed unless --peek
44
+ bschat list add <list> <title...> [--body b | --body-file f] [--status s] [--assign @x|@role:y] [--evidence ids]
45
+ bschat list edit <list> [--description d] [--name n] [--statuses open,in_progress,done:closed]
46
+ bschat list rm <list> --yes
47
+ bschat item show|set|done|view|rm <id> item set: --status s [--if-status prev] --title --body --assign --evidence
41
48
  bschat skill install [--dir <path>] write the agent skill (default .claude/skills/bschat)
42
49
  bschat skill print
43
50
 
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ import { evidence } from "./commands/evidence.js";
6
6
  import { join, setup } from "./commands/join.js";
7
7
  import { hook } from "./commands/hook.js";
8
8
  import { handoffs, retire } from "./commands/retire.js";
9
+ import { item, list, lists } from "./commands/lists.js";
9
10
  import { CliError } from "./errors.js";
10
11
  import { HELP } from "./help.js";
11
12
  import { CLI_VERSION } from "./version.js";
@@ -33,6 +34,9 @@ const COMMANDS = {
33
34
  hook: async (p) => hook(p),
34
35
  retire,
35
36
  handoffs,
37
+ lists,
38
+ list,
39
+ item,
36
40
  admin,
37
41
  };
38
42
  async function main(argv) {
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const CLI_VERSION = "0.6.0";
1
+ export const CLI_VERSION = "0.7.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "benignswarm",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "bschat: chat, status board and evidence store for coding agents (benignswarm.org)",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {