benignswarm 0.7.9 → 0.9.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/agent.js +19 -17
- package/dist/commands/evidence.js +1 -1
- package/dist/commands/hook.js +4 -2
- package/dist/commands/join.js +17 -11
- package/dist/commands/watch.js +49 -13
- package/dist/config.js +113 -18
- package/dist/harness.js +13 -6
- package/dist/help.js +6 -2
- package/dist/state.js +21 -6
- 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", "project", "projects", "post", "title", "body", "body-file", "handoff-to", "evidence", "agent", "parent", "parent-item", "channel", "statuses", "assign", "if-status", "status", "exec", "interval", "requeue-after", "category", "command"];
|
|
3
|
+
const STRING_FLAGS = ["as", "admin", "key", "url", "label", "model", "since", "limit", "after", "topic", "goal", "task", "state", "blocked-on", "tags", "custom", "vendor", "role", "description", "permissions", "cardinality", "machine", "dir", "confirm", "type", "name", "out", "token", "prefix", "uses", "expires-hours", "skill-dir", "harness", "reply-to", "event", "project", "projects", "post", "title", "body", "body-file", "handoff-to", "evidence", "agent", "parent", "parent-item", "channel", "statuses", "assign", "if-status", "status", "exec", "interval", "requeue-after", "category", "command", "follow-delay", "env"];
|
|
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", "yes", "open", "bind-here", "peek", "once", "launchd", "full", "no-bind"];
|
|
5
5
|
export function parse(argv) {
|
|
6
6
|
const options = {};
|
package/dist/commands/agent.js
CHANGED
|
@@ -2,7 +2,7 @@ import { mkdirSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { bool, need, str } from "../args.js";
|
|
4
4
|
import { Client } from "../client.js";
|
|
5
|
-
import { DEFAULT_URL, listSaved, loadAgent, removeAgent, resolveAgentName, resolveModel, saveAgent } from "../config.js";
|
|
5
|
+
import { agentRef, DEFAULT_URL, listSaved, loadAgent, removeAgent, resolveAgentName, resolveModel, saveAgent } from "../config.js";
|
|
6
6
|
import { CliError } from "../errors.js";
|
|
7
7
|
import { defaultMachine } from "../git.js";
|
|
8
8
|
import { fmtMessage, fmtStatus, table } from "../output.js";
|
|
@@ -10,7 +10,7 @@ import { readState, writeState } from "../state.js";
|
|
|
10
10
|
function agentClient(p) {
|
|
11
11
|
const name = resolveAgentName(str(p.flags, "as"));
|
|
12
12
|
const creds = loadAgent(name);
|
|
13
|
-
const model = resolveModel(str(p.flags, "model"));
|
|
13
|
+
const model = resolveModel(str(p.flags, "model"), process.env, process.cwd(), name);
|
|
14
14
|
return { client: Client.forAgent(creds, model), creds };
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
@@ -26,9 +26,10 @@ export function projectOf(p, creds) {
|
|
|
26
26
|
return v;
|
|
27
27
|
}
|
|
28
28
|
export async function login(p, out) {
|
|
29
|
-
const
|
|
30
|
-
if (!
|
|
29
|
+
const asFlag = str(p.flags, "as") ?? process.env.BSCHAT_AGENT;
|
|
30
|
+
if (!asFlag)
|
|
31
31
|
throw new CliError("usage", "login needs --as <agent-name>");
|
|
32
|
+
const name = asFlag.includes("/") ? asFlag.slice(asFlag.indexOf("/") + 1) : asFlag;
|
|
32
33
|
const key = need(p.flags, "key");
|
|
33
34
|
if (!key.startsWith("bs_agent_"))
|
|
34
35
|
throw new CliError("usage", "login expects an agent key (bs_agent_...). For admin keys use: bschat admin login");
|
|
@@ -41,23 +42,24 @@ export async function login(p, out) {
|
|
|
41
42
|
}
|
|
42
43
|
const project = me.project ? me.project.name : null;
|
|
43
44
|
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
|
-
|
|
45
|
+
const ref = agentRef({ project, agent: name });
|
|
46
|
+
out.print(`Saved identity ${ref} (${project ? `project ${project}` : "account-scoped"}) to ${path}\nNext: export BSCHAT_AGENT=${ref}`, { saved: path, agent: name, project, ref });
|
|
45
47
|
}
|
|
46
48
|
export async function logout(p, out) {
|
|
47
49
|
const name = str(p.flags, "as") ?? process.env.BSCHAT_AGENT;
|
|
48
50
|
if (!name)
|
|
49
51
|
throw new CliError("usage", "logout needs --as <agent-name>");
|
|
50
|
-
removeAgent(name);
|
|
51
|
-
out.print(`Removed saved identity ${
|
|
52
|
+
const ref = removeAgent(name);
|
|
53
|
+
out.print(`Removed saved identity ${ref}`, { removed: ref });
|
|
52
54
|
}
|
|
53
55
|
export async function identities(_p, out) {
|
|
54
56
|
const names = listSaved("agents");
|
|
55
57
|
const rows = names.map((n) => {
|
|
56
58
|
const c = loadAgent(n);
|
|
57
|
-
return {
|
|
59
|
+
return { ref: n, agent: c.agent, project: c.project ?? "(account)", url: c.url, machine: c.machine };
|
|
58
60
|
});
|
|
59
61
|
const current = process.env.BSCHAT_AGENT ?? "(BSCHAT_AGENT not set)";
|
|
60
|
-
out.print(`${table(rows, ["agent", "project", "url", "machine"])}\n\ncurrent: ${current}`, { identities: rows, current: process.env.BSCHAT_AGENT ?? null });
|
|
62
|
+
out.print(`${table(rows, ["ref", "agent", "project", "url", "machine"])}\n\ncurrent: ${current}`, { identities: rows, current: process.env.BSCHAT_AGENT ?? null });
|
|
61
63
|
}
|
|
62
64
|
export async function me(p, out) {
|
|
63
65
|
const { client } = agentClient(p);
|
|
@@ -143,11 +145,11 @@ export async function send(p, out) {
|
|
|
143
145
|
const r = await client.call("POST", `/projects/${project}/channels/${channel}/messages`, { body, reply_to: replyTo === undefined ? null : Number(replyTo) });
|
|
144
146
|
{
|
|
145
147
|
// Our own post must not count as unread for us; advance the cursor if we were caught up.
|
|
146
|
-
const st = readState(creds
|
|
148
|
+
const st = readState(agentRef(creds));
|
|
147
149
|
const key = `${project}/${channel}`;
|
|
148
150
|
const last = st.channel_read?.[key];
|
|
149
151
|
if (last !== undefined && last >= r.message.seq - 1)
|
|
150
|
-
writeState(creds
|
|
152
|
+
writeState(agentRef(creds), { ...st, channel_read: { ...(st.channel_read ?? {}), [key]: r.message.seq } });
|
|
151
153
|
}
|
|
152
154
|
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);
|
|
153
155
|
}
|
|
@@ -168,8 +170,8 @@ export async function read(p, out) {
|
|
|
168
170
|
qs.set("limit", str(p.flags, "limit") ?? "100");
|
|
169
171
|
const r = await client.call("GET", `/projects/${project}/channels/${channel}/messages?${qs}`);
|
|
170
172
|
const text = r.messages.length ? r.messages.map(fmtMessage).join("\n") : "(no messages)";
|
|
171
|
-
const st = readState(creds
|
|
172
|
-
writeState(creds
|
|
173
|
+
const st = readState(agentRef(creds));
|
|
174
|
+
writeState(agentRef(creds), { ...st, channel_read: { ...(st.channel_read ?? {}), [`${project}/${channel}`]: r.cursor } });
|
|
173
175
|
out.print(`${text}\ncursor: ${r.cursor}`, r);
|
|
174
176
|
}
|
|
175
177
|
function follow(client, path, out) {
|
|
@@ -203,14 +205,14 @@ export async function inbox(p, out) {
|
|
|
203
205
|
const { client, creds } = agentClient(p);
|
|
204
206
|
const qs = new URLSearchParams();
|
|
205
207
|
const unread = bool(p.flags, "unread");
|
|
206
|
-
const state = readState(creds
|
|
208
|
+
const state = readState(agentRef(creds));
|
|
207
209
|
const after = str(p.flags, "after") ?? (unread ? state.inbox_after : undefined);
|
|
208
210
|
if (after)
|
|
209
211
|
qs.set("after", after);
|
|
210
212
|
qs.set("limit", str(p.flags, "limit") ?? "50");
|
|
211
213
|
const r = await client.call("GET", `/inbox?${qs}`);
|
|
212
214
|
if (unread && r.messages.length)
|
|
213
|
-
writeState(creds
|
|
215
|
+
writeState(agentRef(creds), { ...state, inbox_after: r.messages[r.messages.length - 1].created_at, last_check: Date.now() });
|
|
214
216
|
const fmt = (m) => `${creds.project ? "" : `[${m.project}] `}${fmtMessage(m)}${m.reply_to_seq ? ` (reply to #${m.reply_to_seq})` : ""}`;
|
|
215
217
|
const items = (r.items ?? []).map((i) => `list "${i.list}": [${i.status}] ${i.title} (item ${i.id})`);
|
|
216
218
|
const text = [...r.messages.map(fmt), ...(items.length ? ["assigned to you:", ...items] : [])].join("\n");
|
|
@@ -294,7 +296,7 @@ export async function followCmd(p, out) {
|
|
|
294
296
|
const channel = p.positionals[1];
|
|
295
297
|
const { creds, client } = agentClient(p);
|
|
296
298
|
const unfollow = p.positionals[0] === "unfollow";
|
|
297
|
-
const st = readState(creds
|
|
299
|
+
const st = readState(agentRef(creds));
|
|
298
300
|
const follows = new Set(st.follows ?? []);
|
|
299
301
|
if (p.positionals[0] === "following" || !channel) {
|
|
300
302
|
out.print(follows.size ? [...follows].join("\n") : "(following no channels; the hook shows a digest line only)", { follows: [...follows] });
|
|
@@ -309,6 +311,6 @@ export async function followCmd(p, out) {
|
|
|
309
311
|
await client.call("GET", `/projects/${project}/channels/${channel}/messages?limit=1`); // validates access
|
|
310
312
|
follows.add(key);
|
|
311
313
|
}
|
|
312
|
-
writeState(creds
|
|
314
|
+
writeState(agentRef(creds), { ...st, follows: [...follows] });
|
|
313
315
|
out.print(`${unfollow ? "unfollowed" : "following"} #${channel} in ${project}`, { follows: [...follows] });
|
|
314
316
|
}
|
|
@@ -21,7 +21,7 @@ export async function evidence(p, out) {
|
|
|
21
21
|
const sub = p.positionals[1];
|
|
22
22
|
const name = resolveAgentName(str(p.flags, "as"));
|
|
23
23
|
const creds = loadAgent(name);
|
|
24
|
-
const client = Client.forAgent(creds, resolveModel(str(p.flags, "model")));
|
|
24
|
+
const client = Client.forAgent(creds, resolveModel(str(p.flags, "model"), process.env, process.cwd(), name));
|
|
25
25
|
const project = projectOf(p, creds);
|
|
26
26
|
if (sub === "post") {
|
|
27
27
|
const file = p.positionals[2];
|
package/dist/commands/hook.js
CHANGED
|
@@ -78,7 +78,7 @@ export async function gatherUpdates(name, opts) {
|
|
|
78
78
|
if (follows.has(key)) {
|
|
79
79
|
const r = await client.call("GET", `/projects/${project}/channels/${ch.name}/messages?since=${last}&limit=5`);
|
|
80
80
|
const seen = new Set(messages.map((m) => m.id));
|
|
81
|
-
const others = r.messages.filter((m) => m.agent_name !==
|
|
81
|
+
const others = r.messages.filter((m) => m.agent_name !== creds.agent && !seen.has(m.id));
|
|
82
82
|
for (const m of others)
|
|
83
83
|
followed.push(`${creds.project ? "" : `[${project}] `}${fmtMessage(m)}`);
|
|
84
84
|
const shown = r.messages.length ? r.messages[r.messages.length - 1].seq : last;
|
|
@@ -137,7 +137,9 @@ export async function gatherUpdates(name, opts) {
|
|
|
137
137
|
].join("\n");
|
|
138
138
|
const operator = messages.filter((m) => m.role_name === "human").map((m) => m.created_at).sort().pop() ?? null;
|
|
139
139
|
const cmds = [...(messages.length || items.length ? ["bschat inbox --unread"] : []), ...readCommands];
|
|
140
|
-
|
|
140
|
+
const direct = items.length > 0 || messages.some((m) => m.role_name === "human" || !!m.reply_to_seq || new RegExp(`(^|[^\\w@])@${name.replace(/[-_]/g, "\\$&")}(?![\\w-])`).test(m.body));
|
|
141
|
+
const tier = direct ? "direct" : messages.length ? "role" : followed.length ? "followed" : "digest";
|
|
142
|
+
return { text, counts: { messages: messages.length, items: items.length, followed: followed.length, digest: digest.length }, latestOperatorAt: operator, readCommands: cmds, tier };
|
|
141
143
|
}
|
|
142
144
|
/**
|
|
143
145
|
* Harness hook entry point. Prints new items in the format the harness expects for injecting
|
package/dist/commands/join.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CLI_VERSION } from "../version.js";
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
3
|
import { need, str, bool } from "../args.js";
|
|
4
|
-
import { DEFAULT_URL, loadAgent, resolveModel, saveAgent } from "../config.js";
|
|
4
|
+
import { agentRef, DEFAULT_URL, loadAgent, resolveModel, saveAgent } from "../config.js";
|
|
5
5
|
import { CliError } from "../errors.js";
|
|
6
6
|
import { defaultMachine } from "../git.js";
|
|
7
7
|
import { HARNESSES, findIdentityFile, installForHarness, normalizeVendor, readIdentityFile, writeIdentity } from "../harness.js";
|
|
@@ -33,21 +33,24 @@ export async function setup(p, out) {
|
|
|
33
33
|
if (!name)
|
|
34
34
|
throw new CliError("usage", "setup needs --as <agent-name> (an identity already saved with join or login)");
|
|
35
35
|
const creds = loadAgent(name);
|
|
36
|
+
const ref = agentRef(creds);
|
|
36
37
|
const vendor = normalizeVendor(str(p.flags, "vendor") ?? creds.vendor);
|
|
37
38
|
const root = str(p.flags, "dir") ?? repoRoot();
|
|
38
|
-
const model = resolveModel(str(p.flags, "model"));
|
|
39
|
-
const idPath = writeIdentity(root, vendor,
|
|
39
|
+
const model = resolveModel(str(p.flags, "model"), process.env, process.cwd(), ref);
|
|
40
|
+
const idPath = writeIdentity(root, vendor, creds.agent, model, bool(p.flags, "replace"), creds.project);
|
|
40
41
|
const files = await fetchSkill(creds.url);
|
|
41
42
|
const written = [];
|
|
42
43
|
for (const h of harnessList(p, vendor))
|
|
43
|
-
written.push(...installForHarness(root, h, files,
|
|
44
|
+
written.push(...installForHarness(root, h, files, ref, creds.url));
|
|
44
45
|
const uniq = [...new Set(written)];
|
|
45
|
-
out.print(`Recorded ${vendor} -> ${
|
|
46
|
+
out.print(`Recorded ${vendor} -> ${ref} in ${idPath}\nWrote: ${uniq.join(", ")}`, { identity: idPath, written: uniq });
|
|
46
47
|
}
|
|
47
48
|
export async function join(p, out) {
|
|
48
49
|
const name = str(p.flags, "as") ?? process.env.BSCHAT_AGENT;
|
|
49
50
|
if (!name)
|
|
50
51
|
throw new CliError("usage", "join needs --as <agent-name> (the name you were given)");
|
|
52
|
+
if (name.includes("/"))
|
|
53
|
+
throw new CliError("usage", "join takes the bare agent name; the project comes from the token");
|
|
51
54
|
const token = need(p.flags, "token");
|
|
52
55
|
if (!token.startsWith("bs_enroll_"))
|
|
53
56
|
throw new CliError("usage", "--token must be an enrollment token (bs_enroll_...)");
|
|
@@ -74,13 +77,15 @@ export async function join(p, out) {
|
|
|
74
77
|
const json = await res.json();
|
|
75
78
|
if (!res.ok)
|
|
76
79
|
throw new CliError(json.error ?? `http_${res.status}`, json.message ?? "join failed");
|
|
77
|
-
const
|
|
78
|
-
const
|
|
80
|
+
const project = json.project ? json.project.name : null;
|
|
81
|
+
const ref = agentRef({ project, agent: name });
|
|
82
|
+
const path = saveAgent({ url, key: json.key, account: json.project ? json.project.id : "", project, agent: name, vendor, machine });
|
|
83
|
+
const idPath = noBind ? null : writeIdentity(root, vendor, name, model, bool(p.flags, "replace"), project);
|
|
79
84
|
let written = [];
|
|
80
85
|
if (!bool(p.flags, "no-skill") && !noBind) {
|
|
81
86
|
const files = await fetchSkill(url);
|
|
82
87
|
for (const h of harnessList(p, vendor))
|
|
83
|
-
written.push(...installForHarness(root, h, files,
|
|
88
|
+
written.push(...installForHarness(root, h, files, ref, url));
|
|
84
89
|
written = [...new Set(written)];
|
|
85
90
|
}
|
|
86
91
|
const human = [
|
|
@@ -89,8 +94,8 @@ export async function join(p, out) {
|
|
|
89
94
|
: `Joined as account-wide agent "${name}" (${vendor}) with role ${json.agent.role_name}. Project commands take --project <name>; see them with: bschat me`,
|
|
90
95
|
`Credentials saved to ${path} (private to this machine).`,
|
|
91
96
|
noBind
|
|
92
|
-
? `This repo's ${vendor} identity was left as is. Address yourself on every call: \`bschat --as ${
|
|
93
|
-
: `This repo now maps ${vendor} -> ${
|
|
97
|
+
? `This repo's ${vendor} identity was left as is. Address yourself on every call: \`bschat --as ${ref} <command>\` or \`BSCHAT_AGENT=${ref} bschat <command>\`. Never run bare bschat here as ${name}.`
|
|
98
|
+
: `This repo now maps ${vendor} -> ${ref} in ${idPath}, so bschat commands here know who you are, even after a reboot.`,
|
|
94
99
|
written.length ? `Harness files written: ${written.join(", ")}` : null,
|
|
95
100
|
``,
|
|
96
101
|
`Your model is detected automatically (hook, transcript or harness config); override with --model or BSCHAT_MODEL only if it reports "unknown".`,
|
|
@@ -99,9 +104,10 @@ export async function join(p, out) {
|
|
|
99
104
|
json.revived && json.resume ? `--- ${json.resume.title} ---\n${json.resume.body}\n---${json.resume.evidence.length ? `\nevidence: ${json.resume.evidence.map((e) => `evidence:${e}`).join(" ")}` : ""}` : null,
|
|
100
105
|
json.revived && json.resume ? `Read it before doing anything else.` : null,
|
|
101
106
|
``,
|
|
107
|
+
vendor !== "claude-code" ? `No hook exists for ${vendor}: you will not be told about replies unless you run a watcher. See the skill's "Knowing when someone talks to you" section (bschat watch --launchd).` : null,
|
|
102
108
|
`Next: bschat me && bschat who && bschat inbox && bschat handoffs --open`,
|
|
103
109
|
].filter((l) => l !== null).join("\n");
|
|
104
|
-
out.print(human, { agent: json.agent, project: json.project, saved: path, identity: idPath, bound: !noBind, written, revived: !!json.revived, resume: json.resume ?? null, export: { BSCHAT_AGENT:
|
|
110
|
+
out.print(human, { agent: json.agent, project: json.project, saved: path, identity: idPath, bound: !noBind, written, revived: !!json.revived, resume: json.resume ?? null, export: { BSCHAT_AGENT: ref } });
|
|
105
111
|
}
|
|
106
112
|
function writeIdentityDryRun(root, vendor, name, replace) {
|
|
107
113
|
const p = findIdentityFile(root);
|
package/dist/commands/watch.js
CHANGED
|
@@ -26,10 +26,21 @@ export async function watch(p, out) {
|
|
|
26
26
|
const once = bool(p.flags, "once");
|
|
27
27
|
const full = bool(p.flags, "full");
|
|
28
28
|
const requeueAfter = Number(str(p.flags, "requeue-after") ?? "1800");
|
|
29
|
+
const followDelay = Number(str(p.flags, "follow-delay") ?? "300");
|
|
30
|
+
if (!Number.isInteger(followDelay) || followDelay < 0)
|
|
31
|
+
throw new CliError("usage", "--follow-delay must be a non-negative number of seconds");
|
|
29
32
|
if (bool(p.flags, "launchd")) {
|
|
30
33
|
if (!exec)
|
|
31
34
|
throw new CliError("usage", "--launchd needs --exec so the agent knows what to run");
|
|
32
|
-
|
|
35
|
+
const envNames = (str(p.flags, "env") ?? "").split(",").map((x) => x.trim()).filter(Boolean);
|
|
36
|
+
const extraEnv = {};
|
|
37
|
+
for (const n of envNames) {
|
|
38
|
+
const v = process.env[n];
|
|
39
|
+
if (v === undefined)
|
|
40
|
+
throw new CliError("usage", `--env ${n}: not set in this shell; run this from a terminal inside the harness (Antigravity's terminal has ANTIGRAVITY_LS_ADDRESS and ANTIGRAVITY_CSRF_TOKEN)`);
|
|
41
|
+
extraEnv[n] = v;
|
|
42
|
+
}
|
|
43
|
+
return installLaunchd(name, exec, interval, followDelay, extraEnv, out);
|
|
33
44
|
}
|
|
34
45
|
if (!exec && !once)
|
|
35
46
|
throw new CliError("usage", 'watch --exec "<command>" [--interval 30] | watch --once (print and exit) | watch --exec ... --launchd');
|
|
@@ -37,7 +48,7 @@ export async function watch(p, out) {
|
|
|
37
48
|
log(`watching${exec ? `, will run: ${exec}` : ""} every ${interval}s (${full ? "full text" : "single wake-up"} mode)`);
|
|
38
49
|
for (;;) {
|
|
39
50
|
if (!full) {
|
|
40
|
-
await wakeOnce(name, exec, requeueAfter, log, str(p.flags, "model"), once);
|
|
51
|
+
await wakeOnce(name, exec, requeueAfter, followDelay, log, str(p.flags, "model"), once);
|
|
41
52
|
if (once)
|
|
42
53
|
return;
|
|
43
54
|
await new Promise((r) => setTimeout(r, interval * 1000));
|
|
@@ -66,7 +77,7 @@ export async function watch(p, out) {
|
|
|
66
77
|
await new Promise((r) => setTimeout(r, interval * 1000));
|
|
67
78
|
}
|
|
68
79
|
}
|
|
69
|
-
async function wakeOnce(name, exec, requeueAfter, log, model, once) {
|
|
80
|
+
async function wakeOnce(name, exec, requeueAfter, followDelay, log, model, once) {
|
|
70
81
|
const state = readState(name);
|
|
71
82
|
const pending = state.watch_pending;
|
|
72
83
|
// Did the agent read since we last woke it? Its own CLI calls advance these cursors.
|
|
@@ -80,6 +91,8 @@ async function wakeOnce(name, exec, requeueAfter, log, model, once) {
|
|
|
80
91
|
const u = await gatherUpdates(name, { force: true, model, delivery: "watch", dryRun: true });
|
|
81
92
|
const st = readState(name);
|
|
82
93
|
if (!u) {
|
|
94
|
+
if (st.watch_followed_since)
|
|
95
|
+
writeState(name, { ...st, watch_followed_since: undefined });
|
|
83
96
|
if (once)
|
|
84
97
|
log("nothing new");
|
|
85
98
|
return;
|
|
@@ -92,6 +105,24 @@ async function wakeOnce(name, exec, requeueAfter, log, model, once) {
|
|
|
92
105
|
log(`new activity but a wake-up is already pending (${Math.round((Date.now() - st.watch_pending.at) / 1000)}s); not queueing again`);
|
|
93
106
|
return;
|
|
94
107
|
}
|
|
108
|
+
// Tiering: things addressed to this agent wake it now; followed-channel chatter waits for the
|
|
109
|
+
// follow delay so bursts collapse; digest-only channels never wake it on their own.
|
|
110
|
+
if (u.tier === "digest") {
|
|
111
|
+
if (once)
|
|
112
|
+
log("only digest-level unread; not waking");
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (u.tier === "followed") {
|
|
116
|
+
const since = st.watch_followed_since ?? Date.now();
|
|
117
|
+
if (!st.watch_followed_since)
|
|
118
|
+
writeState(name, { ...st, watch_followed_since: since });
|
|
119
|
+
const waited = Date.now() - since;
|
|
120
|
+
if (waited < followDelay * 1000) {
|
|
121
|
+
if (once)
|
|
122
|
+
log(`followed-channel traffic only; waiting (${Math.round(waited / 1000)}/${followDelay}s) before waking`);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
95
126
|
const summary = u.text.split("\n")[0].replace(/^\[bschat\] (Delivered by your BenignSwarm watcher, not typed by the user\. )?/, "");
|
|
96
127
|
const wake = [
|
|
97
128
|
`[bschat] Wake-up from your BenignSwarm watcher, not typed by the user.`,
|
|
@@ -104,15 +135,18 @@ async function wakeOnce(name, exec, requeueAfter, log, model, once) {
|
|
|
104
135
|
if (exec) {
|
|
105
136
|
const total = u.counts.messages + u.counts.items + u.counts.followed + u.counts.digest;
|
|
106
137
|
const cmd = exec.replaceAll("{count}", String(total)).replaceAll("{summary}", shellQuote(summary)).replaceAll("{text}", shellQuote(wake));
|
|
107
|
-
log(`${operatorNew ? "operator message; " : stale ? "re-waking after timeout; " : ""}queueing wake-up: ${summary}`);
|
|
138
|
+
log(`${operatorNew ? "operator message; " : stale ? "re-waking after timeout; " : ""}[${u.tier}] queueing wake-up: ${summary}`);
|
|
108
139
|
const code = await run(cmd, wake, { BSCHAT_TEXT: wake, BSCHAT_SUMMARY: summary, BSCHAT_COUNT: String(total) });
|
|
109
|
-
if (code !== 0)
|
|
110
|
-
|
|
140
|
+
if (code !== 0) {
|
|
141
|
+
// The agent was NOT woken. Leave nothing pending so the next poll tries again.
|
|
142
|
+
log(`wake command exited ${code}; will retry next poll. If this repeats, the harness needs environment the LaunchAgent lacks (see --env).`);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
111
145
|
}
|
|
112
146
|
else {
|
|
113
147
|
process.stdout.write(wake + "\n");
|
|
114
148
|
}
|
|
115
|
-
writeState(name, { ...
|
|
149
|
+
writeState(name, { ...readState(name), watch_pending: { at: Date.now(), inbox_after: st.inbox_after, channel_read: st.channel_read ?? {} }, watch_operator_at: u.latestOperatorAt ?? st.watch_operator_at, watch_followed_since: undefined });
|
|
116
150
|
}
|
|
117
151
|
function shellQuote(s) {
|
|
118
152
|
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
@@ -126,10 +160,11 @@ function run(cmd, stdin, env) {
|
|
|
126
160
|
});
|
|
127
161
|
}
|
|
128
162
|
/** Writes a launchd agent that keeps `bschat watch` running for this identity on macOS. */
|
|
129
|
-
function installLaunchd(name, exec, interval, out) {
|
|
163
|
+
function installLaunchd(name, exec, interval, followDelay, extraEnv, out) {
|
|
130
164
|
if (process.platform !== "darwin")
|
|
131
165
|
throw new CliError("unsupported", "--launchd is macOS only; on Linux run `bschat watch` under a systemd user service");
|
|
132
|
-
const
|
|
166
|
+
const slug = name.replace("/", ".");
|
|
167
|
+
const label = `org.benignswarm.watch.${slug}`;
|
|
133
168
|
const dir = join(homedir(), "Library", "LaunchAgents");
|
|
134
169
|
const logDir = join(homedir(), ".bschat", "logs");
|
|
135
170
|
mkdirSync(dir, { recursive: true });
|
|
@@ -150,18 +185,19 @@ function installLaunchd(name, exec, interval, out) {
|
|
|
150
185
|
<string>--as</string><string>${name}</string>
|
|
151
186
|
<string>--exec</string><string>${escapeXml(exec)}</string>
|
|
152
187
|
<string>--interval</string><string>${interval}</string>
|
|
188
|
+
<string>--follow-delay</string><string>${followDelay}</string>
|
|
153
189
|
</array>
|
|
154
|
-
<key>EnvironmentVariables</key><dict><key>PATH</key><string>${escapeXml(process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin")}</string><key>HOME</key><string>${homedir()}</string
|
|
190
|
+
<key>EnvironmentVariables</key><dict><key>PATH</key><string>${escapeXml(process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin")}</string><key>HOME</key><string>${homedir()}</string>${Object.entries(extraEnv).map(([k, v]) => `<key>${escapeXml(k)}</key><string>${escapeXml(v)}</string>`).join("")}</dict>
|
|
155
191
|
<key>WorkingDirectory</key><string>${escapeXml(process.cwd())}</string>
|
|
156
192
|
<key>RunAtLoad</key><true/>
|
|
157
193
|
<key>KeepAlive</key><true/>
|
|
158
|
-
<key>StandardOutPath</key><string>${join(logDir, `watch.${
|
|
159
|
-
<key>StandardErrorPath</key><string>${join(logDir, `watch.${
|
|
194
|
+
<key>StandardOutPath</key><string>${join(logDir, `watch.${slug}.out.log`)}</string>
|
|
195
|
+
<key>StandardErrorPath</key><string>${join(logDir, `watch.${slug}.err.log`)}</string>
|
|
160
196
|
</dict></plist>
|
|
161
197
|
`;
|
|
162
198
|
const path = join(dir, `${label}.plist`);
|
|
163
199
|
writeFileSync(path, plist);
|
|
164
|
-
out.print(`Wrote ${path}\nStart it: launchctl bootstrap gui/$(id -u) ${path}\nStop it: launchctl bootout gui/$(id -u)/${label}\nLogs: ${logDir}/watch.${
|
|
200
|
+
out.print(`Wrote ${path}${Object.keys(extraEnv).length ? `\nCaptured env: ${Object.keys(extraEnv).join(", ")} (re-run this command if the harness restarts and these change)` : ""}\nStart it: launchctl bootstrap gui/$(id -u) ${path}\nStop it: launchctl bootout gui/$(id -u)/${label}\nLogs: ${logDir}/watch.${slug}.*.log\nThe watcher runs from ${process.cwd()} so the repo identity file applies.`, { plist: path, label });
|
|
165
201
|
}
|
|
166
202
|
function escapeXml(s) {
|
|
167
203
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
package/dist/config.js
CHANGED
|
@@ -11,10 +11,58 @@ export function configRoot(env = process.env) {
|
|
|
11
11
|
return env.BSCHAT_HOME ?? join(homedir(), ".bschat");
|
|
12
12
|
}
|
|
13
13
|
const NAME_RE = /^[a-z0-9][a-z0-9-_]{0,62}$/;
|
|
14
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Saved agent identities are addressed by a ref `<scope>/<name>` where scope is the project
|
|
16
|
+
* name, or `account` for an account-scoped agent. Agent names are only unique per project on
|
|
17
|
+
* the server, so a bare name is ambiguous as soon as two projects on this machine use it; the
|
|
18
|
+
* ref is what keeps their credentials, read cursors and watchers apart.
|
|
19
|
+
*/
|
|
20
|
+
export const ACCOUNT_SCOPE = "account";
|
|
21
|
+
export function agentRef(creds) {
|
|
22
|
+
return `${creds.project ?? ACCOUNT_SCOPE}/${creds.agent}`;
|
|
23
|
+
}
|
|
24
|
+
export function splitRef(ref) {
|
|
25
|
+
const i = ref.indexOf("/");
|
|
26
|
+
if (i < 0)
|
|
27
|
+
throw new CliError("bad_name", `${JSON.stringify(ref)} is not a qualified agent ref (<project>/<name> or ${ACCOUNT_SCOPE}/<name>)`);
|
|
28
|
+
const scope = ref.slice(0, i);
|
|
29
|
+
const name = ref.slice(i + 1);
|
|
30
|
+
if (!NAME_RE.test(scope) || !NAME_RE.test(name))
|
|
31
|
+
throw new CliError("bad_name", `invalid agent ref ${JSON.stringify(ref)}`);
|
|
32
|
+
return { scope, name };
|
|
33
|
+
}
|
|
34
|
+
export function isRef(v) {
|
|
35
|
+
return v.includes("/");
|
|
36
|
+
}
|
|
37
|
+
function adminPath(name, env) {
|
|
15
38
|
if (!NAME_RE.test(name))
|
|
16
|
-
throw new CliError("bad_name", `invalid
|
|
17
|
-
return join(configRoot(env),
|
|
39
|
+
throw new CliError("bad_name", `invalid admin name ${JSON.stringify(name)}`);
|
|
40
|
+
return join(configRoot(env), "admins", `${name}.json`);
|
|
41
|
+
}
|
|
42
|
+
function agentPath(ref, env) {
|
|
43
|
+
const { scope, name } = splitRef(ref);
|
|
44
|
+
return join(configRoot(env), "agents", scope, `${name}.json`);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Versions before 0.9 stored agents as `agents/<name>.json`, keyed by name alone, which let a
|
|
48
|
+
* join in one project silently overwrite the credentials of a same-named agent in another.
|
|
49
|
+
* Each such file records its project, so it is moved into its scope directory on first use.
|
|
50
|
+
*/
|
|
51
|
+
function migrateLegacyAgents(env) {
|
|
52
|
+
const dir = join(configRoot(env), "agents");
|
|
53
|
+
if (!existsSync(dir))
|
|
54
|
+
return;
|
|
55
|
+
for (const f of readdirSync(dir)) {
|
|
56
|
+
if (!f.endsWith(".json"))
|
|
57
|
+
continue;
|
|
58
|
+
const legacy = join(dir, f);
|
|
59
|
+
const creds = JSON.parse(readFileSync(legacy, "utf8"));
|
|
60
|
+
const target = agentPath(agentRef(creds), env);
|
|
61
|
+
if (existsSync(target))
|
|
62
|
+
throw new CliError("legacy_conflict", `${legacy} and ${target} both exist; delete the one that is wrong`);
|
|
63
|
+
save(target, creds);
|
|
64
|
+
unlinkSync(legacy);
|
|
65
|
+
}
|
|
18
66
|
}
|
|
19
67
|
function save(path, data) {
|
|
20
68
|
mkdirSync(join(path, ".."), { recursive: true });
|
|
@@ -22,26 +70,59 @@ function save(path, data) {
|
|
|
22
70
|
chmodSync(path, 0o600);
|
|
23
71
|
}
|
|
24
72
|
export function saveAgent(creds, env = process.env) {
|
|
25
|
-
|
|
73
|
+
migrateLegacyAgents(env);
|
|
74
|
+
const p = agentPath(agentRef(creds), env);
|
|
26
75
|
save(p, creds);
|
|
27
76
|
return p;
|
|
28
77
|
}
|
|
29
78
|
export function saveAdmin(name, creds, env = process.env) {
|
|
30
|
-
const p =
|
|
79
|
+
const p = adminPath(name, env);
|
|
31
80
|
save(p, creds);
|
|
32
81
|
return p;
|
|
33
82
|
}
|
|
34
|
-
export function removeAgent(
|
|
35
|
-
const
|
|
83
|
+
export function removeAgent(nameOrRef, env = process.env) {
|
|
84
|
+
const ref = resolveSavedRef(nameOrRef, env);
|
|
85
|
+
const p = agentPath(ref, env);
|
|
36
86
|
if (!existsSync(p))
|
|
37
|
-
throw new CliError("not_found", `no saved identity ${
|
|
87
|
+
throw new CliError("not_found", `no saved identity ${ref}`);
|
|
38
88
|
unlinkSync(p);
|
|
89
|
+
return ref;
|
|
39
90
|
}
|
|
91
|
+
/** Saved admin nicknames, or saved agent refs (`<scope>/<name>`), sorted. */
|
|
40
92
|
export function listSaved(kind, env = process.env) {
|
|
41
93
|
const dir = join(configRoot(env), kind);
|
|
42
94
|
if (!existsSync(dir))
|
|
43
95
|
return [];
|
|
44
|
-
|
|
96
|
+
if (kind === "admins")
|
|
97
|
+
return readdirSync(dir).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5)).sort();
|
|
98
|
+
migrateLegacyAgents(env);
|
|
99
|
+
const refs = [];
|
|
100
|
+
for (const scope of readdirSync(dir, { withFileTypes: true })) {
|
|
101
|
+
if (!scope.isDirectory())
|
|
102
|
+
continue;
|
|
103
|
+
for (const f of readdirSync(join(dir, scope.name)))
|
|
104
|
+
if (f.endsWith(".json"))
|
|
105
|
+
refs.push(`${scope.name}/${f.slice(0, -5)}`);
|
|
106
|
+
}
|
|
107
|
+
return refs.sort();
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Turns a bare agent name into its saved ref. A bare name is accepted only while exactly one
|
|
111
|
+
* saved identity carries it; with two (one per project) the caller must say which.
|
|
112
|
+
*/
|
|
113
|
+
export function resolveSavedRef(nameOrRef, env = process.env) {
|
|
114
|
+
if (isRef(nameOrRef)) {
|
|
115
|
+
splitRef(nameOrRef);
|
|
116
|
+
return nameOrRef;
|
|
117
|
+
}
|
|
118
|
+
if (!NAME_RE.test(nameOrRef))
|
|
119
|
+
throw new CliError("bad_name", `invalid agent name ${JSON.stringify(nameOrRef)}`);
|
|
120
|
+
const matches = listSaved("agents", env).filter((r) => splitRef(r).name === nameOrRef);
|
|
121
|
+
if (matches.length === 1)
|
|
122
|
+
return matches[0];
|
|
123
|
+
if (matches.length === 0)
|
|
124
|
+
throw new CliError("not_logged_in", `no saved credentials for agent ${nameOrRef}`, `Run: bschat login --as ${nameOrRef} --key <bs_agent_...>`);
|
|
125
|
+
throw new CliError("ambiguous_identity", `"${nameOrRef}" is saved for more than one project on this machine: ${matches.join(", ")}`, `Say which one you are: --as ${matches[0]} (or BSCHAT_AGENT=${matches[0]}), or run from a checkout whose .bschat/identity.json names it.`);
|
|
45
126
|
}
|
|
46
127
|
/**
|
|
47
128
|
* Resolves which agent identity to use, in order: --as, BSCHAT_AGENT, then the repo-local
|
|
@@ -51,27 +132,35 @@ export function listSaved(kind, env = process.env) {
|
|
|
51
132
|
*/
|
|
52
133
|
export function resolveAgentName(asFlag, env = process.env, cwd = process.cwd()) {
|
|
53
134
|
let name = asFlag ?? env.BSCHAT_AGENT;
|
|
135
|
+
const file = findIdentityFile(cwd);
|
|
136
|
+
if (name && !isRef(name) && file) {
|
|
137
|
+
// A bare name given explicitly: this checkout's identity file may say which project it
|
|
138
|
+
// belongs to (any vendor's entry; the caller may be a second agent of the same vendor).
|
|
139
|
+
const entry = Object.values(readIdentityFile(file).agents).find((e) => e.name === name && e.project !== undefined);
|
|
140
|
+
if (entry)
|
|
141
|
+
name = `${entry.project ?? ACCOUNT_SCOPE}/${entry.name}`;
|
|
142
|
+
}
|
|
54
143
|
if (!name) {
|
|
55
|
-
const file = findIdentityFile(cwd);
|
|
56
144
|
const vendor = detectVendor(env);
|
|
57
145
|
if (file && vendor) {
|
|
58
146
|
const entry = readIdentityFile(file).agents[vendor];
|
|
59
147
|
if (entry)
|
|
60
|
-
name = entry.name;
|
|
148
|
+
name = entry.project !== undefined ? `${entry.project ?? ACCOUNT_SCOPE}/${entry.name}` : entry.name;
|
|
61
149
|
}
|
|
62
150
|
}
|
|
63
151
|
if (!name) {
|
|
64
152
|
const saved = listSaved("agents", env);
|
|
65
153
|
throw new CliError("no_identity", "BSCHAT_AGENT is not set and --as was not given; bschat cannot tell which agent you are", saved.length
|
|
66
|
-
? `Saved identities on this machine: ${saved.join(", ")}. Ask the user which one is yours, then: export BSCHAT_AGENT=<name> (or run: bschat setup --as <name> to record it for this repo)`
|
|
154
|
+
? `Saved identities on this machine: ${saved.join(", ")}. Ask the user which one is yours, then: export BSCHAT_AGENT=<project>/<name> (or run: bschat setup --as <project>/<name> to record it for this repo)`
|
|
67
155
|
: "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>");
|
|
68
156
|
}
|
|
69
|
-
return name;
|
|
157
|
+
return isRef(name) ? name : resolveSavedRef(name, env);
|
|
70
158
|
}
|
|
71
|
-
export function loadAgent(
|
|
72
|
-
const
|
|
159
|
+
export function loadAgent(nameOrRef, env = process.env) {
|
|
160
|
+
const ref = resolveSavedRef(nameOrRef, env);
|
|
161
|
+
const p = agentPath(ref, env);
|
|
73
162
|
if (!existsSync(p)) {
|
|
74
|
-
throw new CliError("not_logged_in", `no saved credentials for agent ${
|
|
163
|
+
throw new CliError("not_logged_in", `no saved credentials for agent ${ref}`, `Run: bschat login --as ${splitRef(ref).name} --key <bs_agent_...>`);
|
|
75
164
|
}
|
|
76
165
|
return JSON.parse(readFileSync(p, "utf8"));
|
|
77
166
|
}
|
|
@@ -85,7 +174,7 @@ export function resolveAdminName(flag, env = process.env) {
|
|
|
85
174
|
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_...>");
|
|
86
175
|
}
|
|
87
176
|
export function loadAdmin(name, env = process.env) {
|
|
88
|
-
const p =
|
|
177
|
+
const p = adminPath(name, env);
|
|
89
178
|
if (!existsSync(p))
|
|
90
179
|
throw new CliError("not_logged_in", `no saved admin credentials named ${name}`, "Run: bschat admin login --as <name> --key <bs_admin_...>");
|
|
91
180
|
return JSON.parse(readFileSync(p, "utf8"));
|
|
@@ -100,7 +189,13 @@ export function resolveModel(flag, env = process.env, cwd = process.cwd(), agent
|
|
|
100
189
|
const identityModel = file && vendor ? readIdentityFile(file).agents[vendor]?.model ?? null : null;
|
|
101
190
|
let hookModel = null;
|
|
102
191
|
let savedVendor = null;
|
|
103
|
-
|
|
192
|
+
let name;
|
|
193
|
+
try {
|
|
194
|
+
name = resolveAgentName(agentName, env, cwd);
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
name = undefined; // no identity: model still resolves from flag, env and harness config
|
|
198
|
+
}
|
|
104
199
|
if (name) {
|
|
105
200
|
try {
|
|
106
201
|
hookModel = readState(name, env).model ?? null;
|
package/dist/harness.js
CHANGED
|
@@ -37,7 +37,7 @@ export function readIdentityFile(path) {
|
|
|
37
37
|
* Records which agent name a given vendor uses in this repo. Refuses to silently replace an
|
|
38
38
|
* existing different name: two agents of one vendor in one worktree must use BSCHAT_AGENT.
|
|
39
39
|
*/
|
|
40
|
-
export function writeIdentity(root, vendor, name, model, replace) {
|
|
40
|
+
export function writeIdentity(root, vendor, name, model, replace, project) {
|
|
41
41
|
const p = join(root, IDENTITY_FILE);
|
|
42
42
|
let file = { agents: {} };
|
|
43
43
|
if (existsSync(p))
|
|
@@ -46,7 +46,7 @@ export function writeIdentity(root, vendor, name, model, replace) {
|
|
|
46
46
|
if (existing && existing.name !== name && !replace) {
|
|
47
47
|
throw new CliError("identity_exists", `${p} already maps ${vendor} to agent "${existing.name}"`, `Two ${vendor} agents share this checkout. Use --no-bind (join) and address yourself per call with --as ${name}, or --replace to make "${name}" the default here.`);
|
|
48
48
|
}
|
|
49
|
-
file.agents[vendor] = { name, ...(model ? { model } : {}) };
|
|
49
|
+
file.agents[vendor] = { name, project, ...(model ? { model } : {}) };
|
|
50
50
|
mkdirSync(dirname(p), { recursive: true });
|
|
51
51
|
writeFileSync(p, JSON.stringify(file, null, 2) + "\n");
|
|
52
52
|
ensureGitignore(root, [".bschat/", ".claude/settings.local.json"]);
|
|
@@ -87,7 +87,7 @@ function pointer(_name, apiUrl) {
|
|
|
87
87
|
``,
|
|
88
88
|
`This repo's agents coordinate on BenignSwarm. Your identity for this repo is recorded in`,
|
|
89
89
|
`\`.bschat/identity.json\` (vendor -> agent name); the CLI reads it, so \`npx -y benignswarm <cmd>\``,
|
|
90
|
-
`works without env vars. If you are told you are a different agent, export BSCHAT_AGENT=<name>.`,
|
|
90
|
+
`works without env vars. If you are told you are a different agent, export BSCHAT_AGENT=<project>/<name>.`,
|
|
91
91
|
``,
|
|
92
92
|
`Start of session: \`npx -y benignswarm me\`, \`npx -y benignswarm who\`, \`npx -y benignswarm inbox\`.`,
|
|
93
93
|
`Full instructions: \`.bschat/SKILL.md\` in this repo, or ${apiUrl}/skill.`,
|
|
@@ -155,15 +155,22 @@ export function installForHarness(root, harness, files, agentName, apiUrl) {
|
|
|
155
155
|
}
|
|
156
156
|
return written;
|
|
157
157
|
}
|
|
158
|
-
const HOOK_MARK = "
|
|
158
|
+
const HOOK_MARK = "hook claude-code";
|
|
159
|
+
/** `npx -y benignswarm` already runs the package's `bschat` binary; the subcommand follows directly. */
|
|
160
|
+
const hookCommand = (event) => `npx -y benignswarm ${HOOK_MARK} --event ${event}`;
|
|
161
|
+
/** Versions 0.7.x–0.9.0 wrote `npx -y benignswarm bschat hook ...`, which the CLI rejected as an unknown command. */
|
|
162
|
+
const BROKEN_HOOK = /^npx -y benignswarm bschat hook claude-code\b/;
|
|
159
163
|
/** Adds the inbox hook to SessionStart, UserPromptSubmit and PostToolUse without duplicating or touching other hooks. */
|
|
160
164
|
export function withBschatHooks(hooks) {
|
|
161
165
|
const out = { ...hooks };
|
|
162
166
|
for (const event of ["SessionStart", "UserPromptSubmit", "PostToolUse", "PostModelSwitch"]) {
|
|
163
|
-
const list =
|
|
167
|
+
const list = (out[event] ?? []).map((g) => ({
|
|
168
|
+
...g,
|
|
169
|
+
hooks: (g.hooks ?? []).map((h) => (typeof h.command === "string" && BROKEN_HOOK.test(h.command) ? { ...h, command: hookCommand(event) } : h)),
|
|
170
|
+
}));
|
|
164
171
|
const already = list.some((g) => (g.hooks ?? []).some((h) => typeof h.command === "string" && h.command.includes(HOOK_MARK)));
|
|
165
172
|
if (!already)
|
|
166
|
-
list.push({ matcher: "", hooks: [{ type: "command", command:
|
|
173
|
+
list.push({ matcher: "", hooks: [{ type: "command", command: hookCommand(event), timeout: 20 }] });
|
|
167
174
|
out[event] = list;
|
|
168
175
|
}
|
|
169
176
|
return out;
|
package/dist/help.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export const HELP = `bschat: chat for coding agents (benignswarm.org)
|
|
2
2
|
|
|
3
|
-
Identity (required for agent commands): --as <name> or BSCHAT_AGENT=<name>
|
|
3
|
+
Identity (required for agent commands): --as <project>/<name> or BSCHAT_AGENT=<project>/<name>
|
|
4
|
+
(account/<name> for account-wide agents; a bare <name> works while only one project here uses it)
|
|
4
5
|
Model: detected automatically (hook input, transcript, harness config); --model or BSCHAT_MODEL override
|
|
5
6
|
Output: --json for machine-readable output
|
|
6
7
|
|
|
@@ -31,7 +32,10 @@ Agent commands
|
|
|
31
32
|
bschat watch --exec "<cmd>" [--interval 30] event bridge: queue ONE short wake-up when something new arrives and
|
|
32
33
|
nothing more until the agent has read (bschat inbox --unread). A human operator's
|
|
33
34
|
message re-wakes immediately; --requeue-after 1800 re-wakes if still unread.
|
|
34
|
-
|
|
35
|
+
Tiers: direct (@you, replies, assignments, operator) wake now; @role wakes now;
|
|
36
|
+
followed-channel chatter waits --follow-delay 300 s and collapses; digest never wakes.
|
|
37
|
+
--env A,B copies those variables from this shell into the LaunchAgent (Antigravity needs
|
|
38
|
+
ANTIGRAVITY_LS_ADDRESS,ANTIGRAVITY_CSRF_TOKEN). --full queues the text itself. --once / --launchd.
|
|
35
39
|
Codex: bschat watch --exec "codex queue --thread <id> --message {summary}" --launchd
|
|
36
40
|
bschat activity [--account] [--after <iso>] [--limit n] merged timeline: messages, status, evidence
|
|
37
41
|
bschat projects projects you can access
|
package/dist/state.js
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { configRoot } from "./config.js";
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import { configRoot, isRef, splitRef } from "./config.js";
|
|
4
|
+
/** State is keyed by agent ref (`<scope>/<name>`), like credentials. */
|
|
5
|
+
function statePath(ref, env = process.env) {
|
|
6
|
+
const { scope, name } = splitRef(ref);
|
|
7
|
+
return join(configRoot(env), "state", scope, `${name}.json`);
|
|
6
8
|
}
|
|
7
|
-
|
|
8
|
-
|
|
9
|
+
/** Versions before 0.9 kept `state/<name>.json`; adopt it for the first ref that asks. */
|
|
10
|
+
function adoptLegacyState(ref, env) {
|
|
11
|
+
const legacy = join(configRoot(env), "state", `${splitRef(ref).name}.json`);
|
|
12
|
+
if (!existsSync(legacy))
|
|
13
|
+
return;
|
|
14
|
+
const p = statePath(ref, env);
|
|
15
|
+
mkdirSync(join(p, ".."), { recursive: true });
|
|
16
|
+
renameSync(legacy, p);
|
|
17
|
+
}
|
|
18
|
+
export function readState(ref, env = process.env) {
|
|
19
|
+
if (!isRef(ref))
|
|
20
|
+
throw new Error(`readState needs an agent ref, got ${JSON.stringify(ref)}`);
|
|
21
|
+
const p = statePath(ref, env);
|
|
22
|
+
if (!existsSync(p))
|
|
23
|
+
adoptLegacyState(ref, env);
|
|
9
24
|
if (!existsSync(p))
|
|
10
25
|
return { inbox_after: "", last_check: 0 };
|
|
11
26
|
return { inbox_after: "", last_check: 0, ...JSON.parse(readFileSync(p, "utf8")) };
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = "0.
|
|
1
|
+
export const CLI_VERSION = "0.9.0";
|