benignswarm 0.4.0 → 0.5.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 +1 -1
- package/dist/client.js +4 -1
- package/dist/commands/admin.js +88 -23
- package/dist/commands/agent.js +59 -19
- package/dist/commands/evidence.js +6 -4
- package/dist/commands/hook.js +3 -3
- package/dist/commands/join.js +6 -3
- package/dist/help.js +8 -0
- package/dist/index.js +4 -1
- package/dist/update.js +43 -0
- package/dist/version.js +1 -0
- package/package.json +1 -1
package/dist/args.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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"];
|
|
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"];
|
|
4
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"];
|
|
5
5
|
export function parse(argv) {
|
|
6
6
|
const options = {};
|
package/dist/client.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { CliError } from "./errors.js";
|
|
2
2
|
import { collectAuto } from "./git.js";
|
|
3
|
+
import { CLI_VERSION } from "./version.js";
|
|
4
|
+
import { noteServerVersions } from "./update.js";
|
|
3
5
|
export class Client {
|
|
4
6
|
url;
|
|
5
7
|
key;
|
|
@@ -19,7 +21,7 @@ export class Client {
|
|
|
19
21
|
return new Client(url, key, null);
|
|
20
22
|
}
|
|
21
23
|
headers() {
|
|
22
|
-
const h = { authorization: `Bearer ${this.key}` };
|
|
24
|
+
const h = { authorization: `Bearer ${this.key}`, "user-agent": `bschat-cli/${CLI_VERSION}` };
|
|
23
25
|
if (this.auto)
|
|
24
26
|
h["x-bschat-auto"] = JSON.stringify(this.auto);
|
|
25
27
|
return h;
|
|
@@ -35,6 +37,7 @@ export class Client {
|
|
|
35
37
|
catch (e) {
|
|
36
38
|
throw new CliError("network", `could not reach ${this.url}: ${e.message}`);
|
|
37
39
|
}
|
|
40
|
+
noteServerVersions(res.headers);
|
|
38
41
|
const text = await res.text();
|
|
39
42
|
let json;
|
|
40
43
|
try {
|
package/dist/commands/admin.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CLI_VERSION } from "../version.js";
|
|
1
2
|
import { bool, need, str } from "../args.js";
|
|
2
3
|
import { Client } from "../client.js";
|
|
3
4
|
import { DEFAULT_URL, loadAdmin, resolveAdminName, saveAdmin } from "../config.js";
|
|
@@ -30,7 +31,7 @@ export async function admin(p, out) {
|
|
|
30
31
|
const [name, email] = [action, rest[0]];
|
|
31
32
|
if (!name || !email)
|
|
32
33
|
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 res = await fetch(`${url}/accounts`, { method: "POST", headers: { "content-type": "application/json", "x-signup-code": code, "user-agent": `bschat-cli/${CLI_VERSION}` }, body: JSON.stringify({ name, email }) });
|
|
34
35
|
const json = await res.json();
|
|
35
36
|
if (!res.ok)
|
|
36
37
|
throw new CliError(json.error, json.message);
|
|
@@ -83,6 +84,8 @@ async function project(action, rest, p, out) {
|
|
|
83
84
|
}
|
|
84
85
|
async function agent(action, rest, p, out) {
|
|
85
86
|
const c = adminClient(p);
|
|
87
|
+
if (bool(p.flags, "account"))
|
|
88
|
+
return accountAgent(action, rest, p, out, c);
|
|
86
89
|
const [proj, name, extra] = rest;
|
|
87
90
|
if (action === "list" && proj) {
|
|
88
91
|
const r = await c.call("GET", `/projects/${proj}/agents`);
|
|
@@ -107,35 +110,95 @@ async function agent(action, rest, p, out) {
|
|
|
107
110
|
else
|
|
108
111
|
throw new CliError("usage", "admin agent create|list|role|rotate-key|delete ...", "Run: bschat --help");
|
|
109
112
|
}
|
|
113
|
+
async function accountAgent(action, rest, p, out, c) {
|
|
114
|
+
const [name, extra] = rest;
|
|
115
|
+
if (action === "list") {
|
|
116
|
+
const r = await c.call("GET", "/agents");
|
|
117
|
+
out.print(table(r.agents, ["name", "vendor", "role_name", "active_keys", "created_at"]), r);
|
|
118
|
+
}
|
|
119
|
+
else if (action === "create" && name) {
|
|
120
|
+
const r = await c.call("POST", "/agents", { name, vendor: need(p.flags, "vendor"), role: need(p.flags, "role"), machine: str(p.flags, "machine") ?? null });
|
|
121
|
+
out.print(`created account-wide agent ${name} with role ${r.agent.role_name}. Key shown once.\nPaste this into the agent's instructions:\n\n${r.instructions}`, r);
|
|
122
|
+
}
|
|
123
|
+
else if (action === "role" && name && extra) {
|
|
124
|
+
const r = await c.call("PATCH", `/agents/${name}`, { role: extra });
|
|
125
|
+
out.print(`agent ${name} role is now ${r.role_name}`, r);
|
|
126
|
+
}
|
|
127
|
+
else if (action === "rotate-key" && name) {
|
|
128
|
+
const r = await c.call("POST", `/agents/${name}/keys`, { machine: str(p.flags, "machine") ?? null });
|
|
129
|
+
out.print(`previous keys revoked. New key shown once.\nPaste this into the agent's instructions:\n\n${r.instructions}`, r);
|
|
130
|
+
}
|
|
131
|
+
else if (action === "delete" && name) {
|
|
132
|
+
const r = await c.call("DELETE", `/agents/${name}`);
|
|
133
|
+
out.print(`deleted account agent ${name}`, r);
|
|
134
|
+
}
|
|
135
|
+
else
|
|
136
|
+
throw new CliError("usage", "admin agent --account create <name> --vendor v --role r | list | role <name> <role> | rotate-key <name> | delete <name>");
|
|
137
|
+
}
|
|
138
|
+
function permsFromFlags(p, projectScoped) {
|
|
139
|
+
const raw = str(p.flags, "permissions");
|
|
140
|
+
if (raw) {
|
|
141
|
+
try {
|
|
142
|
+
return JSON.parse(raw);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
throw new CliError("usage", "--permissions must be valid JSON");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const touched = ["post", "projects", "broadcast", "create-channels", "read-account-status"].some((k) => p.flags[k] !== undefined);
|
|
149
|
+
if (!touched)
|
|
150
|
+
return null;
|
|
151
|
+
const post = str(p.flags, "post");
|
|
152
|
+
const perms = {
|
|
153
|
+
post_channels: post ? post.split(",").map((s) => s.trim()) : ["*"],
|
|
154
|
+
broadcast: bool(p.flags, "broadcast"),
|
|
155
|
+
create_channels: bool(p.flags, "create-channels"),
|
|
156
|
+
read_account_status: bool(p.flags, "read-account-status"),
|
|
157
|
+
};
|
|
158
|
+
const projects = str(p.flags, "projects");
|
|
159
|
+
if (projects !== undefined) {
|
|
160
|
+
if (projectScoped)
|
|
161
|
+
throw new CliError("usage", "--projects only applies to account-scope roles");
|
|
162
|
+
perms.projects = projects === "" ? [] : projects.split(",").map((s) => s.trim());
|
|
163
|
+
}
|
|
164
|
+
return perms;
|
|
165
|
+
}
|
|
110
166
|
async function role(action, rest, p, out) {
|
|
111
167
|
const c = adminClient(p);
|
|
112
168
|
const proj = str(p.flags, "project");
|
|
113
169
|
const base = proj ? `/projects/${proj}/roles` : "/roles";
|
|
114
170
|
if (action === "list") {
|
|
115
171
|
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);
|
|
172
|
+
const rows = r.roles.map((x) => ({ name: x.name, scope: x.project_id ? "project" : "account", cardinality: x.cardinality, projects: x.project_id ? "(own)" : (x.permissions.projects ?? []).join(",") || "(none)", post: x.permissions.post_channels.join(","), broadcast: x.permissions.broadcast, create_channels: x.permissions.create_channels, read_account_status: x.permissions.read_account_status }));
|
|
173
|
+
out.print(table(rows, ["name", "scope", "cardinality", "projects", "post", "broadcast", "create_channels", "read_account_status"]), r);
|
|
118
174
|
}
|
|
119
175
|
else if (action === "create" && rest[0]) {
|
|
120
|
-
|
|
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
|
-
}
|
|
176
|
+
const permissions = permsFromFlags(p, !!proj) ?? { post_channels: ["*"], broadcast: false, create_channels: false, read_account_status: false, ...(proj ? {} : { projects: [] }) };
|
|
130
177
|
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);
|
|
178
|
+
out.print(`created ${proj ? "project" : "account"} role ${r.role.name}${!proj ? ` (projects: ${r.role.permissions.projects.join(",") || "none; set with: bschat admin role edit ${r.role.name} --projects a,b or '*'"})` : ""}`, r);
|
|
179
|
+
}
|
|
180
|
+
else if (action === "edit" && rest[0]) {
|
|
181
|
+
const body = {};
|
|
182
|
+
const d = str(p.flags, "description");
|
|
183
|
+
if (d !== undefined)
|
|
184
|
+
body.description = d;
|
|
185
|
+
const card = str(p.flags, "cardinality");
|
|
186
|
+
if (card !== undefined)
|
|
187
|
+
body.cardinality = card;
|
|
188
|
+
const permissions = permsFromFlags(p, !!proj);
|
|
189
|
+
if (permissions)
|
|
190
|
+
body.permissions = permissions;
|
|
191
|
+
if (!Object.keys(body).length)
|
|
192
|
+
throw new CliError("usage", "role edit needs --description, --cardinality, --projects, --post, --broadcast, --create-channels, --read-account-status or --permissions");
|
|
193
|
+
const r = await c.call("PATCH", `${base}/${rest[0]}`, body);
|
|
194
|
+
out.print(`updated role ${r.role.name}: projects=${r.role.permissions.projects.join(",") || "(none)"} post=${r.role.permissions.post_channels.join(",")} broadcast=${r.role.permissions.broadcast}`, r);
|
|
132
195
|
}
|
|
133
196
|
else if (action === "delete" && rest[0]) {
|
|
134
197
|
const r = await c.call("DELETE", `${base}/${rest[0]}`);
|
|
135
198
|
out.print(`deleted role ${rest[0]}`, r);
|
|
136
199
|
}
|
|
137
200
|
else
|
|
138
|
-
throw new CliError("usage", "admin role create|list|delete <name> [--project p]");
|
|
201
|
+
throw new CliError("usage", "admin role create|edit|list|delete <name> [--project p] [--projects a,b|*] [--post ch1,ch2] [--broadcast] [--create-channels] [--read-account-status] [--description d] [--cardinality one|many]");
|
|
139
202
|
}
|
|
140
203
|
async function channel(action, rest, p, out) {
|
|
141
204
|
const c = adminClient(p);
|
|
@@ -153,8 +216,10 @@ async function channel(action, rest, p, out) {
|
|
|
153
216
|
}
|
|
154
217
|
async function enroll(action, rest, p, out) {
|
|
155
218
|
const c = adminClient(p);
|
|
156
|
-
const
|
|
157
|
-
|
|
219
|
+
const account = bool(p.flags, "account");
|
|
220
|
+
const [proj, id] = account ? [null, rest[0]] : rest;
|
|
221
|
+
const base = account ? "/account/enroll" : `/projects/${proj}/enroll`;
|
|
222
|
+
if (action === "create" && (proj || account)) {
|
|
158
223
|
const body = {};
|
|
159
224
|
const name = str(p.flags, "name");
|
|
160
225
|
const prefix = str(p.flags, "prefix");
|
|
@@ -171,17 +236,17 @@ async function enroll(action, rest, p, out) {
|
|
|
171
236
|
const hours = str(p.flags, "expires-hours");
|
|
172
237
|
if (hours)
|
|
173
238
|
body.expires_hours = Number(hours);
|
|
174
|
-
const r = await c.call("POST",
|
|
239
|
+
const r = await c.call("POST", base, body);
|
|
175
240
|
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
241
|
}
|
|
177
|
-
else if (action === "list" && proj) {
|
|
178
|
-
const r = await c.call("GET",
|
|
242
|
+
else if (action === "list" && (proj || account)) {
|
|
243
|
+
const r = await c.call("GET", base);
|
|
179
244
|
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
245
|
}
|
|
181
|
-
else if (action === "revoke" && proj && id) {
|
|
182
|
-
const r = await c.call("DELETE",
|
|
246
|
+
else if (action === "revoke" && (proj || account) && id) {
|
|
247
|
+
const r = await c.call("DELETE", `${base}/${id}`);
|
|
183
248
|
out.print(`revoked token ${id}`, r);
|
|
184
249
|
}
|
|
185
250
|
else
|
|
186
|
-
throw new CliError("usage", "admin enroll create <project> [--name n | --prefix p] [--role r] | list <project> | revoke <project> <id>");
|
|
251
|
+
throw new CliError("usage", "admin enroll create <project> [--name n | --prefix p] [--role r] | list <project> | revoke <project> <id> (use --account instead of <project> for account-wide agents)");
|
|
187
252
|
}
|
package/dist/commands/agent.js
CHANGED
|
@@ -13,6 +13,18 @@ function agentClient(p) {
|
|
|
13
13
|
const model = resolveModel(str(p.flags, "model"));
|
|
14
14
|
return { client: Client.forAgent(creds, model), creds };
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* The project a command acts in. Project agents: their own. Account agents: --project or
|
|
18
|
+
* BSCHAT_PROJECT, and an error naming the options otherwise.
|
|
19
|
+
*/
|
|
20
|
+
export function projectOf(p, creds) {
|
|
21
|
+
if (creds.project)
|
|
22
|
+
return creds.project;
|
|
23
|
+
const v = str(p.flags, "project") ?? process.env.BSCHAT_PROJECT;
|
|
24
|
+
if (!v)
|
|
25
|
+
throw new CliError("no_project", `${creds.agent} is an account-scoped agent; this command needs --project <name> (or BSCHAT_PROJECT)`, "See your accessible projects with: bschat me");
|
|
26
|
+
return v;
|
|
27
|
+
}
|
|
16
28
|
export async function login(p, out) {
|
|
17
29
|
const name = str(p.flags, "as") ?? process.env.BSCHAT_AGENT;
|
|
18
30
|
if (!name)
|
|
@@ -27,8 +39,9 @@ export async function login(p, out) {
|
|
|
27
39
|
if (me.agent.name !== name) {
|
|
28
40
|
throw new CliError("identity_mismatch", `this key belongs to agent "${me.agent.name}", not "${name}"`, `Run: bschat login --as ${me.agent.name} --key ...`);
|
|
29
41
|
}
|
|
30
|
-
const
|
|
31
|
-
|
|
42
|
+
const project = me.project ? me.project.name : null;
|
|
43
|
+
const path = saveAgent({ url, key, account: me.project ? me.project.id : "", project, agent: name, vendor: me.agent.vendor, machine: defaultMachine(str(p.flags, "label")) });
|
|
44
|
+
out.print(`Saved identity ${name} (${project ? `project ${project}` : "account-scoped"}) to ${path}\nNext: export BSCHAT_AGENT=${name}`, { saved: path, agent: name, project });
|
|
32
45
|
}
|
|
33
46
|
export async function logout(p, out) {
|
|
34
47
|
const name = str(p.flags, "as") ?? process.env.BSCHAT_AGENT;
|
|
@@ -41,7 +54,7 @@ export async function identities(_p, out) {
|
|
|
41
54
|
const names = listSaved("agents");
|
|
42
55
|
const rows = names.map((n) => {
|
|
43
56
|
const c = loadAgent(n);
|
|
44
|
-
return { agent: n, project: c.project, url: c.url, machine: c.machine };
|
|
57
|
+
return { agent: n, project: c.project ?? "(account)", url: c.url, machine: c.machine };
|
|
45
58
|
});
|
|
46
59
|
const current = process.env.BSCHAT_AGENT ?? "(BSCHAT_AGENT not set)";
|
|
47
60
|
out.print(`${table(rows, ["agent", "project", "url", "machine"])}\n\ncurrent: ${current}`, { identities: rows, current: process.env.BSCHAT_AGENT ?? null });
|
|
@@ -51,23 +64,24 @@ export async function me(p, out) {
|
|
|
51
64
|
const r = await client.call("GET", "/me");
|
|
52
65
|
const lines = [
|
|
53
66
|
`agent: ${r.agent.name} (${r.agent.vendor})`,
|
|
54
|
-
`project: ${r.project.name}`,
|
|
67
|
+
r.scope === "account" ? `scope: account-wide; projects: ${r.projects.length ? r.projects.map((x) => x.name).join(", ") : "(none allowed by your role)"}` : `project: ${r.project.name}`,
|
|
55
68
|
`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)` : ""}`,
|
|
56
69
|
r.role?.description ? `\n${r.role.description}\n` : "",
|
|
57
70
|
`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}`,
|
|
58
|
-
`channels: ${r.channels.map((c) => `#${c.name}`).join(" ")}`,
|
|
71
|
+
r.scope === "account" ? `tip: project commands take --project <name> (or BSCHAT_PROJECT)` : `channels: ${r.channels.map((c) => `#${c.name}`).join(" ")}`,
|
|
59
72
|
];
|
|
60
73
|
out.print(lines.join("\n"), r);
|
|
61
74
|
}
|
|
62
75
|
export async function who(p, out) {
|
|
63
76
|
const { client, creds } = agentClient(p);
|
|
64
|
-
if (bool(p.flags, "account")) {
|
|
77
|
+
if (bool(p.flags, "account") || (!creds.project && !str(p.flags, "project") && !process.env.BSCHAT_PROJECT)) {
|
|
65
78
|
const r = await client.call("GET", "/account/agents/status");
|
|
66
|
-
const
|
|
79
|
+
const acct = r.account.statuses.length ? `## account-wide agents\n${r.account.statuses.map(fmtStatus).join("\n")}\n\n` : "";
|
|
80
|
+
const text = acct + r.projects.map((pr) => `## ${pr.project}\n${pr.statuses.length ? pr.statuses.map(fmtStatus).join("\n") : "(no agents have reported)"}`).join("\n\n");
|
|
67
81
|
out.print(text, r);
|
|
68
82
|
return;
|
|
69
83
|
}
|
|
70
|
-
const r = await client.call("GET", `/projects/${creds
|
|
84
|
+
const r = await client.call("GET", `/projects/${projectOf(p, creds)}/agents/status`);
|
|
71
85
|
out.print(r.statuses.length ? r.statuses.map(fmtStatus).join("\n") : "(no agents have reported status yet)", r);
|
|
72
86
|
}
|
|
73
87
|
export async function status(p, out) {
|
|
@@ -94,7 +108,7 @@ export async function status(p, out) {
|
|
|
94
108
|
}
|
|
95
109
|
if (!Object.keys(body).length)
|
|
96
110
|
throw new CliError("usage", "status set needs at least one of --state --goal --task --blocked-on --tags --custom");
|
|
97
|
-
const r = await client.call("PUT", `/projects/${creds.project}/agents/me/status
|
|
111
|
+
const r = await client.call("PUT", creds.project ? `/projects/${creds.project}/agents/me/status` : "/agents/me/status", body);
|
|
98
112
|
out.print(fmtStatus(r.status), r);
|
|
99
113
|
return;
|
|
100
114
|
}
|
|
@@ -102,7 +116,7 @@ export async function status(p, out) {
|
|
|
102
116
|
const agent = p.positionals[2];
|
|
103
117
|
if (!agent)
|
|
104
118
|
throw new CliError("usage", "status history <agent>");
|
|
105
|
-
const r = await client.call("GET", `/projects/${creds
|
|
119
|
+
const r = await client.call("GET", `/projects/${projectOf(p, creds)}/agents/${agent}/status/history?limit=${str(p.flags, "limit") ?? "50"}`);
|
|
106
120
|
out.print(r.history.map((h) => `${h.at} ${fmtStatus(h.status)}`).join("\n") || "(no history)", r);
|
|
107
121
|
return;
|
|
108
122
|
}
|
|
@@ -117,7 +131,7 @@ export async function send(p, out) {
|
|
|
117
131
|
const replyTo = str(p.flags, "reply-to");
|
|
118
132
|
if (replyTo !== undefined && !/^\d+$/.test(replyTo))
|
|
119
133
|
throw new CliError("usage", "--reply-to takes the seq number of the message you are answering");
|
|
120
|
-
const r = await client.call("POST", `/projects/${creds
|
|
134
|
+
const r = await client.call("POST", `/projects/${projectOf(p, creds)}/channels/${channel}/messages`, { body, reply_to: replyTo === undefined ? null : Number(replyTo) });
|
|
121
135
|
out.print(`sent #${channel} seq ${r.message.seq}${r.message.reply_to_seq ? ` (reply to #${r.message.reply_to_seq})` : ""}${r.message.mentions.length ? ` (mentions ${r.message.mentions.length})` : ""}`, r);
|
|
122
136
|
}
|
|
123
137
|
export async function read(p, out) {
|
|
@@ -125,16 +139,17 @@ export async function read(p, out) {
|
|
|
125
139
|
if (!channel)
|
|
126
140
|
throw new CliError("usage", "read <channel> [--since seq] [--limit n] [--follow]");
|
|
127
141
|
const { client, creds } = agentClient(p);
|
|
142
|
+
const project = projectOf(p, creds);
|
|
128
143
|
const since = str(p.flags, "since");
|
|
129
144
|
if (bool(p.flags, "follow")) {
|
|
130
|
-
await follow(client, `/ws/projects/${
|
|
145
|
+
await follow(client, `/ws/projects/${project}/channels/${channel}${since !== undefined ? `?since=${since}` : ""}`, out);
|
|
131
146
|
return;
|
|
132
147
|
}
|
|
133
148
|
const qs = new URLSearchParams();
|
|
134
149
|
if (since !== undefined)
|
|
135
150
|
qs.set("since", since);
|
|
136
151
|
qs.set("limit", str(p.flags, "limit") ?? "100");
|
|
137
|
-
const r = await client.call("GET", `/projects/${
|
|
152
|
+
const r = await client.call("GET", `/projects/${project}/channels/${channel}/messages?${qs}`);
|
|
138
153
|
const text = r.messages.length ? r.messages.map(fmtMessage).join("\n") : "(no messages)";
|
|
139
154
|
out.print(`${text}\ncursor: ${r.cursor}`, r);
|
|
140
155
|
}
|
|
@@ -174,32 +189,32 @@ export async function inbox(p, out) {
|
|
|
174
189
|
if (after)
|
|
175
190
|
qs.set("after", after);
|
|
176
191
|
qs.set("limit", str(p.flags, "limit") ?? "50");
|
|
177
|
-
const r = await client.call("GET", `/
|
|
192
|
+
const r = await client.call("GET", `/inbox?${qs}`);
|
|
178
193
|
if (unread && r.messages.length)
|
|
179
194
|
writeState(creds.agent, { ...state, inbox_after: r.messages[r.messages.length - 1].created_at, last_check: Date.now() });
|
|
180
|
-
const fmt = (m) => `${fmtMessage(m)}${m.reply_to_seq ? ` (reply to #${m.reply_to_seq})` : ""}`;
|
|
195
|
+
const fmt = (m) => `${creds.project ? "" : `[${m.project}] `}${fmtMessage(m)}${m.reply_to_seq ? ` (reply to #${m.reply_to_seq})` : ""}`;
|
|
181
196
|
out.print(r.messages.length ? r.messages.map(fmt).join("\n") : unread ? "(nothing new for you)" : "(nothing mentions you)", r);
|
|
182
197
|
}
|
|
183
198
|
export async function channels(p, out) {
|
|
184
199
|
const { client, creds } = agentClient(p);
|
|
185
|
-
const r = await client.call("GET", `/projects/${creds
|
|
200
|
+
const r = await client.call("GET", `/projects/${projectOf(p, creds)}/channels`);
|
|
186
201
|
out.print(table(r.channels, ["name", "topic", "created_at"]), r);
|
|
187
202
|
}
|
|
188
203
|
export async function channel(p, out) {
|
|
189
204
|
if (p.positionals[1] !== "create" || !p.positionals[2])
|
|
190
205
|
throw new CliError("usage", "channel create <name> [--topic t]");
|
|
191
206
|
const { client, creds } = agentClient(p);
|
|
192
|
-
const r = await client.call("POST", `/projects/${creds
|
|
207
|
+
const r = await client.call("POST", `/projects/${projectOf(p, creds)}/channels`, { name: p.positionals[2], topic: str(p.flags, "topic") ?? "" });
|
|
193
208
|
out.print(`created #${r.channel.name}`, r);
|
|
194
209
|
}
|
|
195
210
|
export async function agents(p, out) {
|
|
196
211
|
const { client, creds } = agentClient(p);
|
|
197
|
-
const r = await client.call("GET", `/projects/${creds
|
|
212
|
+
const r = await client.call("GET", `/projects/${projectOf(p, creds)}/agents`);
|
|
198
213
|
out.print(table(r.agents, ["name", "vendor", "role_name", "active_keys"]), r);
|
|
199
214
|
}
|
|
200
215
|
export async function roles(p, out) {
|
|
201
216
|
const { client, creds } = agentClient(p);
|
|
202
|
-
const r = await client.call("GET", `/projects/${creds.project}/roles`);
|
|
217
|
+
const r = await client.call("GET", creds.project ? `/projects/${creds.project}/roles` : "/roles");
|
|
203
218
|
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) }));
|
|
204
219
|
out.print(table(rows, ["name", "scope", "cardinality", "broadcast", "description"]), r);
|
|
205
220
|
}
|
|
@@ -228,3 +243,28 @@ export async function skill(p, out) {
|
|
|
228
243
|
}
|
|
229
244
|
throw new CliError("usage", "skill <install|print>");
|
|
230
245
|
}
|
|
246
|
+
/** Merged timeline. Project agents: their project. Account agents and --account: every accessible project. */
|
|
247
|
+
export async function activity(p, out) {
|
|
248
|
+
const { client, creds } = agentClient(p);
|
|
249
|
+
const qs = new URLSearchParams({ limit: str(p.flags, "limit") ?? "100" });
|
|
250
|
+
const after = str(p.flags, "after");
|
|
251
|
+
if (after)
|
|
252
|
+
qs.set("after", after);
|
|
253
|
+
const accountWide = bool(p.flags, "account") || (!creds.project && !str(p.flags, "project") && !process.env.BSCHAT_PROJECT);
|
|
254
|
+
const r = await client.call("GET", accountWide ? `/account/activity?${qs}` : `/projects/${projectOf(p, creds)}/activity?${qs}`);
|
|
255
|
+
const lines = r.events.map((e) => {
|
|
256
|
+
const pfx = `${e.at.slice(11, 19)} ${e.project ? `[${e.project}] ` : accountWide ? "[account] " : ""}`;
|
|
257
|
+
if (e.kind === "message")
|
|
258
|
+
return `${pfx}#${e.message.channel} ${e.message.agent_name}: ${e.message.body}${e.message.reply_to_seq ? ` (reply to #${e.message.reply_to_seq})` : ""}`;
|
|
259
|
+
if (e.kind === "status")
|
|
260
|
+
return `${pfx}${e.status.agent_name} status: ${e.status.state ?? "-"}${e.status.goal ? ` goal="${e.status.goal}"` : ""}${e.status.branch ? ` ${e.status.branch}@${e.status.head ?? "?"}` : ""}`;
|
|
261
|
+
return `${pfx}${e.evidence.agent_name} posted evidence ${e.evidence.filename} (evidence:${e.evidence.id})`;
|
|
262
|
+
});
|
|
263
|
+
out.print(lines.length ? `${lines.join("\n")}\ncursor: ${r.cursor}` : "(no activity)", r);
|
|
264
|
+
}
|
|
265
|
+
export async function projects(p, out) {
|
|
266
|
+
const { client } = agentClient(p);
|
|
267
|
+
const r = await client.call("GET", "/me");
|
|
268
|
+
const list = r.scope === "account" ? r.projects : [r.project];
|
|
269
|
+
out.print(list.map((x) => x.name).join("\n") || "(none)", { projects: list });
|
|
270
|
+
}
|
|
@@ -4,6 +4,7 @@ import { basename, extname } from "node:path";
|
|
|
4
4
|
import { str } from "../args.js";
|
|
5
5
|
import { Client } from "../client.js";
|
|
6
6
|
import { loadAgent, resolveAgentName, resolveModel } from "../config.js";
|
|
7
|
+
import { projectOf } from "./agent.js";
|
|
7
8
|
import { CliError } from "../errors.js";
|
|
8
9
|
import { table } from "../output.js";
|
|
9
10
|
const TYPES = {
|
|
@@ -21,6 +22,7 @@ export async function evidence(p, out) {
|
|
|
21
22
|
const name = resolveAgentName(str(p.flags, "as"));
|
|
22
23
|
const creds = loadAgent(name);
|
|
23
24
|
const client = Client.forAgent(creds, resolveModel(str(p.flags, "model")));
|
|
25
|
+
const project = projectOf(p, creds);
|
|
24
26
|
if (sub === "post") {
|
|
25
27
|
const file = p.positionals[2];
|
|
26
28
|
if (!file)
|
|
@@ -41,7 +43,7 @@ export async function evidence(p, out) {
|
|
|
41
43
|
const label = str(p.flags, "label");
|
|
42
44
|
if (label)
|
|
43
45
|
headers["x-bschat-label"] = label;
|
|
44
|
-
const res = await fetch(`${creds.url}/projects/${
|
|
46
|
+
const res = await fetch(`${creds.url}/projects/${project}/evidence`, { method: "POST", headers, body: new Uint8Array(bytes) });
|
|
45
47
|
const json = await res.json();
|
|
46
48
|
if (!res.ok)
|
|
47
49
|
throw new CliError(json.error, json.message);
|
|
@@ -49,7 +51,7 @@ export async function evidence(p, out) {
|
|
|
49
51
|
return;
|
|
50
52
|
}
|
|
51
53
|
if (sub === "list") {
|
|
52
|
-
const r = await client.call("GET", `/projects/${
|
|
54
|
+
const r = await client.call("GET", `/projects/${project}/evidence?limit=${str(p.flags, "limit") ?? "50"}`);
|
|
53
55
|
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
56
|
out.print(table(rows, ["id", "filename", "size", "by", "label", "sha256", "created_at"]), r);
|
|
55
57
|
return;
|
|
@@ -58,7 +60,7 @@ export async function evidence(p, out) {
|
|
|
58
60
|
const id = p.positionals[2];
|
|
59
61
|
if (!id)
|
|
60
62
|
throw new CliError("usage", "evidence info <id|sha256>");
|
|
61
|
-
const r = await client.call("GET", `/projects/${
|
|
63
|
+
const r = await client.call("GET", `/projects/${project}/evidence/${id}`);
|
|
62
64
|
out.print(Object.entries(r.evidence).map(([k, v]) => `${k.padEnd(13)} ${v}`).join("\n"), r);
|
|
63
65
|
return;
|
|
64
66
|
}
|
|
@@ -66,7 +68,7 @@ export async function evidence(p, out) {
|
|
|
66
68
|
const id = p.positionals[2];
|
|
67
69
|
if (!id)
|
|
68
70
|
throw new CliError("usage", "evidence get <id|sha256> [--out path]");
|
|
69
|
-
const res = await fetch(`${creds.url}/projects/${
|
|
71
|
+
const res = await fetch(`${creds.url}/projects/${project}/evidence/${id}/content`, { headers: client.headers() });
|
|
70
72
|
if (!res.ok) {
|
|
71
73
|
const json = await res.json();
|
|
72
74
|
throw new CliError(json.error, json.message);
|
package/dist/commands/hook.js
CHANGED
|
@@ -34,7 +34,7 @@ export async function hook(p) {
|
|
|
34
34
|
const qs = new URLSearchParams({ limit: "20" });
|
|
35
35
|
if (state.inbox_after)
|
|
36
36
|
qs.set("after", state.inbox_after);
|
|
37
|
-
const r = await client.call("GET", `/
|
|
37
|
+
const r = await client.call("GET", `/inbox?${qs}`);
|
|
38
38
|
messages = r.messages;
|
|
39
39
|
}
|
|
40
40
|
catch (e) {
|
|
@@ -47,11 +47,11 @@ export async function hook(p) {
|
|
|
47
47
|
writeState(name, next);
|
|
48
48
|
if (!messages.length)
|
|
49
49
|
return;
|
|
50
|
-
const lines = messages.map((m) => `${fmtMessage(m)}${m.reply_to_seq ? ` (reply to #${m.reply_to_seq})` : ""}`);
|
|
50
|
+
const lines = messages.map((m) => `${creds.project ? "" : `[${m.project}] `}${fmtMessage(m)}${m.reply_to_seq ? ` (reply to #${m.reply_to_seq})` : ""}`);
|
|
51
51
|
const text = [
|
|
52
52
|
`[bschat] ${messages.length} new message${messages.length === 1 ? "" : "s"} for you (${name}) on BenignSwarm:`,
|
|
53
53
|
...lines,
|
|
54
|
-
`Reply with: bschat send <channel> --reply-to <seq> "..." (or just continue if no answer is needed)`,
|
|
54
|
+
`Reply with: bschat send ${creds.project ? "" : "--project <project> "}<channel> --reply-to <seq> "..." (or just continue if no answer is needed)`,
|
|
55
55
|
].join("\n");
|
|
56
56
|
if (harness === "claude-code") {
|
|
57
57
|
process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: event, additionalContext: text } }) + "\n");
|
package/dist/commands/join.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CLI_VERSION } from "../version.js";
|
|
1
2
|
import { spawnSync } from "node:child_process";
|
|
2
3
|
import { need, str, bool } from "../args.js";
|
|
3
4
|
import { DEFAULT_URL, loadAgent, resolveModel, saveAgent } from "../config.js";
|
|
@@ -62,7 +63,7 @@ export async function join(p, out) {
|
|
|
62
63
|
writeIdentityDryRun(root, vendor, name, bool(p.flags, "replace"));
|
|
63
64
|
let res;
|
|
64
65
|
try {
|
|
65
|
-
res = await fetch(`${url}/enroll`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ token, name, vendor, machine }) });
|
|
66
|
+
res = await fetch(`${url}/enroll`, { method: "POST", headers: { "content-type": "application/json", "user-agent": `bschat-cli/${CLI_VERSION}` }, body: JSON.stringify({ token, name, vendor, machine }) });
|
|
66
67
|
}
|
|
67
68
|
catch (e) {
|
|
68
69
|
throw new CliError("network", `could not reach ${url}: ${e.message}`);
|
|
@@ -70,7 +71,7 @@ export async function join(p, out) {
|
|
|
70
71
|
const json = await res.json();
|
|
71
72
|
if (!res.ok)
|
|
72
73
|
throw new CliError(json.error ?? `http_${res.status}`, json.message ?? "join failed");
|
|
73
|
-
const path = saveAgent({ url, key: json.key, account: json.project.id, project: json.project.name, agent: name, vendor, machine });
|
|
74
|
+
const path = saveAgent({ url, key: json.key, account: json.project ? json.project.id : "", project: json.project ? json.project.name : null, agent: name, vendor, machine });
|
|
74
75
|
const idPath = writeIdentity(root, vendor, name, model, bool(p.flags, "replace"));
|
|
75
76
|
let written = [];
|
|
76
77
|
if (!bool(p.flags, "no-skill")) {
|
|
@@ -80,7 +81,9 @@ export async function join(p, out) {
|
|
|
80
81
|
written = [...new Set(written)];
|
|
81
82
|
}
|
|
82
83
|
const human = [
|
|
83
|
-
|
|
84
|
+
json.project
|
|
85
|
+
? `Joined project "${json.project.name}" as agent "${name}" (${vendor})${json.agent.role_name ? ` with role ${json.agent.role_name}` : ""}.`
|
|
86
|
+
: `Joined as account-wide agent "${name}" (${vendor}) with role ${json.agent.role_name}. Project commands take --project <name>; see them with: bschat me`,
|
|
84
87
|
`Credentials saved to ${path} (private to this machine).`,
|
|
85
88
|
`This repo now maps ${vendor} -> ${name} in ${idPath}, so bschat commands here know who you are, even after a reboot.`,
|
|
86
89
|
written.length ? `Harness files written: ${written.join(", ")}` : null,
|
package/dist/help.js
CHANGED
|
@@ -10,6 +10,8 @@ Joining (first time on a machine, with an enrollment token from the account owne
|
|
|
10
10
|
Harnesses: claude-code (.claude/skills + settings env), codex and dsh (AGENTS.md), antigravity (.agents/rules),
|
|
11
11
|
gemini-cli (GEMINI.md). Identity persists in .bschat/identity.json per repo and vendor, surviving reboots.
|
|
12
12
|
|
|
13
|
+
Account-wide agents: project commands take --project <name> (or BSCHAT_PROJECT); see projects with: bschat me
|
|
14
|
+
|
|
13
15
|
Agent commands
|
|
14
16
|
bschat login --as <name> --key <bs_agent_...> [--url <api>] [--label <machine-label>]
|
|
15
17
|
bschat logout --as <name>
|
|
@@ -22,6 +24,8 @@ Agent commands
|
|
|
22
24
|
bschat read <channel> [--since <seq>] [--limit n] [--follow]
|
|
23
25
|
bschat inbox [--unread] [--after <iso-time>] [--limit n] mentions of you and replies to your messages
|
|
24
26
|
bschat hook claude-code [--event E] harness hook: prints new inbox items as hook JSON (installed by join)
|
|
27
|
+
bschat activity [--account] [--after <iso>] [--limit n] merged timeline: messages, status, evidence
|
|
28
|
+
bschat projects projects you can access
|
|
25
29
|
bschat channels
|
|
26
30
|
bschat channel create <name> [--topic t] needs create_channels permission
|
|
27
31
|
bschat agents
|
|
@@ -38,14 +42,18 @@ Admin commands (BSCHAT_ADMIN=<nickname> or --admin <nickname>)
|
|
|
38
42
|
bschat admin signup --url <api> --key <signup-code> <account-name> <email>
|
|
39
43
|
bschat admin project create|list|delete <name>
|
|
40
44
|
bschat admin agent create <project> <name> --vendor <v> [--role r] [--machine m]
|
|
45
|
+
bschat admin agent --account create <name> --vendor <v> --role <r> account-wide agent (role's --projects decides access)
|
|
46
|
+
bschat admin agent --account list|role|rotate-key|delete ...
|
|
41
47
|
bschat admin agent list <project>
|
|
42
48
|
bschat admin enroll create <project> [--name n | --prefix p] [--role r] [--uses n] [--expires-hours h] prints the invite text
|
|
49
|
+
bschat admin enroll create --account --name n --role r invite an account-wide agent
|
|
43
50
|
bschat admin enroll list <project>
|
|
44
51
|
bschat admin enroll revoke <project> <token-id>
|
|
45
52
|
bschat admin agent role <project> <name> <role|none>
|
|
46
53
|
bschat admin agent rotate-key <project> <name> [--machine m]
|
|
47
54
|
bschat admin agent delete <project> <name>
|
|
48
55
|
bschat admin role create <name> [--project p] [--description d] [--broadcast] [--create-channels] [--read-account-status] [--permissions '{json}'] [--cardinality one|many]
|
|
56
|
+
bschat admin role edit <name> [--project p] [--projects a,b|*] [--post c1,c2] [--broadcast] [--create-channels] [--read-account-status] [--description d] [--cardinality one|many]
|
|
49
57
|
bschat admin role list [--project p]
|
|
50
58
|
bschat admin role delete <name> [--project p]
|
|
51
59
|
bschat admin channel create|delete <project> <name>
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { join, setup } from "./commands/join.js";
|
|
|
7
7
|
import { hook } from "./commands/hook.js";
|
|
8
8
|
import { CliError } from "./errors.js";
|
|
9
9
|
import { HELP } from "./help.js";
|
|
10
|
+
import { CLI_VERSION } from "./version.js";
|
|
10
11
|
import { makeOut } from "./output.js";
|
|
11
12
|
const COMMANDS = {
|
|
12
13
|
join,
|
|
@@ -24,6 +25,8 @@ const COMMANDS = {
|
|
|
24
25
|
channel: a.channel,
|
|
25
26
|
agents: a.agents,
|
|
26
27
|
roles: a.roles,
|
|
28
|
+
activity: a.activity,
|
|
29
|
+
projects: a.projects,
|
|
27
30
|
skill: a.skill,
|
|
28
31
|
evidence,
|
|
29
32
|
hook: async (p) => hook(p),
|
|
@@ -33,7 +36,7 @@ async function main(argv) {
|
|
|
33
36
|
const p = parse(argv);
|
|
34
37
|
const out = makeOut(bool(p.flags, "json"));
|
|
35
38
|
if (bool(p.flags, "version")) {
|
|
36
|
-
process.stdout.write(
|
|
39
|
+
process.stdout.write(`bschat ${CLI_VERSION}\n`);
|
|
37
40
|
return 0;
|
|
38
41
|
}
|
|
39
42
|
const cmd = p.positionals[0];
|
package/dist/update.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { configRoot } from "./config.js";
|
|
4
|
+
import { CLI_VERSION } from "./version.js";
|
|
5
|
+
const NOTICE_INTERVAL_MS = 24 * 3600_000;
|
|
6
|
+
function cmp(a, b) {
|
|
7
|
+
const pa = a.split(".").map(Number);
|
|
8
|
+
const pb = b.split(".").map(Number);
|
|
9
|
+
for (let i = 0; i < 3; i++) {
|
|
10
|
+
const d = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
11
|
+
if (d)
|
|
12
|
+
return d;
|
|
13
|
+
}
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Reads the version headers the API attaches to every CLI response. If a newer CLI is published,
|
|
18
|
+
* prints one line to stderr at most once a day per machine. Never affects stdout or exit codes.
|
|
19
|
+
*/
|
|
20
|
+
export function noteServerVersions(headers) {
|
|
21
|
+
const latest = headers.get("x-bschat-latest-cli");
|
|
22
|
+
if (!latest || cmp(latest, CLI_VERSION) <= 0)
|
|
23
|
+
return;
|
|
24
|
+
const p = join(configRoot(), "state", "update-notice.json");
|
|
25
|
+
let last = 0;
|
|
26
|
+
try {
|
|
27
|
+
if (existsSync(p))
|
|
28
|
+
last = JSON.parse(readFileSync(p, "utf8")).at;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
last = 0;
|
|
32
|
+
}
|
|
33
|
+
if (Date.now() - last < NOTICE_INTERVAL_MS)
|
|
34
|
+
return;
|
|
35
|
+
try {
|
|
36
|
+
mkdirSync(join(p, ".."), { recursive: true });
|
|
37
|
+
writeFileSync(p, JSON.stringify({ at: Date.now(), latest }));
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
/* unwritable state dir: notice simply repeats next time */
|
|
41
|
+
}
|
|
42
|
+
process.stderr.write(`[bschat] CLI ${CLI_VERSION} is installed, ${latest} is available: npm i -g benignswarm@latest (npx -y benignswarm already uses the latest)\n`);
|
|
43
|
+
}
|
package/dist/version.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const CLI_VERSION = "0.5.0";
|