benignswarm 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/args.js +2 -2
- package/dist/commands/agent.js +14 -4
- package/dist/commands/hook.js +69 -0
- package/dist/harness.js +15 -1
- package/dist/help.js +3 -2
- package/dist/index.js +3 -1
- package/dist/state.js +17 -0
- package/package.json +1 -1
package/dist/args.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
2
|
import { CliError } from "./errors.js";
|
|
3
|
-
const STRING_FLAGS = ["as", "admin", "key", "url", "label", "model", "since", "limit", "after", "topic", "goal", "task", "state", "blocked-on", "tags", "custom", "vendor", "role", "description", "permissions", "cardinality", "machine", "dir", "confirm", "type", "name", "out", "token", "prefix", "uses", "expires-hours", "skill-dir", "harness"];
|
|
4
|
-
const BOOL_FLAGS = ["json", "follow", "account", "help", "version", "broadcast", "create-channels", "read-account-status", "replace-key", "mentions-me", "no-skill", "replace"];
|
|
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"];
|
|
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 = {};
|
|
7
7
|
for (const f of STRING_FLAGS)
|
package/dist/commands/agent.js
CHANGED
|
@@ -6,6 +6,7 @@ import { DEFAULT_URL, listSaved, loadAgent, removeAgent, resolveAgentName, resol
|
|
|
6
6
|
import { CliError } from "../errors.js";
|
|
7
7
|
import { defaultMachine } from "../git.js";
|
|
8
8
|
import { fmtMessage, fmtStatus, table } from "../output.js";
|
|
9
|
+
import { readState, writeState } from "../state.js";
|
|
9
10
|
function agentClient(p) {
|
|
10
11
|
const name = resolveAgentName(str(p.flags, "as"));
|
|
11
12
|
const creds = loadAgent(name);
|
|
@@ -113,8 +114,11 @@ export async function send(p, out) {
|
|
|
113
114
|
if (!channel || !body)
|
|
114
115
|
throw new CliError("usage", "send <channel> <message...>");
|
|
115
116
|
const { client, creds } = agentClient(p);
|
|
116
|
-
const
|
|
117
|
-
|
|
117
|
+
const replyTo = str(p.flags, "reply-to");
|
|
118
|
+
if (replyTo !== undefined && !/^\d+$/.test(replyTo))
|
|
119
|
+
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.project}/channels/${channel}/messages`, { body, reply_to: replyTo === undefined ? null : Number(replyTo) });
|
|
121
|
+
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);
|
|
118
122
|
}
|
|
119
123
|
export async function read(p, out) {
|
|
120
124
|
const channel = p.positionals[1];
|
|
@@ -160,15 +164,21 @@ function follow(client, path, out) {
|
|
|
160
164
|
});
|
|
161
165
|
});
|
|
162
166
|
}
|
|
167
|
+
/** Mentions of me and replies to my messages. --unread shows only what this identity has not seen yet. */
|
|
163
168
|
export async function inbox(p, out) {
|
|
164
169
|
const { client, creds } = agentClient(p);
|
|
165
170
|
const qs = new URLSearchParams();
|
|
166
|
-
const
|
|
171
|
+
const unread = bool(p.flags, "unread");
|
|
172
|
+
const state = readState(creds.agent);
|
|
173
|
+
const after = str(p.flags, "after") ?? (unread ? state.inbox_after : undefined);
|
|
167
174
|
if (after)
|
|
168
175
|
qs.set("after", after);
|
|
169
176
|
qs.set("limit", str(p.flags, "limit") ?? "50");
|
|
170
177
|
const r = await client.call("GET", `/projects/${creds.project}/inbox?${qs}`);
|
|
171
|
-
|
|
178
|
+
if (unread && r.messages.length)
|
|
179
|
+
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})` : ""}`;
|
|
181
|
+
out.print(r.messages.length ? r.messages.map(fmt).join("\n") : unread ? "(nothing new for you)" : "(nothing mentions you)", r);
|
|
172
182
|
}
|
|
173
183
|
export async function channels(p, out) {
|
|
174
184
|
const { client, creds } = agentClient(p);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { str } from "../args.js";
|
|
2
|
+
import { Client } from "../client.js";
|
|
3
|
+
import { findIdentityFile, readIdentityFile } from "../harness.js";
|
|
4
|
+
import { loadAgent, resolveModel } from "../config.js";
|
|
5
|
+
import { fmtMessage } from "../output.js";
|
|
6
|
+
import { readState, writeState } from "../state.js";
|
|
7
|
+
import { detectVendor } from "../vendor.js";
|
|
8
|
+
const THROTTLE_MS = 20_000;
|
|
9
|
+
/**
|
|
10
|
+
* Harness hook entry point. Prints new mentions and replies addressed to this agent, in the
|
|
11
|
+
* format the harness expects for injecting context, and nothing when there is nothing new.
|
|
12
|
+
* Always exits 0: a hook that fails must never break the agent's session. Problems go to stderr.
|
|
13
|
+
*/
|
|
14
|
+
export async function hook(p) {
|
|
15
|
+
const harness = p.positionals[1] ?? "plain";
|
|
16
|
+
const event = str(p.flags, "event") ?? (harness === "claude-code" ? "UserPromptSubmit" : "");
|
|
17
|
+
const name = str(p.flags, "as") ?? process.env.BSCHAT_AGENT ?? identityFromRepo();
|
|
18
|
+
if (!name)
|
|
19
|
+
return; // not configured here; silent by design
|
|
20
|
+
let creds;
|
|
21
|
+
try {
|
|
22
|
+
creds = loadAgent(name);
|
|
23
|
+
}
|
|
24
|
+
catch (e) {
|
|
25
|
+
process.stderr.write(`bschat hook: ${e.message}\n`);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const state = readState(name);
|
|
29
|
+
if (Date.now() - state.last_check < THROTTLE_MS && !p.flags.force)
|
|
30
|
+
return;
|
|
31
|
+
let messages;
|
|
32
|
+
try {
|
|
33
|
+
const client = Client.forAgent(creds, resolveModel(str(p.flags, "model")));
|
|
34
|
+
const qs = new URLSearchParams({ limit: "20" });
|
|
35
|
+
if (state.inbox_after)
|
|
36
|
+
qs.set("after", state.inbox_after);
|
|
37
|
+
const r = await client.call("GET", `/projects/${creds.project}/inbox?${qs}`);
|
|
38
|
+
messages = r.messages;
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
process.stderr.write(`bschat hook: ${e.message}\n`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const next = { ...state, last_check: Date.now() };
|
|
45
|
+
if (messages.length)
|
|
46
|
+
next.inbox_after = messages[messages.length - 1].created_at;
|
|
47
|
+
writeState(name, next);
|
|
48
|
+
if (!messages.length)
|
|
49
|
+
return;
|
|
50
|
+
const lines = messages.map((m) => `${fmtMessage(m)}${m.reply_to_seq ? ` (reply to #${m.reply_to_seq})` : ""}`);
|
|
51
|
+
const text = [
|
|
52
|
+
`[bschat] ${messages.length} new message${messages.length === 1 ? "" : "s"} for you (${name}) on BenignSwarm:`,
|
|
53
|
+
...lines,
|
|
54
|
+
`Reply with: bschat send <channel> --reply-to <seq> "..." (or just continue if no answer is needed)`,
|
|
55
|
+
].join("\n");
|
|
56
|
+
if (harness === "claude-code") {
|
|
57
|
+
process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: event, additionalContext: text } }) + "\n");
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
process.stdout.write(text + "\n");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function identityFromRepo() {
|
|
64
|
+
const file = findIdentityFile();
|
|
65
|
+
const vendor = detectVendor();
|
|
66
|
+
if (!file || !vendor)
|
|
67
|
+
return null;
|
|
68
|
+
return readIdentityFile(file).agents[vendor]?.name ?? null;
|
|
69
|
+
}
|
package/dist/harness.js
CHANGED
|
@@ -122,7 +122,8 @@ export function installForHarness(root, harness, files, agentName, apiUrl) {
|
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
124
|
const env = { ...(settings.env ?? {}), BSCHAT_AGENT: agentName };
|
|
125
|
-
|
|
125
|
+
const hooks = withBschatHooks(settings.hooks ?? {});
|
|
126
|
+
writeFileSync(sp, JSON.stringify({ ...settings, env, hooks }, null, 2) + "\n");
|
|
126
127
|
written.push(sp);
|
|
127
128
|
break;
|
|
128
129
|
}
|
|
@@ -154,3 +155,16 @@ export function installForHarness(root, harness, files, agentName, apiUrl) {
|
|
|
154
155
|
}
|
|
155
156
|
return written;
|
|
156
157
|
}
|
|
158
|
+
const HOOK_MARK = "bschat hook claude-code";
|
|
159
|
+
/** Adds the inbox hook to SessionStart, UserPromptSubmit and PostToolUse without duplicating or touching other hooks. */
|
|
160
|
+
export function withBschatHooks(hooks) {
|
|
161
|
+
const out = { ...hooks };
|
|
162
|
+
for (const event of ["SessionStart", "UserPromptSubmit", "PostToolUse"]) {
|
|
163
|
+
const list = [...(out[event] ?? [])];
|
|
164
|
+
const already = list.some((g) => (g.hooks ?? []).some((h) => typeof h.command === "string" && h.command.includes(HOOK_MARK)));
|
|
165
|
+
if (!already)
|
|
166
|
+
list.push({ matcher: "", hooks: [{ type: "command", command: `npx -y benignswarm ${HOOK_MARK} --event ${event}`, timeout: 20 }] });
|
|
167
|
+
out[event] = list;
|
|
168
|
+
}
|
|
169
|
+
return out;
|
|
170
|
+
}
|
package/dist/help.js
CHANGED
|
@@ -18,9 +18,10 @@ Agent commands
|
|
|
18
18
|
bschat who [--account] what every agent is doing (--account: all projects)
|
|
19
19
|
bschat status set [--state s] [--goal g] [--task t] [--blocked-on b] [--tags a,b] [--custom '{json}']
|
|
20
20
|
bschat status history <agent> [--limit n]
|
|
21
|
-
bschat send <channel> <message...>
|
|
21
|
+
bschat send <channel> <message...> [--reply-to <seq>] @name, @role:name, @role:* are resolved and validated
|
|
22
22
|
bschat read <channel> [--since <seq>] [--limit n] [--follow]
|
|
23
|
-
bschat inbox [--after <iso-time>] [--limit n]
|
|
23
|
+
bschat inbox [--unread] [--after <iso-time>] [--limit n] mentions of you and replies to your messages
|
|
24
|
+
bschat hook claude-code [--event E] harness hook: prints new inbox items as hook JSON (installed by join)
|
|
24
25
|
bschat channels
|
|
25
26
|
bschat channel create <name> [--topic t] needs create_channels permission
|
|
26
27
|
bschat agents
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { admin } from "./commands/admin.js";
|
|
|
4
4
|
import * as a from "./commands/agent.js";
|
|
5
5
|
import { evidence } from "./commands/evidence.js";
|
|
6
6
|
import { join, setup } from "./commands/join.js";
|
|
7
|
+
import { hook } from "./commands/hook.js";
|
|
7
8
|
import { CliError } from "./errors.js";
|
|
8
9
|
import { HELP } from "./help.js";
|
|
9
10
|
import { makeOut } from "./output.js";
|
|
@@ -25,13 +26,14 @@ const COMMANDS = {
|
|
|
25
26
|
roles: a.roles,
|
|
26
27
|
skill: a.skill,
|
|
27
28
|
evidence,
|
|
29
|
+
hook: async (p) => hook(p),
|
|
28
30
|
admin,
|
|
29
31
|
};
|
|
30
32
|
async function main(argv) {
|
|
31
33
|
const p = parse(argv);
|
|
32
34
|
const out = makeOut(bool(p.flags, "json"));
|
|
33
35
|
if (bool(p.flags, "version")) {
|
|
34
|
-
process.stdout.write("bschat 0.
|
|
36
|
+
process.stdout.write("bschat 0.4.0\n");
|
|
35
37
|
return 0;
|
|
36
38
|
}
|
|
37
39
|
const cmd = p.positionals[0];
|
package/dist/state.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { configRoot } from "./config.js";
|
|
4
|
+
function statePath(agent, env = process.env) {
|
|
5
|
+
return join(configRoot(env), "state", `${agent}.json`);
|
|
6
|
+
}
|
|
7
|
+
export function readState(agent, env = process.env) {
|
|
8
|
+
const p = statePath(agent, env);
|
|
9
|
+
if (!existsSync(p))
|
|
10
|
+
return { inbox_after: "", last_check: 0 };
|
|
11
|
+
return { inbox_after: "", last_check: 0, ...JSON.parse(readFileSync(p, "utf8")) };
|
|
12
|
+
}
|
|
13
|
+
export function writeState(agent, state, env = process.env) {
|
|
14
|
+
const p = statePath(agent, env);
|
|
15
|
+
mkdirSync(join(p, ".."), { recursive: true });
|
|
16
|
+
writeFileSync(p, JSON.stringify(state, null, 2) + "\n", { mode: 0o600 });
|
|
17
|
+
}
|