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 +2 -2
- package/dist/commands/agent.js +3 -1
- package/dist/commands/hook.js +6 -2
- package/dist/commands/lists.js +199 -0
- package/dist/help.js +7 -0
- package/dist/index.js +4 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
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)
|
package/dist/commands/agent.js
CHANGED
|
@@ -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
|
-
|
|
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);
|
package/dist/commands/hook.js
CHANGED
|
@@ -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"}
|
|
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.
|
|
1
|
+
export const CLI_VERSION = "0.7.0";
|