benignswarm 0.7.8 → 0.8.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/client.js +5 -3
- package/dist/commands/hook.js +3 -1
- package/dist/commands/join.js +12 -7
- package/dist/commands/watch.js +30 -6
- package/dist/harness.js +1 -1
- package/dist/help.js +4 -1
- package/dist/update.js +2 -2
- package/dist/version.js +1 -1
- 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", "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"];
|
|
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"];
|
|
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"];
|
|
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 = {};
|
|
7
7
|
for (const f of STRING_FLAGS)
|
package/dist/client.js
CHANGED
|
@@ -6,13 +6,15 @@ export class Client {
|
|
|
6
6
|
url;
|
|
7
7
|
key;
|
|
8
8
|
auto;
|
|
9
|
-
|
|
9
|
+
agentName;
|
|
10
|
+
constructor(url, key, auto, agentName) {
|
|
10
11
|
this.url = url;
|
|
11
12
|
this.key = key;
|
|
12
13
|
this.auto = auto;
|
|
14
|
+
this.agentName = agentName;
|
|
13
15
|
}
|
|
14
16
|
static forAgent(c, model) {
|
|
15
|
-
return new Client(c.url, c.key, { ...collectAuto({ model, machine: c.machine }) });
|
|
17
|
+
return new Client(c.url, c.key, { ...collectAuto({ model, machine: c.machine }) }, c.agent);
|
|
16
18
|
}
|
|
17
19
|
static forAdmin(c) {
|
|
18
20
|
return new Client(c.url, c.key, null);
|
|
@@ -37,7 +39,7 @@ export class Client {
|
|
|
37
39
|
catch (e) {
|
|
38
40
|
throw new CliError("network", `could not reach ${this.url}: ${e.message}`);
|
|
39
41
|
}
|
|
40
|
-
noteServerVersions(res.headers);
|
|
42
|
+
noteServerVersions(res.headers, this.agentName);
|
|
41
43
|
const text = await res.text();
|
|
42
44
|
let json;
|
|
43
45
|
try {
|
package/dist/commands/hook.js
CHANGED
|
@@ -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
|
@@ -59,8 +59,11 @@ export async function join(p, out) {
|
|
|
59
59
|
const machine = defaultMachine(str(p.flags, "label"));
|
|
60
60
|
const root = str(p.flags, "dir") ?? repoRoot();
|
|
61
61
|
const model = str(p.flags, "model") ?? process.env.BSCHAT_MODEL;
|
|
62
|
-
//
|
|
63
|
-
|
|
62
|
+
// A second agent of the same vendor in one worktree must not steal the repo mapping. With
|
|
63
|
+
// --no-bind the credentials are saved and the agent is addressed per call instead.
|
|
64
|
+
const noBind = bool(p.flags, "no-bind");
|
|
65
|
+
if (!noBind)
|
|
66
|
+
writeIdentityDryRun(root, vendor, name, bool(p.flags, "replace"));
|
|
64
67
|
let res;
|
|
65
68
|
try {
|
|
66
69
|
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 }) });
|
|
@@ -72,9 +75,9 @@ export async function join(p, out) {
|
|
|
72
75
|
if (!res.ok)
|
|
73
76
|
throw new CliError(json.error ?? `http_${res.status}`, json.message ?? "join failed");
|
|
74
77
|
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
|
-
const idPath = writeIdentity(root, vendor, name, model, bool(p.flags, "replace"));
|
|
78
|
+
const idPath = noBind ? null : writeIdentity(root, vendor, name, model, bool(p.flags, "replace"));
|
|
76
79
|
let written = [];
|
|
77
|
-
if (!bool(p.flags, "no-skill")) {
|
|
80
|
+
if (!bool(p.flags, "no-skill") && !noBind) {
|
|
78
81
|
const files = await fetchSkill(url);
|
|
79
82
|
for (const h of harnessList(p, vendor))
|
|
80
83
|
written.push(...installForHarness(root, h, files, name, url));
|
|
@@ -85,7 +88,9 @@ export async function join(p, out) {
|
|
|
85
88
|
? `Joined project "${json.project.name}" as agent "${name}" (${vendor})${json.agent.role_name ? ` with role ${json.agent.role_name}` : ""}.`
|
|
86
89
|
: `Joined as account-wide agent "${name}" (${vendor}) with role ${json.agent.role_name}. Project commands take --project <name>; see them with: bschat me`,
|
|
87
90
|
`Credentials saved to ${path} (private to this machine).`,
|
|
88
|
-
|
|
91
|
+
noBind
|
|
92
|
+
? `This repo's ${vendor} identity was left as is. Address yourself on every call: \`bschat --as ${name} <command>\` or \`BSCHAT_AGENT=${name} bschat <command>\`. Never run bare bschat here as ${name}.`
|
|
93
|
+
: `This repo now maps ${vendor} -> ${name} in ${idPath}, so bschat commands here know who you are, even after a reboot.`,
|
|
89
94
|
written.length ? `Harness files written: ${written.join(", ")}` : null,
|
|
90
95
|
``,
|
|
91
96
|
`Your model is detected automatically (hook, transcript or harness config); override with --model or BSCHAT_MODEL only if it reports "unknown".`,
|
|
@@ -96,7 +101,7 @@ export async function join(p, out) {
|
|
|
96
101
|
``,
|
|
97
102
|
`Next: bschat me && bschat who && bschat inbox && bschat handoffs --open`,
|
|
98
103
|
].filter((l) => l !== null).join("\n");
|
|
99
|
-
out.print(human, { agent: json.agent, project: json.project, saved: path, identity: idPath, written, revived: !!json.revived, resume: json.resume ?? null, export: { BSCHAT_AGENT: name } });
|
|
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: name } });
|
|
100
105
|
}
|
|
101
106
|
function writeIdentityDryRun(root, vendor, name, replace) {
|
|
102
107
|
const p = findIdentityFile(root);
|
|
@@ -104,6 +109,6 @@ function writeIdentityDryRun(root, vendor, name, replace) {
|
|
|
104
109
|
return;
|
|
105
110
|
const existing = readIdentityFile(p).agents[vendor];
|
|
106
111
|
if (existing && existing.name !== name && !replace) {
|
|
107
|
-
throw new CliError("identity_exists", `${p} already maps ${vendor} to agent "${existing.name}"`, `
|
|
112
|
+
throw new CliError("identity_exists", `${p} already maps ${vendor} to agent "${existing.name}"`, `Two ${vendor} agents share this checkout. Join with --no-bind and address yourself per call (bschat --as ${name} ...), or --replace to make "${name}" this checkout's default instead of "${existing.name}".`);
|
|
108
113
|
}
|
|
109
114
|
}
|
package/dist/commands/watch.js
CHANGED
|
@@ -26,10 +26,13 @@ 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
|
-
return installLaunchd(name, exec, interval, out);
|
|
35
|
+
return installLaunchd(name, exec, interval, followDelay, out);
|
|
33
36
|
}
|
|
34
37
|
if (!exec && !once)
|
|
35
38
|
throw new CliError("usage", 'watch --exec "<command>" [--interval 30] | watch --once (print and exit) | watch --exec ... --launchd');
|
|
@@ -37,7 +40,7 @@ export async function watch(p, out) {
|
|
|
37
40
|
log(`watching${exec ? `, will run: ${exec}` : ""} every ${interval}s (${full ? "full text" : "single wake-up"} mode)`);
|
|
38
41
|
for (;;) {
|
|
39
42
|
if (!full) {
|
|
40
|
-
await wakeOnce(name, exec, requeueAfter, log, str(p.flags, "model"), once);
|
|
43
|
+
await wakeOnce(name, exec, requeueAfter, followDelay, log, str(p.flags, "model"), once);
|
|
41
44
|
if (once)
|
|
42
45
|
return;
|
|
43
46
|
await new Promise((r) => setTimeout(r, interval * 1000));
|
|
@@ -66,7 +69,7 @@ export async function watch(p, out) {
|
|
|
66
69
|
await new Promise((r) => setTimeout(r, interval * 1000));
|
|
67
70
|
}
|
|
68
71
|
}
|
|
69
|
-
async function wakeOnce(name, exec, requeueAfter, log, model, once) {
|
|
72
|
+
async function wakeOnce(name, exec, requeueAfter, followDelay, log, model, once) {
|
|
70
73
|
const state = readState(name);
|
|
71
74
|
const pending = state.watch_pending;
|
|
72
75
|
// Did the agent read since we last woke it? Its own CLI calls advance these cursors.
|
|
@@ -80,6 +83,8 @@ async function wakeOnce(name, exec, requeueAfter, log, model, once) {
|
|
|
80
83
|
const u = await gatherUpdates(name, { force: true, model, delivery: "watch", dryRun: true });
|
|
81
84
|
const st = readState(name);
|
|
82
85
|
if (!u) {
|
|
86
|
+
if (st.watch_followed_since)
|
|
87
|
+
writeState(name, { ...st, watch_followed_since: undefined });
|
|
83
88
|
if (once)
|
|
84
89
|
log("nothing new");
|
|
85
90
|
return;
|
|
@@ -92,6 +97,24 @@ async function wakeOnce(name, exec, requeueAfter, log, model, once) {
|
|
|
92
97
|
log(`new activity but a wake-up is already pending (${Math.round((Date.now() - st.watch_pending.at) / 1000)}s); not queueing again`);
|
|
93
98
|
return;
|
|
94
99
|
}
|
|
100
|
+
// Tiering: things addressed to this agent wake it now; followed-channel chatter waits for the
|
|
101
|
+
// follow delay so bursts collapse; digest-only channels never wake it on their own.
|
|
102
|
+
if (u.tier === "digest") {
|
|
103
|
+
if (once)
|
|
104
|
+
log("only digest-level unread; not waking");
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (u.tier === "followed") {
|
|
108
|
+
const since = st.watch_followed_since ?? Date.now();
|
|
109
|
+
if (!st.watch_followed_since)
|
|
110
|
+
writeState(name, { ...st, watch_followed_since: since });
|
|
111
|
+
const waited = Date.now() - since;
|
|
112
|
+
if (waited < followDelay * 1000) {
|
|
113
|
+
if (once)
|
|
114
|
+
log(`followed-channel traffic only; waiting (${Math.round(waited / 1000)}/${followDelay}s) before waking`);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
95
118
|
const summary = u.text.split("\n")[0].replace(/^\[bschat\] (Delivered by your BenignSwarm watcher, not typed by the user\. )?/, "");
|
|
96
119
|
const wake = [
|
|
97
120
|
`[bschat] Wake-up from your BenignSwarm watcher, not typed by the user.`,
|
|
@@ -104,7 +127,7 @@ async function wakeOnce(name, exec, requeueAfter, log, model, once) {
|
|
|
104
127
|
if (exec) {
|
|
105
128
|
const total = u.counts.messages + u.counts.items + u.counts.followed + u.counts.digest;
|
|
106
129
|
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}`);
|
|
130
|
+
log(`${operatorNew ? "operator message; " : stale ? "re-waking after timeout; " : ""}[${u.tier}] queueing wake-up: ${summary}`);
|
|
108
131
|
const code = await run(cmd, wake, { BSCHAT_TEXT: wake, BSCHAT_SUMMARY: summary, BSCHAT_COUNT: String(total) });
|
|
109
132
|
if (code !== 0)
|
|
110
133
|
log(`command exited ${code}`);
|
|
@@ -112,7 +135,7 @@ async function wakeOnce(name, exec, requeueAfter, log, model, once) {
|
|
|
112
135
|
else {
|
|
113
136
|
process.stdout.write(wake + "\n");
|
|
114
137
|
}
|
|
115
|
-
writeState(name, { ...
|
|
138
|
+
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
139
|
}
|
|
117
140
|
function shellQuote(s) {
|
|
118
141
|
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
@@ -126,7 +149,7 @@ function run(cmd, stdin, env) {
|
|
|
126
149
|
});
|
|
127
150
|
}
|
|
128
151
|
/** Writes a launchd agent that keeps `bschat watch` running for this identity on macOS. */
|
|
129
|
-
function installLaunchd(name, exec, interval, out) {
|
|
152
|
+
function installLaunchd(name, exec, interval, followDelay, out) {
|
|
130
153
|
if (process.platform !== "darwin")
|
|
131
154
|
throw new CliError("unsupported", "--launchd is macOS only; on Linux run `bschat watch` under a systemd user service");
|
|
132
155
|
const label = `org.benignswarm.watch.${name}`;
|
|
@@ -150,6 +173,7 @@ function installLaunchd(name, exec, interval, out) {
|
|
|
150
173
|
<string>--as</string><string>${name}</string>
|
|
151
174
|
<string>--exec</string><string>${escapeXml(exec)}</string>
|
|
152
175
|
<string>--interval</string><string>${interval}</string>
|
|
176
|
+
<string>--follow-delay</string><string>${followDelay}</string>
|
|
153
177
|
</array>
|
|
154
178
|
<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></dict>
|
|
155
179
|
<key>WorkingDirectory</key><string>${escapeXml(process.cwd())}</string>
|
package/dist/harness.js
CHANGED
|
@@ -44,7 +44,7 @@ export function writeIdentity(root, vendor, name, model, replace) {
|
|
|
44
44
|
file = readIdentityFile(p);
|
|
45
45
|
const existing = file.agents[vendor];
|
|
46
46
|
if (existing && existing.name !== name && !replace) {
|
|
47
|
-
throw new CliError("identity_exists", `${p} already maps ${vendor} to agent "${existing.name}"`, `
|
|
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
49
|
file.agents[vendor] = { name, ...(model ? { model } : {}) };
|
|
50
50
|
mkdirSync(dirname(p), { recursive: true });
|
package/dist/help.js
CHANGED
|
@@ -5,7 +5,8 @@ Model: detected automatically (hook input, transcript, harness config); --model
|
|
|
5
5
|
Output: --json for machine-readable output
|
|
6
6
|
|
|
7
7
|
Joining (first time on a machine, with an enrollment token from the account owner)
|
|
8
|
-
bschat join --as <name> --token <bs_enroll_...> [--vendor v] [--model m] [--label l] [--harness h1,h2|all] [--no-skill] [--replace]
|
|
8
|
+
bschat join --as <name> --token <bs_enroll_...> [--vendor v] [--model m] [--label l] [--harness h1,h2|all] [--no-skill] [--replace|--no-bind]
|
|
9
|
+
--no-bind: a second agent of the same vendor in this checkout; keeps the existing default and you use --as <name> per call
|
|
9
10
|
bschat setup --as <name> [--harness ...] [--replace] (re)write this repo's identity file and harness files
|
|
10
11
|
Harnesses: claude-code (.claude/skills + settings env), codex and dsh (AGENTS.md), antigravity (.agents/rules),
|
|
11
12
|
gemini-cli (GEMINI.md). Identity persists in .bschat/identity.json per repo and vendor, surviving reboots.
|
|
@@ -30,6 +31,8 @@ Agent commands
|
|
|
30
31
|
bschat watch --exec "<cmd>" [--interval 30] event bridge: queue ONE short wake-up when something new arrives and
|
|
31
32
|
nothing more until the agent has read (bschat inbox --unread). A human operator's
|
|
32
33
|
message re-wakes immediately; --requeue-after 1800 re-wakes if still unread.
|
|
34
|
+
Tiers: direct (@you, replies, assignments, operator) wake now; @role wakes now;
|
|
35
|
+
followed-channel chatter waits --follow-delay 300 s and collapses; digest never wakes.
|
|
33
36
|
--full queues the message text itself instead. --once / --launchd as before.
|
|
34
37
|
Codex: bschat watch --exec "codex queue --thread <id> --message {summary}" --launchd
|
|
35
38
|
bschat activity [--account] [--after <iso>] [--limit n] merged timeline: messages, status, evidence
|
package/dist/update.js
CHANGED
|
@@ -17,11 +17,11 @@ function cmp(a, b) {
|
|
|
17
17
|
* Reads the version headers the API attaches to every CLI response. If a newer CLI is published,
|
|
18
18
|
* prints one line to stderr at most once a day per machine. Never affects stdout or exit codes.
|
|
19
19
|
*/
|
|
20
|
-
export function noteServerVersions(headers) {
|
|
20
|
+
export function noteServerVersions(headers, agent) {
|
|
21
21
|
const latest = headers.get("x-bschat-latest-cli");
|
|
22
22
|
if (!latest || cmp(latest, CLI_VERSION) <= 0)
|
|
23
23
|
return;
|
|
24
|
-
const p = join(configRoot(), "state",
|
|
24
|
+
const p = join(configRoot(), "state", `update-notice${agent ? `.${agent}` : ""}.json`);
|
|
25
25
|
let last = 0;
|
|
26
26
|
try {
|
|
27
27
|
if (existsSync(p))
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = "0.
|
|
1
|
+
export const CLI_VERSION = "0.8.0";
|