benignswarm 0.7.4 → 0.7.5
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/hook.js +16 -2
- package/dist/commands/watch.js +66 -4
- package/dist/help.js +4 -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"];
|
|
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"];
|
|
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"];
|
|
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"];
|
|
5
5
|
export function parse(argv) {
|
|
6
6
|
const options = {};
|
|
7
7
|
for (const f of STRING_FLAGS)
|
package/dist/commands/hook.js
CHANGED
|
@@ -59,6 +59,7 @@ export async function gatherUpdates(name, opts) {
|
|
|
59
59
|
const follows = new Set(state.follows ?? []);
|
|
60
60
|
const digest = [];
|
|
61
61
|
const followed = [];
|
|
62
|
+
const readCommands = [];
|
|
62
63
|
try {
|
|
63
64
|
const projects = creds.project ? [creds.project] : ((await client.call("GET", "/me")).projects ?? []).map((x) => x.name);
|
|
64
65
|
for (const project of projects) {
|
|
@@ -73,6 +74,7 @@ export async function gatherUpdates(name, opts) {
|
|
|
73
74
|
const unread = ch.cursor - last;
|
|
74
75
|
if (unread <= 0)
|
|
75
76
|
continue;
|
|
77
|
+
readCommands.push(`bschat read ${creds.project ? "" : `--project ${project} `}${ch.name} --since ${last}`);
|
|
76
78
|
if (follows.has(key)) {
|
|
77
79
|
const r = await client.call("GET", `/projects/${project}/channels/${ch.name}/messages?since=${last}&limit=5`);
|
|
78
80
|
const seen = new Set(messages.map((m) => m.id));
|
|
@@ -94,7 +96,17 @@ export async function gatherUpdates(name, opts) {
|
|
|
94
96
|
process.stderr.write(`bschat: channels: ${e.message}\n`);
|
|
95
97
|
}
|
|
96
98
|
next.channel_read = channelRead;
|
|
97
|
-
|
|
99
|
+
if (opts.dryRun) {
|
|
100
|
+
// Persist only the baselines of channels never seen before; everything else stays unread.
|
|
101
|
+
const merged = { ...(state.channel_read ?? {}) };
|
|
102
|
+
for (const [k, v] of Object.entries(channelRead))
|
|
103
|
+
if (merged[k] === undefined)
|
|
104
|
+
merged[k] = v;
|
|
105
|
+
writeState(name, { ...state, channel_read: merged });
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
writeState(name, next);
|
|
109
|
+
}
|
|
98
110
|
if (!messages.length && !items.length && !digest.length && !followed.length)
|
|
99
111
|
return null;
|
|
100
112
|
const lines = messages.map((m) => `${creds.project ? "" : `[${m.project}] `}${fmtMessage(m)}${m.reply_to_seq ? ` (reply to #${m.reply_to_seq})` : ""}`);
|
|
@@ -123,7 +135,9 @@ export async function gatherUpdates(name, opts) {
|
|
|
123
135
|
...(digest.length ? [`Unread elsewhere: ${digest.join("; ")}`] : []),
|
|
124
136
|
...footer,
|
|
125
137
|
].join("\n");
|
|
126
|
-
|
|
138
|
+
const operator = messages.filter((m) => m.role_name === "human").map((m) => m.created_at).sort().pop() ?? null;
|
|
139
|
+
const cmds = [...(messages.length || items.length ? ["bschat inbox --unread"] : []), ...readCommands];
|
|
140
|
+
return { text, counts: { messages: messages.length, items: items.length, followed: followed.length, digest: digest.length }, latestOperatorAt: operator, readCommands: cmds };
|
|
127
141
|
}
|
|
128
142
|
/**
|
|
129
143
|
* Harness hook entry point. Prints new items in the format the harness expects for injecting
|
package/dist/commands/watch.js
CHANGED
|
@@ -6,11 +6,16 @@ import { bool, str } from "../args.js";
|
|
|
6
6
|
import { resolveAgentName } from "../config.js";
|
|
7
7
|
import { CliError } from "../errors.js";
|
|
8
8
|
import { gatherUpdates } from "./hook.js";
|
|
9
|
+
import { readState, writeState } from "../state.js";
|
|
9
10
|
/**
|
|
10
11
|
* Event bridge for harnesses that can be woken from outside (Codex: `codex queue --thread <id>`).
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
*
|
|
13
|
+
* Default mode queues ONE short wake-up and nothing else until the agent has read (its own
|
|
14
|
+
* `bschat inbox --unread` / `bschat read` advance the shared cursors). Content is never put in
|
|
15
|
+
* the queue, so the agent always reads it fresh and superseded messages collapse naturally.
|
|
16
|
+
* A direct message from a human operator re-wakes immediately even while a wake-up is pending.
|
|
17
|
+
* `--full` restores the old behaviour of queueing the text itself (for harnesses that cannot run
|
|
18
|
+
* bschat themselves when woken).
|
|
14
19
|
*/
|
|
15
20
|
export async function watch(p, out) {
|
|
16
21
|
const name = resolveAgentName(str(p.flags, "as"));
|
|
@@ -19,6 +24,8 @@ export async function watch(p, out) {
|
|
|
19
24
|
if (!Number.isInteger(interval) || interval < 5)
|
|
20
25
|
throw new CliError("usage", "--interval must be an integer number of seconds, at least 5");
|
|
21
26
|
const once = bool(p.flags, "once");
|
|
27
|
+
const full = bool(p.flags, "full");
|
|
28
|
+
const requeueAfter = Number(str(p.flags, "requeue-after") ?? "1800");
|
|
22
29
|
if (bool(p.flags, "launchd")) {
|
|
23
30
|
if (!exec)
|
|
24
31
|
throw new CliError("usage", "--launchd needs --exec so the agent knows what to run");
|
|
@@ -27,8 +34,15 @@ export async function watch(p, out) {
|
|
|
27
34
|
if (!exec && !once)
|
|
28
35
|
throw new CliError("usage", 'watch --exec "<command>" [--interval 30] | watch --once (print and exit) | watch --exec ... --launchd');
|
|
29
36
|
const log = (m) => process.stderr.write(`${new Date().toISOString()} bschat watch[${name}]: ${m}\n`);
|
|
30
|
-
log(`watching${exec ? `, will run: ${exec}` : ""} every ${interval}s`);
|
|
37
|
+
log(`watching${exec ? `, will run: ${exec}` : ""} every ${interval}s (${full ? "full text" : "single wake-up"} mode)`);
|
|
31
38
|
for (;;) {
|
|
39
|
+
if (!full) {
|
|
40
|
+
await wakeOnce(name, exec, requeueAfter, log, str(p.flags, "model"), once);
|
|
41
|
+
if (once)
|
|
42
|
+
return;
|
|
43
|
+
await new Promise((r) => setTimeout(r, interval * 1000));
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
32
46
|
const u = await gatherUpdates(name, { force: true, model: str(p.flags, "model"), delivery: "watch" });
|
|
33
47
|
if (u) {
|
|
34
48
|
const total = u.counts.messages + u.counts.items + u.counts.followed + u.counts.digest;
|
|
@@ -52,6 +66,54 @@ export async function watch(p, out) {
|
|
|
52
66
|
await new Promise((r) => setTimeout(r, interval * 1000));
|
|
53
67
|
}
|
|
54
68
|
}
|
|
69
|
+
async function wakeOnce(name, exec, requeueAfter, log, model, once) {
|
|
70
|
+
const state = readState(name);
|
|
71
|
+
const pending = state.watch_pending;
|
|
72
|
+
// Did the agent read since we last woke it? Its own CLI calls advance these cursors.
|
|
73
|
+
if (pending) {
|
|
74
|
+
const readSince = state.inbox_after !== pending.inbox_after || JSON.stringify(state.channel_read ?? {}) !== JSON.stringify(pending.channel_read);
|
|
75
|
+
if (readSince) {
|
|
76
|
+
writeState(name, { ...state, watch_pending: undefined });
|
|
77
|
+
log("agent has read since the last wake-up; pending cleared");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const u = await gatherUpdates(name, { force: true, model, delivery: "watch", dryRun: true });
|
|
81
|
+
const st = readState(name);
|
|
82
|
+
if (!u) {
|
|
83
|
+
if (once)
|
|
84
|
+
log("nothing new");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const operatorNew = u.latestOperatorAt !== null && u.latestOperatorAt !== (st.watch_operator_at ?? null);
|
|
88
|
+
const stillPending = !!st.watch_pending;
|
|
89
|
+
const stale = stillPending && Date.now() - st.watch_pending.at > requeueAfter * 1000;
|
|
90
|
+
if (stillPending && !operatorNew && !stale) {
|
|
91
|
+
if (once)
|
|
92
|
+
log(`new activity but a wake-up is already pending (${Math.round((Date.now() - st.watch_pending.at) / 1000)}s); not queueing again`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const summary = u.text.split("\n")[0].replace(/^\[bschat\] (Delivered by your BenignSwarm watcher, not typed by the user\. )?/, "");
|
|
96
|
+
const wake = [
|
|
97
|
+
`[bschat] Wake-up from your BenignSwarm watcher, not typed by the user.`,
|
|
98
|
+
operatorNew ? `A human operator wrote to you directly.` : null,
|
|
99
|
+
`New since you last looked: ${summary}`,
|
|
100
|
+
`Run these, in order, to read it all:`,
|
|
101
|
+
...u.readCommands.map((c) => ` ${c}`),
|
|
102
|
+
`Reply in chat with bschat send, not here.`,
|
|
103
|
+
].filter(Boolean).join("\n");
|
|
104
|
+
if (exec) {
|
|
105
|
+
const total = u.counts.messages + u.counts.items + u.counts.followed + u.counts.digest;
|
|
106
|
+
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}`);
|
|
108
|
+
const code = await run(cmd, wake, { BSCHAT_TEXT: wake, BSCHAT_SUMMARY: summary, BSCHAT_COUNT: String(total) });
|
|
109
|
+
if (code !== 0)
|
|
110
|
+
log(`command exited ${code}`);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
process.stdout.write(wake + "\n");
|
|
114
|
+
}
|
|
115
|
+
writeState(name, { ...st, watch_pending: { at: Date.now(), inbox_after: st.inbox_after, channel_read: st.channel_read ?? {} }, watch_operator_at: u.latestOperatorAt ?? st.watch_operator_at });
|
|
116
|
+
}
|
|
55
117
|
function shellQuote(s) {
|
|
56
118
|
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
57
119
|
}
|
package/dist/help.js
CHANGED
|
@@ -26,8 +26,10 @@ Agent commands
|
|
|
26
26
|
bschat follow <channel> | unfollow <channel> | following followed channels' new messages are injected by the hook;
|
|
27
27
|
other channels appear as a one-line unread digest
|
|
28
28
|
bschat hook claude-code [--event E] harness hook: prints new inbox items as hook JSON (installed by join)
|
|
29
|
-
bschat watch --exec "<cmd>" [--interval 30] event bridge:
|
|
30
|
-
|
|
29
|
+
bschat watch --exec "<cmd>" [--interval 30] event bridge: queue ONE short wake-up when something new arrives and
|
|
30
|
+
nothing more until the agent has read (bschat inbox --unread). A human operator's
|
|
31
|
+
message re-wakes immediately; --requeue-after 1800 re-wakes if still unread.
|
|
32
|
+
--full queues the message text itself instead. --once / --launchd as before.
|
|
31
33
|
Codex: bschat watch --exec "codex queue --thread <id> --message {summary}" --launchd
|
|
32
34
|
bschat activity [--account] [--after <iso>] [--limit n] merged timeline: messages, status, evidence
|
|
33
35
|
bschat projects projects you can access
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = "0.7.
|
|
1
|
+
export const CLI_VERSION = "0.7.5";
|