benignswarm 0.4.1 → 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/commands/admin.js +86 -22
- 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 +4 -2
- package/dist/help.js +8 -0
- package/dist/index.js +2 -0
- package/dist/version.js +1 -1
- 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/commands/admin.js
CHANGED
|
@@ -84,6 +84,8 @@ async function project(action, rest, p, out) {
|
|
|
84
84
|
}
|
|
85
85
|
async function agent(action, rest, p, out) {
|
|
86
86
|
const c = adminClient(p);
|
|
87
|
+
if (bool(p.flags, "account"))
|
|
88
|
+
return accountAgent(action, rest, p, out, c);
|
|
87
89
|
const [proj, name, extra] = rest;
|
|
88
90
|
if (action === "list" && proj) {
|
|
89
91
|
const r = await c.call("GET", `/projects/${proj}/agents`);
|
|
@@ -108,35 +110,95 @@ async function agent(action, rest, p, out) {
|
|
|
108
110
|
else
|
|
109
111
|
throw new CliError("usage", "admin agent create|list|role|rotate-key|delete ...", "Run: bschat --help");
|
|
110
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
|
+
}
|
|
111
166
|
async function role(action, rest, p, out) {
|
|
112
167
|
const c = adminClient(p);
|
|
113
168
|
const proj = str(p.flags, "project");
|
|
114
169
|
const base = proj ? `/projects/${proj}/roles` : "/roles";
|
|
115
170
|
if (action === "list") {
|
|
116
171
|
const r = await c.call("GET", base);
|
|
117
|
-
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 }));
|
|
118
|
-
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);
|
|
119
174
|
}
|
|
120
175
|
else if (action === "create" && rest[0]) {
|
|
121
|
-
|
|
122
|
-
const raw = str(p.flags, "permissions");
|
|
123
|
-
if (raw) {
|
|
124
|
-
try {
|
|
125
|
-
permissions = JSON.parse(raw);
|
|
126
|
-
}
|
|
127
|
-
catch {
|
|
128
|
-
throw new CliError("usage", "--permissions must be valid JSON");
|
|
129
|
-
}
|
|
130
|
-
}
|
|
176
|
+
const permissions = permsFromFlags(p, !!proj) ?? { post_channels: ["*"], broadcast: false, create_channels: false, read_account_status: false, ...(proj ? {} : { projects: [] }) };
|
|
131
177
|
const r = await c.call("POST", base, { name: rest[0], description: str(p.flags, "description") ?? "", permissions, cardinality: str(p.flags, "cardinality") ?? "many" });
|
|
132
|
-
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);
|
|
133
195
|
}
|
|
134
196
|
else if (action === "delete" && rest[0]) {
|
|
135
197
|
const r = await c.call("DELETE", `${base}/${rest[0]}`);
|
|
136
198
|
out.print(`deleted role ${rest[0]}`, r);
|
|
137
199
|
}
|
|
138
200
|
else
|
|
139
|
-
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]");
|
|
140
202
|
}
|
|
141
203
|
async function channel(action, rest, p, out) {
|
|
142
204
|
const c = adminClient(p);
|
|
@@ -154,8 +216,10 @@ async function channel(action, rest, p, out) {
|
|
|
154
216
|
}
|
|
155
217
|
async function enroll(action, rest, p, out) {
|
|
156
218
|
const c = adminClient(p);
|
|
157
|
-
const
|
|
158
|
-
|
|
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)) {
|
|
159
223
|
const body = {};
|
|
160
224
|
const name = str(p.flags, "name");
|
|
161
225
|
const prefix = str(p.flags, "prefix");
|
|
@@ -172,17 +236,17 @@ async function enroll(action, rest, p, out) {
|
|
|
172
236
|
const hours = str(p.flags, "expires-hours");
|
|
173
237
|
if (hours)
|
|
174
238
|
body.expires_hours = Number(hours);
|
|
175
|
-
const r = await c.call("POST",
|
|
239
|
+
const r = await c.call("POST", base, body);
|
|
176
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);
|
|
177
241
|
}
|
|
178
|
-
else if (action === "list" && proj) {
|
|
179
|
-
const r = await c.call("GET",
|
|
242
|
+
else if (action === "list" && (proj || account)) {
|
|
243
|
+
const r = await c.call("GET", base);
|
|
180
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);
|
|
181
245
|
}
|
|
182
|
-
else if (action === "revoke" && proj && id) {
|
|
183
|
-
const r = await c.call("DELETE",
|
|
246
|
+
else if (action === "revoke" && (proj || account) && id) {
|
|
247
|
+
const r = await c.call("DELETE", `${base}/${id}`);
|
|
184
248
|
out.print(`revoked token ${id}`, r);
|
|
185
249
|
}
|
|
186
250
|
else
|
|
187
|
-
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)");
|
|
188
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
|
@@ -71,7 +71,7 @@ export async function join(p, out) {
|
|
|
71
71
|
const json = await res.json();
|
|
72
72
|
if (!res.ok)
|
|
73
73
|
throw new CliError(json.error ?? `http_${res.status}`, json.message ?? "join failed");
|
|
74
|
-
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 });
|
|
75
75
|
const idPath = writeIdentity(root, vendor, name, model, bool(p.flags, "replace"));
|
|
76
76
|
let written = [];
|
|
77
77
|
if (!bool(p.flags, "no-skill")) {
|
|
@@ -81,7 +81,9 @@ export async function join(p, out) {
|
|
|
81
81
|
written = [...new Set(written)];
|
|
82
82
|
}
|
|
83
83
|
const human = [
|
|
84
|
-
|
|
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`,
|
|
85
87
|
`Credentials saved to ${path} (private to this machine).`,
|
|
86
88
|
`This repo now maps ${vendor} -> ${name} in ${idPath}, so bschat commands here know who you are, even after a reboot.`,
|
|
87
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
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = "0.
|
|
1
|
+
export const CLI_VERSION = "0.5.0";
|