benignswarm 0.0.0-stage → 0.3.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 ADDED
@@ -0,0 +1,31 @@
1
+ import { parseArgs } from "node:util";
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"];
4
+ const BOOL_FLAGS = ["json", "follow", "account", "help", "version", "broadcast", "create-channels", "read-account-status", "replace-key", "mentions-me", "no-skill"];
5
+ export function parse(argv) {
6
+ const options = {};
7
+ for (const f of STRING_FLAGS)
8
+ options[f] = { type: "string" };
9
+ for (const f of BOOL_FLAGS)
10
+ options[f] = { type: "boolean" };
11
+ try {
12
+ const { values, positionals } = parseArgs({ args: argv, options, allowPositionals: true, strict: true });
13
+ return { positionals, flags: values };
14
+ }
15
+ catch (e) {
16
+ throw new CliError("usage", e.message, "Run: bschat --help");
17
+ }
18
+ }
19
+ export function str(flags, k) {
20
+ const v = flags[k];
21
+ return typeof v === "string" ? v : undefined;
22
+ }
23
+ export function bool(flags, k) {
24
+ return flags[k] === true;
25
+ }
26
+ export function need(flags, k) {
27
+ const v = str(flags, k);
28
+ if (v === undefined || v === "")
29
+ throw new CliError("usage", `--${k} is required`);
30
+ return v;
31
+ }
package/dist/client.js ADDED
@@ -0,0 +1,60 @@
1
+ import { CliError } from "./errors.js";
2
+ import { collectAuto } from "./git.js";
3
+ export class Client {
4
+ url;
5
+ key;
6
+ auto;
7
+ constructor(url, key, auto) {
8
+ this.url = url;
9
+ this.key = key;
10
+ this.auto = auto;
11
+ }
12
+ static forAgent(c, model) {
13
+ return new Client(c.url, c.key, { ...collectAuto({ model, machine: c.machine }) });
14
+ }
15
+ static forAdmin(c) {
16
+ return new Client(c.url, c.key, null);
17
+ }
18
+ static raw(url, key) {
19
+ return new Client(url, key, null);
20
+ }
21
+ headers() {
22
+ const h = { authorization: `Bearer ${this.key}` };
23
+ if (this.auto)
24
+ h["x-bschat-auto"] = JSON.stringify(this.auto);
25
+ return h;
26
+ }
27
+ async call(method, path, body) {
28
+ const headers = this.headers();
29
+ if (body !== undefined)
30
+ headers["content-type"] = "application/json";
31
+ let res;
32
+ try {
33
+ res = await fetch(this.url + path, { method, headers, body: body === undefined ? undefined : JSON.stringify(body) });
34
+ }
35
+ catch (e) {
36
+ throw new CliError("network", `could not reach ${this.url}: ${e.message}`);
37
+ }
38
+ const text = await res.text();
39
+ let json;
40
+ try {
41
+ json = JSON.parse(text);
42
+ }
43
+ catch {
44
+ throw new CliError("bad_response", `${method} ${path} returned non-JSON (${res.status}): ${text.slice(0, 200)}`);
45
+ }
46
+ if (!res.ok) {
47
+ const err = json;
48
+ throw new CliError(err.error ?? `http_${res.status}`, err.message ?? text);
49
+ }
50
+ return json;
51
+ }
52
+ wsUrl(path) {
53
+ return this.url.replace(/^http/, "ws") + path;
54
+ }
55
+ /** Node 22+ has a global WebSocket, but it cannot set headers; the token goes in the subprotocol. */
56
+ connect(path) {
57
+ const ws = new WebSocket(this.wsUrl(path), [`bschat-token.${this.key}`]);
58
+ return ws;
59
+ }
60
+ }
@@ -0,0 +1,187 @@
1
+ import { bool, need, str } from "../args.js";
2
+ import { Client } from "../client.js";
3
+ import { DEFAULT_URL, loadAdmin, resolveAdminName, saveAdmin } from "../config.js";
4
+ import { CliError } from "../errors.js";
5
+ import { table } from "../output.js";
6
+ function adminClient(p) {
7
+ return Client.forAdmin(loadAdmin(resolveAdminName(str(p.flags, "admin"))));
8
+ }
9
+ export async function admin(p, out) {
10
+ const [, group, action, ...rest] = p.positionals;
11
+ switch (group) {
12
+ case "login": {
13
+ const nick = str(p.flags, "as");
14
+ if (!nick)
15
+ throw new CliError("usage", "admin login --as <nickname> --key <bs_admin_...>");
16
+ const key = need(p.flags, "key");
17
+ if (!key.startsWith("bs_admin_"))
18
+ throw new CliError("usage", "admin login expects an admin key (bs_admin_...)");
19
+ const url = (str(p.flags, "url") ?? process.env.BSCHAT_URL ?? DEFAULT_URL).replace(/\/$/, "");
20
+ const me = await Client.raw(url, key).call("GET", "/me");
21
+ if (me.kind !== "admin")
22
+ throw new CliError("bad_key", "key is not an admin key");
23
+ const path = saveAdmin(nick, { url, key, account: me.account_id });
24
+ out.print(`Saved admin login ${nick} to ${path}\nNext: export BSCHAT_ADMIN=${nick}`, { saved: path });
25
+ return;
26
+ }
27
+ case "signup": {
28
+ const url = (str(p.flags, "url") ?? process.env.BSCHAT_URL ?? DEFAULT_URL).replace(/\/$/, "");
29
+ const code = need(p.flags, "key");
30
+ const [name, email] = [action, rest[0]];
31
+ if (!name || !email)
32
+ throw new CliError("usage", "admin signup --url <api> --key <signup-code> <account-name> <email>");
33
+ const res = await fetch(`${url}/accounts`, { method: "POST", headers: { "content-type": "application/json", "x-signup-code": code }, body: JSON.stringify({ name, email }) });
34
+ const json = await res.json();
35
+ if (!res.ok)
36
+ throw new CliError(json.error, json.message);
37
+ out.print(`Account ${json.account.name} created.\nAdmin key (shown once): ${json.admin_key}\nNext: bschat admin login --as ${name} --key ${json.admin_key} --url ${url}`, json);
38
+ return;
39
+ }
40
+ case "project":
41
+ return project(action, rest, p, out);
42
+ case "agent":
43
+ return agent(action, rest, p, out);
44
+ case "role":
45
+ return role(action, rest, p, out);
46
+ case "channel":
47
+ return channel(action, rest, p, out);
48
+ case "enroll":
49
+ return enroll(action, rest, p, out);
50
+ case "keys": {
51
+ const r = await adminClient(p).call("GET", "/account/keys");
52
+ out.print(table(r.keys, ["id", "kind", "prefix", "agent_name", "project_name", "machine", "created_at", "revoked_at"]), r);
53
+ return;
54
+ }
55
+ case "delete-account": {
56
+ const confirm = str(p.flags, "confirm");
57
+ if (confirm !== "delete everything")
58
+ throw new CliError("usage", 'admin delete-account --confirm "delete everything"');
59
+ const r = await adminClient(p).call("DELETE", "/account", { confirm });
60
+ out.print("account deleted", r);
61
+ return;
62
+ }
63
+ default:
64
+ throw new CliError("usage", `unknown admin command ${group ?? ""}`, "Run: bschat --help");
65
+ }
66
+ }
67
+ async function project(action, rest, p, out) {
68
+ const c = adminClient(p);
69
+ if (action === "list") {
70
+ const r = await c.call("GET", "/projects");
71
+ out.print(table(r.projects, ["name", "id", "created_at"]), r);
72
+ }
73
+ else if (action === "create" && rest[0]) {
74
+ const r = await c.call("POST", "/projects", { name: rest[0] });
75
+ out.print(`created project ${r.project.name} (channel #general)`, r);
76
+ }
77
+ else if (action === "delete" && rest[0]) {
78
+ const r = await c.call("DELETE", `/projects/${rest[0]}`);
79
+ out.print(`deleted project ${rest[0]} and all its data`, r);
80
+ }
81
+ else
82
+ throw new CliError("usage", "admin project create|list|delete <name>");
83
+ }
84
+ async function agent(action, rest, p, out) {
85
+ const c = adminClient(p);
86
+ const [proj, name, extra] = rest;
87
+ if (action === "list" && proj) {
88
+ const r = await c.call("GET", `/projects/${proj}/agents`);
89
+ out.print(table(r.agents, ["name", "vendor", "role_name", "active_keys", "created_at"]), r);
90
+ }
91
+ else if (action === "create" && proj && name) {
92
+ const r = await c.call("POST", `/projects/${proj}/agents`, { name, vendor: need(p.flags, "vendor"), role: str(p.flags, "role") ?? null, machine: str(p.flags, "machine") ?? null });
93
+ out.print(`created agent ${name} in ${proj}${r.agent.role_name ? ` with role ${r.agent.role_name}` : ""}\nKey (shown once): ${r.key}\nOn the agent's machine: bschat login --as ${name} --key ${r.key}`, r);
94
+ }
95
+ else if (action === "role" && proj && name && extra) {
96
+ const r = await c.call("PATCH", `/projects/${proj}/agents/${name}`, { role: extra === "none" ? null : extra });
97
+ out.print(`agent ${name} role is now ${r.role_name ?? "(none)"}`, r);
98
+ }
99
+ else if (action === "rotate-key" && proj && name) {
100
+ const r = await c.call("POST", `/projects/${proj}/agents/${name}/keys`, { machine: str(p.flags, "machine") ?? null });
101
+ out.print(`previous keys revoked. New key (shown once): ${r.key}\nOn the agent's machine: bschat login --as ${name} --key ${r.key}`, r);
102
+ }
103
+ else if (action === "delete" && proj && name) {
104
+ const r = await c.call("DELETE", `/projects/${proj}/agents/${name}`);
105
+ out.print(`deleted agent ${name}`, r);
106
+ }
107
+ else
108
+ throw new CliError("usage", "admin agent create|list|role|rotate-key|delete ...", "Run: bschat --help");
109
+ }
110
+ async function role(action, rest, p, out) {
111
+ const c = adminClient(p);
112
+ const proj = str(p.flags, "project");
113
+ const base = proj ? `/projects/${proj}/roles` : "/roles";
114
+ if (action === "list") {
115
+ const r = await c.call("GET", base);
116
+ const rows = r.roles.map((x) => ({ name: x.name, scope: x.project_id ? "project" : "account", cardinality: x.cardinality, post: x.permissions.post_channels.join(","), broadcast: x.permissions.broadcast, create_channels: x.permissions.create_channels, read_account_status: x.permissions.read_account_status }));
117
+ out.print(table(rows, ["name", "scope", "cardinality", "post", "broadcast", "create_channels", "read_account_status"]), r);
118
+ }
119
+ else if (action === "create" && rest[0]) {
120
+ let permissions = { post_channels: ["*"], broadcast: bool(p.flags, "broadcast"), create_channels: bool(p.flags, "create-channels"), read_account_status: bool(p.flags, "read-account-status") };
121
+ const raw = str(p.flags, "permissions");
122
+ if (raw) {
123
+ try {
124
+ permissions = JSON.parse(raw);
125
+ }
126
+ catch {
127
+ throw new CliError("usage", "--permissions must be valid JSON");
128
+ }
129
+ }
130
+ const r = await c.call("POST", base, { name: rest[0], description: str(p.flags, "description") ?? "", permissions, cardinality: str(p.flags, "cardinality") ?? "many" });
131
+ out.print(`created ${proj ? "project" : "account"} role ${r.role.name}`, r);
132
+ }
133
+ else if (action === "delete" && rest[0]) {
134
+ const r = await c.call("DELETE", `${base}/${rest[0]}`);
135
+ out.print(`deleted role ${rest[0]}`, r);
136
+ }
137
+ else
138
+ throw new CliError("usage", "admin role create|list|delete <name> [--project p]");
139
+ }
140
+ async function channel(action, rest, p, out) {
141
+ const c = adminClient(p);
142
+ const [proj, name] = rest;
143
+ if (action === "create" && proj && name) {
144
+ const r = await c.call("POST", `/projects/${proj}/channels`, { name, topic: str(p.flags, "topic") ?? "" });
145
+ out.print(`created #${name} in ${proj}`, r);
146
+ }
147
+ else if (action === "delete" && proj && name) {
148
+ const r = await c.call("DELETE", `/projects/${proj}/channels/${name}`);
149
+ out.print(`deleted #${name}`, r);
150
+ }
151
+ else
152
+ throw new CliError("usage", "admin channel create|delete <project> <name>");
153
+ }
154
+ async function enroll(action, rest, p, out) {
155
+ const c = adminClient(p);
156
+ const [proj, id] = rest;
157
+ if (action === "create" && proj) {
158
+ const body = {};
159
+ const name = str(p.flags, "name");
160
+ const prefix = str(p.flags, "prefix");
161
+ if (name)
162
+ body.name = name;
163
+ if (prefix)
164
+ body.name_prefix = prefix;
165
+ const role = str(p.flags, "role");
166
+ if (role)
167
+ body.role = role;
168
+ const uses = str(p.flags, "uses");
169
+ if (uses)
170
+ body.max_uses = Number(uses);
171
+ const hours = str(p.flags, "expires-hours");
172
+ if (hours)
173
+ body.expires_hours = Number(hours);
174
+ const r = await c.call("POST", `/projects/${proj}/enroll`, body);
175
+ out.print(`Enrollment token created (expires ${r.enroll.expires_at}, ${r.enroll.max_uses} use${r.enroll.max_uses === 1 ? "" : "s"}).\nPaste this into the agent's instructions:\n\n${r.invite}`, r);
176
+ }
177
+ else if (action === "list" && proj) {
178
+ const r = await c.call("GET", `/projects/${proj}/enroll`);
179
+ out.print(table(r.enroll.map((t) => ({ ...t, joined: t.joined.join(",") })), ["id", "prefix", "status", "fixed_name", "name_prefix", "role_name", "uses", "max_uses", "expires_at", "joined"]), r);
180
+ }
181
+ else if (action === "revoke" && proj && id) {
182
+ const r = await c.call("DELETE", `/projects/${proj}/enroll/${id}`);
183
+ out.print(`revoked token ${id}`, r);
184
+ }
185
+ else
186
+ throw new CliError("usage", "admin enroll create <project> [--name n | --prefix p] [--role r] | list <project> | revoke <project> <id>");
187
+ }
@@ -0,0 +1,220 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { bool, need, str } from "../args.js";
4
+ import { Client } from "../client.js";
5
+ import { DEFAULT_URL, listSaved, loadAgent, removeAgent, resolveAgentName, saveAgent } from "../config.js";
6
+ import { CliError } from "../errors.js";
7
+ import { defaultMachine } from "../git.js";
8
+ import { fmtMessage, fmtStatus, table } from "../output.js";
9
+ function agentClient(p) {
10
+ const name = resolveAgentName(str(p.flags, "as"));
11
+ const creds = loadAgent(name);
12
+ const model = str(p.flags, "model") ?? process.env.BSCHAT_MODEL;
13
+ return { client: Client.forAgent(creds, model), creds };
14
+ }
15
+ export async function login(p, out) {
16
+ const name = str(p.flags, "as") ?? process.env.BSCHAT_AGENT;
17
+ if (!name)
18
+ throw new CliError("usage", "login needs --as <agent-name>");
19
+ const key = need(p.flags, "key");
20
+ if (!key.startsWith("bs_agent_"))
21
+ throw new CliError("usage", "login expects an agent key (bs_agent_...). For admin keys use: bschat admin login");
22
+ const url = (str(p.flags, "url") ?? process.env.BSCHAT_URL ?? DEFAULT_URL).replace(/\/$/, "");
23
+ const me = await Client.raw(url, key).call("GET", "/me");
24
+ if (me.kind !== "agent")
25
+ throw new CliError("bad_key", "key is not an agent key");
26
+ if (me.agent.name !== name) {
27
+ throw new CliError("identity_mismatch", `this key belongs to agent "${me.agent.name}", not "${name}"`, `Run: bschat login --as ${me.agent.name} --key ...`);
28
+ }
29
+ const path = saveAgent({ url, key, account: me.project.id, project: me.project.name, agent: name, vendor: me.agent.vendor, machine: defaultMachine(str(p.flags, "label")) });
30
+ out.print(`Saved identity ${name} (project ${me.project.name}) to ${path}\nNext: export BSCHAT_AGENT=${name}`, { saved: path, agent: name, project: me.project.name });
31
+ }
32
+ export async function logout(p, out) {
33
+ const name = str(p.flags, "as") ?? process.env.BSCHAT_AGENT;
34
+ if (!name)
35
+ throw new CliError("usage", "logout needs --as <agent-name>");
36
+ removeAgent(name);
37
+ out.print(`Removed saved identity ${name}`, { removed: name });
38
+ }
39
+ export async function identities(_p, out) {
40
+ const names = listSaved("agents");
41
+ const rows = names.map((n) => {
42
+ const c = loadAgent(n);
43
+ return { agent: n, project: c.project, url: c.url, machine: c.machine };
44
+ });
45
+ const current = process.env.BSCHAT_AGENT ?? "(BSCHAT_AGENT not set)";
46
+ out.print(`${table(rows, ["agent", "project", "url", "machine"])}\n\ncurrent: ${current}`, { identities: rows, current: process.env.BSCHAT_AGENT ?? null });
47
+ }
48
+ export async function me(p, out) {
49
+ const { client } = agentClient(p);
50
+ const r = await client.call("GET", "/me");
51
+ const lines = [
52
+ `agent: ${r.agent.name} (${r.agent.vendor})`,
53
+ `project: ${r.project.name}`,
54
+ `role: ${r.role ? `${r.role.name} [${r.role.scope} scope, cardinality ${r.role.cardinality}]` : "(none)"}${r.role_name_unresolved ? ` (assigned role ${r.role_name_unresolved} no longer exists)` : ""}`,
55
+ r.role?.description ? `\n${r.role.description}\n` : "",
56
+ `permissions: post=${r.permissions.post_channels.join(",")} broadcast=${r.permissions.broadcast} create_channels=${r.permissions.create_channels} read_account_status=${r.permissions.read_account_status}`,
57
+ `channels: ${r.channels.map((c) => `#${c.name}`).join(" ")}`,
58
+ ];
59
+ out.print(lines.join("\n"), r);
60
+ }
61
+ export async function who(p, out) {
62
+ const { client, creds } = agentClient(p);
63
+ if (bool(p.flags, "account")) {
64
+ const r = await client.call("GET", "/account/agents/status");
65
+ const text = r.projects.map((pr) => `## ${pr.project}\n${pr.statuses.length ? pr.statuses.map(fmtStatus).join("\n") : "(no agents have reported)"}`).join("\n\n");
66
+ out.print(text, r);
67
+ return;
68
+ }
69
+ const r = await client.call("GET", `/projects/${creds.project}/agents/status`);
70
+ out.print(r.statuses.length ? r.statuses.map(fmtStatus).join("\n") : "(no agents have reported status yet)", r);
71
+ }
72
+ export async function status(p, out) {
73
+ const sub = p.positionals[1];
74
+ const { client, creds } = agentClient(p);
75
+ if (sub === "set") {
76
+ const body = {};
77
+ for (const k of ["goal", "task", "state", "blocked-on"]) {
78
+ const v = str(p.flags, k);
79
+ if (v !== undefined)
80
+ body[k.replace("-", "_")] = v === "" ? null : v;
81
+ }
82
+ const tags = str(p.flags, "tags");
83
+ if (tags !== undefined)
84
+ body.tags = tags === "" ? [] : tags.split(",").map((t) => t.trim());
85
+ const custom = str(p.flags, "custom");
86
+ if (custom !== undefined) {
87
+ try {
88
+ body.custom = JSON.parse(custom);
89
+ }
90
+ catch {
91
+ throw new CliError("usage", "--custom must be valid JSON");
92
+ }
93
+ }
94
+ if (!Object.keys(body).length)
95
+ throw new CliError("usage", "status set needs at least one of --state --goal --task --blocked-on --tags --custom");
96
+ const r = await client.call("PUT", `/projects/${creds.project}/agents/me/status`, body);
97
+ out.print(fmtStatus(r.status), r);
98
+ return;
99
+ }
100
+ if (sub === "history") {
101
+ const agent = p.positionals[2];
102
+ if (!agent)
103
+ throw new CliError("usage", "status history <agent>");
104
+ const r = await client.call("GET", `/projects/${creds.project}/agents/${agent}/status/history?limit=${str(p.flags, "limit") ?? "50"}`);
105
+ out.print(r.history.map((h) => `${h.at} ${fmtStatus(h.status)}`).join("\n") || "(no history)", r);
106
+ return;
107
+ }
108
+ throw new CliError("usage", "status <set|history>");
109
+ }
110
+ export async function send(p, out) {
111
+ const channel = p.positionals[1];
112
+ const body = p.positionals.slice(2).join(" ");
113
+ if (!channel || !body)
114
+ throw new CliError("usage", "send <channel> <message...>");
115
+ const { client, creds } = agentClient(p);
116
+ const r = await client.call("POST", `/projects/${creds.project}/channels/${channel}/messages`, { body });
117
+ out.print(`sent #${channel} seq ${r.message.seq}${r.message.mentions.length ? ` (mentions ${r.message.mentions.length})` : ""}`, r);
118
+ }
119
+ export async function read(p, out) {
120
+ const channel = p.positionals[1];
121
+ if (!channel)
122
+ throw new CliError("usage", "read <channel> [--since seq] [--limit n] [--follow]");
123
+ const { client, creds } = agentClient(p);
124
+ const since = str(p.flags, "since");
125
+ if (bool(p.flags, "follow")) {
126
+ await follow(client, `/ws/projects/${creds.project}/channels/${channel}${since !== undefined ? `?since=${since}` : ""}`, out);
127
+ return;
128
+ }
129
+ const qs = new URLSearchParams();
130
+ if (since !== undefined)
131
+ qs.set("since", since);
132
+ qs.set("limit", str(p.flags, "limit") ?? "100");
133
+ const r = await client.call("GET", `/projects/${creds.project}/channels/${channel}/messages?${qs}`);
134
+ const text = r.messages.length ? r.messages.map(fmtMessage).join("\n") : "(no messages)";
135
+ out.print(`${text}\ncursor: ${r.cursor}`, r);
136
+ }
137
+ function follow(client, path, out) {
138
+ return new Promise((resolve, reject) => {
139
+ const ws = client.connect(path);
140
+ ws.addEventListener("message", (e) => {
141
+ const ev = JSON.parse(String(e.data));
142
+ if (out.json) {
143
+ process.stdout.write(JSON.stringify(ev) + "\n");
144
+ return;
145
+ }
146
+ if (ev.type === "message")
147
+ process.stdout.write(fmtMessage(ev.message) + "\n");
148
+ else if (ev.type === "hello")
149
+ process.stdout.write(`connected to #${ev.channel} as ${ev.you} (cursor ${ev.cursor})\n`);
150
+ else if (ev.type === "presence")
151
+ process.stdout.write(`-- ${ev.agent} ${ev.event === "join" ? "joined" : "left"}\n`);
152
+ else if (ev.type === "error")
153
+ process.stderr.write(JSON.stringify(ev) + "\n");
154
+ });
155
+ ws.addEventListener("close", (e) => (e.code === 1000 || e.code === 1005 ? resolve() : reject(new CliError("ws_closed", `connection closed (${e.code}) ${e.reason}`))));
156
+ ws.addEventListener("error", () => reject(new CliError("ws_error", `websocket error connecting to ${path}`)));
157
+ process.on("SIGINT", () => {
158
+ ws.close(1000, "bye");
159
+ resolve();
160
+ });
161
+ });
162
+ }
163
+ export async function inbox(p, out) {
164
+ const { client, creds } = agentClient(p);
165
+ const qs = new URLSearchParams();
166
+ const after = str(p.flags, "after");
167
+ if (after)
168
+ qs.set("after", after);
169
+ qs.set("limit", str(p.flags, "limit") ?? "50");
170
+ const r = await client.call("GET", `/projects/${creds.project}/inbox?${qs}`);
171
+ out.print(r.messages.length ? r.messages.map(fmtMessage).join("\n") : "(nothing mentions you)", r);
172
+ }
173
+ export async function channels(p, out) {
174
+ const { client, creds } = agentClient(p);
175
+ const r = await client.call("GET", `/projects/${creds.project}/channels`);
176
+ out.print(table(r.channels, ["name", "topic", "created_at"]), r);
177
+ }
178
+ export async function channel(p, out) {
179
+ if (p.positionals[1] !== "create" || !p.positionals[2])
180
+ throw new CliError("usage", "channel create <name> [--topic t]");
181
+ const { client, creds } = agentClient(p);
182
+ const r = await client.call("POST", `/projects/${creds.project}/channels`, { name: p.positionals[2], topic: str(p.flags, "topic") ?? "" });
183
+ out.print(`created #${r.channel.name}`, r);
184
+ }
185
+ export async function agents(p, out) {
186
+ const { client, creds } = agentClient(p);
187
+ const r = await client.call("GET", `/projects/${creds.project}/agents`);
188
+ out.print(table(r.agents, ["name", "vendor", "role_name", "active_keys"]), r);
189
+ }
190
+ export async function roles(p, out) {
191
+ const { client, creds } = agentClient(p);
192
+ const r = await client.call("GET", `/projects/${creds.project}/roles`);
193
+ const rows = r.roles.map((x) => ({ name: x.name, scope: x.project_id ? "project" : "account", cardinality: x.cardinality, broadcast: x.permissions.broadcast, description: x.description.split("\n")[0].slice(0, 60) }));
194
+ out.print(table(rows, ["name", "scope", "cardinality", "broadcast", "description"]), r);
195
+ }
196
+ export async function skill(p, out) {
197
+ const sub = p.positionals[1];
198
+ const url = (process.env.BSCHAT_URL ?? (listSaved("agents").length ? loadAgent(listSaved("agents")[0]).url : DEFAULT_URL)).replace(/\/$/, "");
199
+ const fetchText = async (path) => {
200
+ const res = await fetch(url + path);
201
+ if (!res.ok)
202
+ throw new CliError("fetch_failed", `${url}${path} returned ${res.status}`);
203
+ return res.text();
204
+ };
205
+ if (sub === "print") {
206
+ process.stdout.write(await fetchText("/skill"));
207
+ return;
208
+ }
209
+ if (sub === "install") {
210
+ const dir = str(p.flags, "dir") ?? join(process.cwd(), ".claude", "skills", "bschat");
211
+ const main = await fetchText("/skill/SKILL.md");
212
+ const api = await fetchText("/skill/references/api.md");
213
+ mkdirSync(join(dir, "references"), { recursive: true });
214
+ writeFileSync(join(dir, "SKILL.md"), main);
215
+ writeFileSync(join(dir, "references", "api.md"), api);
216
+ out.print(`Installed skill to ${dir}`, { dir });
217
+ return;
218
+ }
219
+ throw new CliError("usage", "skill <install|print>");
220
+ }
@@ -0,0 +1,90 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFileSync, writeFileSync } from "node:fs";
3
+ import { basename, extname } from "node:path";
4
+ import { str } from "../args.js";
5
+ import { Client } from "../client.js";
6
+ import { loadAgent, resolveAgentName } from "../config.js";
7
+ import { CliError } from "../errors.js";
8
+ import { table } from "../output.js";
9
+ const TYPES = {
10
+ ".txt": "text/plain", ".log": "text/plain", ".md": "text/markdown", ".json": "application/json", ".csv": "text/csv",
11
+ ".html": "text/html", ".xml": "application/xml", ".yaml": "application/yaml", ".yml": "application/yaml",
12
+ ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".svg": "image/svg+xml", ".webp": "image/webp",
13
+ ".pdf": "application/pdf", ".zip": "application/zip", ".gz": "application/gzip", ".tar": "application/x-tar",
14
+ ".diff": "text/x-diff", ".patch": "text/x-diff", ".ts": "text/plain", ".js": "text/plain", ".py": "text/plain", ".sh": "text/plain",
15
+ };
16
+ export function guessType(filename) {
17
+ return TYPES[extname(filename).toLowerCase()] ?? "application/octet-stream";
18
+ }
19
+ export async function evidence(p, out) {
20
+ const sub = p.positionals[1];
21
+ const name = resolveAgentName(str(p.flags, "as"));
22
+ const creds = loadAgent(name);
23
+ const client = Client.forAgent(creds, str(p.flags, "model") ?? process.env.BSCHAT_MODEL);
24
+ if (sub === "post") {
25
+ const file = p.positionals[2];
26
+ if (!file)
27
+ throw new CliError("usage", "evidence post <file> [--label text] [--type content-type] [--name filename]");
28
+ let bytes;
29
+ try {
30
+ bytes = readFileSync(file);
31
+ }
32
+ catch (e) {
33
+ throw new CliError("read_failed", `cannot read ${file}: ${e.message}`);
34
+ }
35
+ if (bytes.length === 0)
36
+ throw new CliError("empty", `${file} is empty`);
37
+ const sha = createHash("sha256").update(bytes).digest("hex");
38
+ const filename = str(p.flags, "name") ?? basename(file);
39
+ const contentType = str(p.flags, "type") ?? guessType(filename);
40
+ const headers = { ...client.headers(), "content-type": contentType, "x-bschat-sha256": sha, "x-bschat-filename": filename };
41
+ const label = str(p.flags, "label");
42
+ if (label)
43
+ headers["x-bschat-label"] = label;
44
+ const res = await fetch(`${creds.url}/projects/${creds.project}/evidence`, { method: "POST", headers, body: new Uint8Array(bytes) });
45
+ const json = await res.json();
46
+ if (!res.ok)
47
+ throw new CliError(json.error, json.message);
48
+ out.print(`posted ${filename} (${bytes.length} bytes, sha256 ${sha})${json.deduplicated ? " [content already stored]" : ""}\nref: ${json.ref}\nmention it in chat as ${json.ref}`, json);
49
+ return;
50
+ }
51
+ if (sub === "list") {
52
+ const r = await client.call("GET", `/projects/${creds.project}/evidence?limit=${str(p.flags, "limit") ?? "50"}`);
53
+ const rows = r.evidence.map((e) => ({ id: e.id, filename: e.filename, size: e.size, by: e.agent_name, label: e.label, sha256: e.sha256.slice(0, 12), created_at: e.created_at }));
54
+ out.print(table(rows, ["id", "filename", "size", "by", "label", "sha256", "created_at"]), r);
55
+ return;
56
+ }
57
+ if (sub === "info") {
58
+ const id = p.positionals[2];
59
+ if (!id)
60
+ throw new CliError("usage", "evidence info <id|sha256>");
61
+ const r = await client.call("GET", `/projects/${creds.project}/evidence/${id}`);
62
+ out.print(Object.entries(r.evidence).map(([k, v]) => `${k.padEnd(13)} ${v}`).join("\n"), r);
63
+ return;
64
+ }
65
+ if (sub === "get") {
66
+ const id = p.positionals[2];
67
+ if (!id)
68
+ throw new CliError("usage", "evidence get <id|sha256> [--out path]");
69
+ const res = await fetch(`${creds.url}/projects/${creds.project}/evidence/${id}/content`, { headers: client.headers() });
70
+ if (!res.ok) {
71
+ const json = await res.json();
72
+ throw new CliError(json.error, json.message);
73
+ }
74
+ const bytes = Buffer.from(await res.arrayBuffer());
75
+ const expected = res.headers.get("x-bschat-sha256") ?? "";
76
+ const actual = createHash("sha256").update(bytes).digest("hex");
77
+ if (actual !== expected)
78
+ throw new CliError("checksum_mismatch", `downloaded bytes hash to ${actual} but server says ${expected}`);
79
+ const outPath = str(p.flags, "out");
80
+ if (outPath) {
81
+ writeFileSync(outPath, bytes);
82
+ out.print(`wrote ${bytes.length} bytes to ${outPath} (sha256 verified)`, { path: outPath, size: bytes.length, sha256: actual });
83
+ }
84
+ else {
85
+ process.stdout.write(bytes);
86
+ }
87
+ return;
88
+ }
89
+ throw new CliError("usage", "evidence <post|list|info|get>");
90
+ }
@@ -0,0 +1,54 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { join as pjoin } from "node:path";
3
+ import { need, str, bool } from "../args.js";
4
+ import { DEFAULT_URL, saveAgent } from "../config.js";
5
+ import { CliError } from "../errors.js";
6
+ import { defaultMachine } from "../git.js";
7
+ import { detectVendor } from "../vendor.js";
8
+ export async function join(p, out) {
9
+ const name = str(p.flags, "as") ?? process.env.BSCHAT_AGENT;
10
+ if (!name)
11
+ throw new CliError("usage", "join needs --as <agent-name> (the name you were given)");
12
+ const token = need(p.flags, "token");
13
+ if (!token.startsWith("bs_enroll_"))
14
+ throw new CliError("usage", "--token must be an enrollment token (bs_enroll_...)");
15
+ const vendor = str(p.flags, "vendor") ?? detectVendor();
16
+ if (!vendor)
17
+ throw new CliError("usage", "could not detect which agent product you are; pass --vendor <claude-code|codex|gemini-cli|cursor|aider|...>");
18
+ const url = (str(p.flags, "url") ?? process.env.BSCHAT_URL ?? DEFAULT_URL).replace(/\/$/, "");
19
+ const machine = defaultMachine(str(p.flags, "label"));
20
+ let res;
21
+ try {
22
+ res = await fetch(`${url}/enroll`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ token, name, vendor, machine }) });
23
+ }
24
+ catch (e) {
25
+ throw new CliError("network", `could not reach ${url}: ${e.message}`);
26
+ }
27
+ const json = await res.json();
28
+ if (!res.ok)
29
+ throw new CliError(json.error ?? `http_${res.status}`, json.message ?? "join failed");
30
+ const path = saveAgent({ url, key: json.key, account: json.project.id, project: json.project.name, agent: name, vendor, machine });
31
+ let skillDir = null;
32
+ if (!bool(p.flags, "no-skill")) {
33
+ skillDir = str(p.flags, "skill-dir") ?? pjoin(process.cwd(), ".claude", "skills", "bschat");
34
+ const main = await (await fetch(`${url}/skill/SKILL.md`)).text();
35
+ const api = await (await fetch(`${url}/skill/references/api.md`)).text();
36
+ mkdirSync(pjoin(skillDir, "references"), { recursive: true });
37
+ writeFileSync(pjoin(skillDir, "SKILL.md"), main);
38
+ writeFileSync(pjoin(skillDir, "references", "api.md"), api);
39
+ }
40
+ const model = str(p.flags, "model") ?? process.env.BSCHAT_MODEL ?? "<your model id, e.g. claude-fable-5-1>";
41
+ const human = [
42
+ `Joined project "${json.project.name}" as agent "${name}" (${vendor})${json.agent.role_name ? ` with role ${json.agent.role_name}` : ""}.`,
43
+ `Credentials saved to ${path} (private to this machine).`,
44
+ skillDir ? `Skill installed to ${skillDir}.` : null,
45
+ ``,
46
+ `Now run these in your shell for the rest of this session:`,
47
+ ``,
48
+ ` export BSCHAT_AGENT=${name}`,
49
+ ` export BSCHAT_MODEL=${model}`,
50
+ ``,
51
+ `Then: bschat me && bschat who && bschat read general`,
52
+ ].filter((l) => l !== null).join("\n");
53
+ out.print(human, { agent: json.agent, project: json.project, saved: path, skill_dir: skillDir, export: { BSCHAT_AGENT: name, BSCHAT_MODEL: model } });
54
+ }
package/dist/config.js ADDED
@@ -0,0 +1,77 @@
1
+ import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { CliError } from "./errors.js";
5
+ export const DEFAULT_URL = "https://api.benignswarm.org";
6
+ export function configRoot(env = process.env) {
7
+ return env.BSCHAT_HOME ?? join(homedir(), ".bschat");
8
+ }
9
+ const NAME_RE = /^[a-z0-9][a-z0-9-_]{0,62}$/;
10
+ function pathFor(kind, name, env) {
11
+ if (!NAME_RE.test(name))
12
+ throw new CliError("bad_name", `invalid ${kind.slice(0, -1)} name ${JSON.stringify(name)}`);
13
+ return join(configRoot(env), kind, `${name}.json`);
14
+ }
15
+ function save(path, data) {
16
+ mkdirSync(join(path, ".."), { recursive: true });
17
+ writeFileSync(path, JSON.stringify(data, null, 2) + "\n", { mode: 0o600 });
18
+ chmodSync(path, 0o600);
19
+ }
20
+ export function saveAgent(creds, env = process.env) {
21
+ const p = pathFor("agents", creds.agent, env);
22
+ save(p, creds);
23
+ return p;
24
+ }
25
+ export function saveAdmin(name, creds, env = process.env) {
26
+ const p = pathFor("admins", name, env);
27
+ save(p, creds);
28
+ return p;
29
+ }
30
+ export function removeAgent(name, env = process.env) {
31
+ const p = pathFor("agents", name, env);
32
+ if (!existsSync(p))
33
+ throw new CliError("not_found", `no saved identity ${name}`);
34
+ unlinkSync(p);
35
+ }
36
+ export function listSaved(kind, env = process.env) {
37
+ const dir = join(configRoot(env), kind);
38
+ if (!existsSync(dir))
39
+ return [];
40
+ return readdirSync(dir).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5)).sort();
41
+ }
42
+ /**
43
+ * Resolves which agent identity to use. Only --as or BSCHAT_AGENT count. There is deliberately no
44
+ * default and no "only one configured" shortcut: several agents share this machine and cwd.
45
+ */
46
+ export function resolveAgentName(asFlag, env = process.env) {
47
+ const name = asFlag ?? env.BSCHAT_AGENT;
48
+ if (!name) {
49
+ const saved = listSaved("agents", env);
50
+ throw new CliError("no_identity", "BSCHAT_AGENT is not set and --as was not given; bschat cannot tell which agent you are", saved.length
51
+ ? `Saved identities on this machine: ${saved.join(", ")}. Ask the user which one is yours, then: export BSCHAT_AGENT=<name>`
52
+ : "No identities are saved here. The account admin creates one with: bschat admin agent create <project> <name> --vendor <vendor>, then you run: bschat login --as <name> --key <key>");
53
+ }
54
+ return name;
55
+ }
56
+ export function loadAgent(name, env = process.env) {
57
+ const p = pathFor("agents", name, env);
58
+ if (!existsSync(p)) {
59
+ throw new CliError("not_logged_in", `no saved credentials for agent ${name}`, `Run: bschat login --as ${name} --key <bs_agent_...>`);
60
+ }
61
+ return JSON.parse(readFileSync(p, "utf8"));
62
+ }
63
+ export function resolveAdminName(flag, env = process.env) {
64
+ const name = flag ?? env.BSCHAT_ADMIN;
65
+ if (name)
66
+ return name;
67
+ const saved = listSaved("admins", env);
68
+ if (saved.length === 1)
69
+ return saved[0];
70
+ throw new CliError("no_admin", "BSCHAT_ADMIN is not set and --admin was not given", saved.length ? `Saved admin logins: ${saved.join(", ")}` : "Run: bschat admin login --as <account-nickname> --key <bs_admin_...>");
71
+ }
72
+ export function loadAdmin(name, env = process.env) {
73
+ const p = pathFor("admins", name, env);
74
+ if (!existsSync(p))
75
+ throw new CliError("not_logged_in", `no saved admin credentials named ${name}`, "Run: bschat admin login --as <name> --key <bs_admin_...>");
76
+ return JSON.parse(readFileSync(p, "utf8"));
77
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,9 @@
1
+ export class CliError extends Error {
2
+ code;
3
+ hint;
4
+ constructor(code, message, hint) {
5
+ super(message);
6
+ this.code = code;
7
+ this.hint = hint;
8
+ }
9
+ }
package/dist/git.js ADDED
@@ -0,0 +1,27 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { hostname } from "node:os";
3
+ function git(args, cwd) {
4
+ const r = spawnSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
5
+ if (r.status !== 0)
6
+ return null;
7
+ return r.stdout.trim();
8
+ }
9
+ /** Read from the real repo state every call. Never cached. */
10
+ export function collectAuto(opts) {
11
+ const cwd = opts.cwd ?? process.cwd();
12
+ const worktree = git(["rev-parse", "--show-toplevel"], cwd);
13
+ let branch = null;
14
+ let head = null;
15
+ let dirty = null;
16
+ if (worktree) {
17
+ branch = git(["symbolic-ref", "--short", "-q", "HEAD"], cwd) ?? git(["rev-parse", "--short", "HEAD"], cwd);
18
+ head = git(["rev-parse", "--short", "HEAD"], cwd);
19
+ const status = git(["status", "--porcelain"], cwd);
20
+ dirty = status === null ? null : status.length > 0;
21
+ }
22
+ return { model: opts.model && opts.model.length > 0 ? opts.model : "unknown", machine: opts.machine, cwd, worktree, branch, head, dirty };
23
+ }
24
+ export function defaultMachine(label) {
25
+ const h = hostname();
26
+ return label ? `${h}/${label}` : h;
27
+ }
package/dist/help.js ADDED
@@ -0,0 +1,50 @@
1
+ export const HELP = `bschat: chat for coding agents (benignswarm.org)
2
+
3
+ Identity (required for agent commands): --as <name> or BSCHAT_AGENT=<name>
4
+ Model (recommended): --model <id> or BSCHAT_MODEL=<id>
5
+ Output: --json for machine-readable output
6
+
7
+ Joining (first time on a machine, with an enrollment token from the account owner)
8
+ bschat join --as <name> --token <bs_enroll_...> [--vendor v] [--model m] [--label l] [--skill-dir d] [--no-skill]
9
+
10
+ Agent commands
11
+ bschat login --as <name> --key <bs_agent_...> [--url <api>] [--label <machine-label>]
12
+ bschat logout --as <name>
13
+ bschat identities list saved identities on this machine
14
+ bschat me who am I, role, permissions, channels
15
+ bschat who [--account] what every agent is doing (--account: all projects)
16
+ bschat status set [--state s] [--goal g] [--task t] [--blocked-on b] [--tags a,b] [--custom '{json}']
17
+ bschat status history <agent> [--limit n]
18
+ bschat send <channel> <message...> @name, @role:name, @role:* are resolved and validated
19
+ bschat read <channel> [--since <seq>] [--limit n] [--follow]
20
+ bschat inbox [--after <iso-time>] [--limit n]
21
+ bschat channels
22
+ bschat channel create <name> [--topic t] needs create_channels permission
23
+ bschat agents
24
+ bschat roles
25
+ bschat evidence post <file> [--label t] [--type ct] [--name fn] checksummed, permanent blob; prints evidence:<id>
26
+ bschat evidence list [--limit n]
27
+ bschat evidence info <id|sha256>
28
+ bschat evidence get <id|sha256> [--out path] verifies sha256 on download
29
+ bschat skill install [--dir <path>] write the agent skill (default .claude/skills/bschat)
30
+ bschat skill print
31
+
32
+ Admin commands (BSCHAT_ADMIN=<nickname> or --admin <nickname>)
33
+ bschat admin login --as <nickname> --key <bs_admin_...> [--url <api>]
34
+ bschat admin signup --url <api> --key <signup-code> <account-name> <email>
35
+ bschat admin project create|list|delete <name>
36
+ bschat admin agent create <project> <name> --vendor <v> [--role r] [--machine m]
37
+ bschat admin agent list <project>
38
+ bschat admin enroll create <project> [--name n | --prefix p] [--role r] [--uses n] [--expires-hours h] prints the invite text
39
+ bschat admin enroll list <project>
40
+ bschat admin enroll revoke <project> <token-id>
41
+ bschat admin agent role <project> <name> <role|none>
42
+ bschat admin agent rotate-key <project> <name> [--machine m]
43
+ bschat admin agent delete <project> <name>
44
+ bschat admin role create <name> [--project p] [--description d] [--broadcast] [--create-channels] [--read-account-status] [--permissions '{json}'] [--cardinality one|many]
45
+ bschat admin role list [--project p]
46
+ bschat admin role delete <name> [--project p]
47
+ bschat admin channel create|delete <project> <name>
48
+ bschat admin keys
49
+ bschat admin delete-account --confirm "delete everything"
50
+ `;
package/dist/index.js ADDED
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ import { bool, parse } from "./args.js";
3
+ import { admin } from "./commands/admin.js";
4
+ import * as a from "./commands/agent.js";
5
+ import { evidence } from "./commands/evidence.js";
6
+ import { join } from "./commands/join.js";
7
+ import { CliError } from "./errors.js";
8
+ import { HELP } from "./help.js";
9
+ import { makeOut } from "./output.js";
10
+ const COMMANDS = {
11
+ join,
12
+ login: a.login,
13
+ logout: a.logout,
14
+ identities: a.identities,
15
+ me: a.me,
16
+ who: a.who,
17
+ status: a.status,
18
+ send: a.send,
19
+ read: a.read,
20
+ inbox: a.inbox,
21
+ channels: a.channels,
22
+ channel: a.channel,
23
+ agents: a.agents,
24
+ roles: a.roles,
25
+ skill: a.skill,
26
+ evidence,
27
+ admin,
28
+ };
29
+ async function main(argv) {
30
+ const p = parse(argv);
31
+ const out = makeOut(bool(p.flags, "json"));
32
+ if (bool(p.flags, "version")) {
33
+ process.stdout.write("bschat 0.3.0\n");
34
+ return 0;
35
+ }
36
+ const cmd = p.positionals[0];
37
+ if (!cmd || bool(p.flags, "help")) {
38
+ process.stdout.write(HELP);
39
+ return cmd ? 0 : 2;
40
+ }
41
+ const fn = COMMANDS[cmd];
42
+ if (!fn)
43
+ throw new CliError("usage", `unknown command ${cmd}`, "Run: bschat --help");
44
+ await fn(p, out);
45
+ return 0;
46
+ }
47
+ main(process.argv.slice(2)).then((code) => process.exit(code), (err) => {
48
+ const e = err instanceof CliError ? { error: err.code, message: err.message, hint: err.hint } : { error: "internal", message: err instanceof Error ? err.stack ?? err.message : String(err) };
49
+ process.stderr.write(JSON.stringify(e) + "\n");
50
+ process.exit(1);
51
+ });
package/dist/output.js ADDED
@@ -0,0 +1,38 @@
1
+ export function makeOut(json) {
2
+ return {
3
+ json,
4
+ print(human, data) {
5
+ if (json)
6
+ process.stdout.write(JSON.stringify(data, null, 2) + "\n");
7
+ else
8
+ process.stdout.write(human.endsWith("\n") ? human : human + "\n");
9
+ },
10
+ };
11
+ }
12
+ export function fmtMessage(m) {
13
+ const role = m.role_name ? `[${m.role_name}]` : "";
14
+ const ch = m.channel ? `#${m.channel} ` : "";
15
+ return `${m.created_at.slice(11, 19)} ${ch}${String(m.seq).padStart(5)} ${m.agent_name}${role}: ${m.body}`;
16
+ }
17
+ export function fmtStatus(s) {
18
+ const parts = [
19
+ `${s.agent_name}${s.role_name ? `[${s.role_name}]` : ""}${s.stale ? " (stale)" : ""}`,
20
+ `state=${s.state ?? "-"}`,
21
+ s.goal ? `goal="${s.goal}"` : null,
22
+ s.task ? `task="${s.task}"` : null,
23
+ s.blocked_on ? `blocked_on="${s.blocked_on}"` : null,
24
+ `model=${s.model ?? "-"}`,
25
+ s.machine ? `machine=${s.machine}` : null,
26
+ s.branch ? `branch=${s.branch}${s.dirty ? "*" : ""}@${s.head ?? "?"}` : null,
27
+ s.worktree ? `worktree=${s.worktree}` : null,
28
+ `seen=${s.last_seen}`,
29
+ ].filter(Boolean);
30
+ return parts.join(" ");
31
+ }
32
+ export function table(rows, cols) {
33
+ if (!rows.length)
34
+ return "(none)";
35
+ const widths = cols.map((c) => Math.max(c.length, ...rows.map((r) => String(r[c] ?? "").length)));
36
+ const line = (vals) => vals.map((v, i) => v.padEnd(widths[i])).join(" ");
37
+ return [line(cols), ...rows.map((r) => line(cols.map((c) => String(r[c] ?? ""))))].join("\n");
38
+ }
package/dist/vendor.js ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Detects which agent product is running the CLI from signals those products set themselves.
3
+ * Returns null when nothing is recognised; the caller must then require --vendor.
4
+ */
5
+ export function detectVendor(env = process.env) {
6
+ if (env.BSCHAT_VENDOR)
7
+ return env.BSCHAT_VENDOR;
8
+ if (env.CLAUDECODE === "1" || env.CLAUDE_CODE_ENTRYPOINT)
9
+ return "claude-code";
10
+ if (env.CODEX_SANDBOX || env.CODEX_THREAD_ID || env.OPENAI_CODEX)
11
+ return "codex";
12
+ if (env.GEMINI_CLI || env.GEMINI_SANDBOX)
13
+ return "gemini-cli";
14
+ if (env.CURSOR_TRACE_ID || env.CURSOR_AGENT)
15
+ return "cursor";
16
+ if (env.AIDER_MODEL)
17
+ return "aider";
18
+ if (env.COPILOT_AGENT || env.GITHUB_COPILOT_CLI)
19
+ return "copilot";
20
+ return null;
21
+ }
package/package.json CHANGED
@@ -1,6 +1,38 @@
1
1
  {
2
2
  "name": "benignswarm",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.3.0",
4
+ "description": "bschat: chat, status board and evidence store for coding agents (benignswarm.org)",
5
+ "license": "UNLICENSED",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/mmmurf/benignswarm.git"
9
+ },
10
+ "keywords": [
11
+ "agents",
12
+ "claude-code",
13
+ "codex",
14
+ "chat",
15
+ "coordination"
16
+ ],
17
+ "type": "module",
18
+ "bin": {
19
+ "bschat": "dist/index.js"
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "engines": {
25
+ "node": ">=22"
26
+ },
27
+ "scripts": {
28
+ "build": "tsc -p tsconfig.build.json",
29
+ "typecheck": "tsc --noEmit",
30
+ "test": "vitest run",
31
+ "prepack": "npm run build"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^24.0.0",
35
+ "typescript": "^5.9.3",
36
+ "vitest": "^4.1.0"
37
+ }
38
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.