benignswarm 0.3.1 → 0.3.2

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 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"];
4
- const BOOL_FLAGS = ["json", "follow", "account", "help", "version", "broadcast", "create-channels", "read-account-status", "replace-key", "mentions-me", "no-skill"];
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"];
5
5
  export function parse(argv) {
6
6
  const options = {};
7
7
  for (const f of STRING_FLAGS)
@@ -90,7 +90,7 @@ async function agent(action, rest, p, out) {
90
90
  }
91
91
  else if (action === "create" && proj && name) {
92
92
  const r = await c.call("POST", `/projects/${proj}/agents`, { name, vendor: need(p.flags, "vendor"), role: str(p.flags, "role") ?? null, machine: str(p.flags, "machine") ?? null });
93
- out.print(`created agent ${name} in ${proj}${r.agent.role_name ? ` with role ${r.agent.role_name}` : ""}\nKey (shown once): ${r.key}\nOn the agent's machine: bschat login --as ${name} --key ${r.key}`, r);
93
+ out.print(`created agent ${name} in ${proj}${r.agent.role_name ? ` with role ${r.agent.role_name}` : ""}. Key shown once.\nPaste this into the agent's instructions:\n\n${r.instructions}`, r);
94
94
  }
95
95
  else if (action === "role" && proj && name && extra) {
96
96
  const r = await c.call("PATCH", `/projects/${proj}/agents/${name}`, { role: extra === "none" ? null : extra });
@@ -98,7 +98,7 @@ async function agent(action, rest, p, out) {
98
98
  }
99
99
  else if (action === "rotate-key" && proj && name) {
100
100
  const r = await c.call("POST", `/projects/${proj}/agents/${name}/keys`, { machine: str(p.flags, "machine") ?? null });
101
- out.print(`previous keys revoked. New key (shown once): ${r.key}\nOn the agent's machine: bschat login --as ${name} --key ${r.key}`, r);
101
+ out.print(`previous keys revoked. New key shown once.\nPaste this into the agent's instructions:\n\n${r.instructions}`, r);
102
102
  }
103
103
  else if (action === "delete" && proj && name) {
104
104
  const r = await c.call("DELETE", `/projects/${proj}/agents/${name}`);
@@ -2,14 +2,14 @@ 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, saveAgent } from "../config.js";
5
+ import { 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";
9
9
  function agentClient(p) {
10
10
  const name = resolveAgentName(str(p.flags, "as"));
11
11
  const creds = loadAgent(name);
12
- const model = str(p.flags, "model") ?? process.env.BSCHAT_MODEL;
12
+ const model = resolveModel(str(p.flags, "model"));
13
13
  return { client: Client.forAgent(creds, model), creds };
14
14
  }
15
15
  export async function login(p, out) {
@@ -3,7 +3,7 @@ import { readFileSync, writeFileSync } from "node:fs";
3
3
  import { basename, extname } from "node:path";
4
4
  import { str } from "../args.js";
5
5
  import { Client } from "../client.js";
6
- import { loadAgent, resolveAgentName } from "../config.js";
6
+ import { loadAgent, resolveAgentName, resolveModel } from "../config.js";
7
7
  import { CliError } from "../errors.js";
8
8
  import { table } from "../output.js";
9
9
  const TYPES = {
@@ -20,7 +20,7 @@ export async function evidence(p, out) {
20
20
  const sub = p.positionals[1];
21
21
  const name = resolveAgentName(str(p.flags, "as"));
22
22
  const creds = loadAgent(name);
23
- const client = Client.forAgent(creds, str(p.flags, "model") ?? process.env.BSCHAT_MODEL);
23
+ const client = Client.forAgent(creds, resolveModel(str(p.flags, "model")));
24
24
  if (sub === "post") {
25
25
  const file = p.positionals[2];
26
26
  if (!file)
@@ -1,10 +1,48 @@
1
- import { mkdirSync, writeFileSync } from "node:fs";
2
- import { join as pjoin } from "node:path";
1
+ import { spawnSync } from "node:child_process";
3
2
  import { need, str, bool } from "../args.js";
4
- import { DEFAULT_URL, saveAgent } from "../config.js";
3
+ import { DEFAULT_URL, loadAgent, resolveModel, saveAgent } from "../config.js";
5
4
  import { CliError } from "../errors.js";
6
5
  import { defaultMachine } from "../git.js";
6
+ import { HARNESSES, findIdentityFile, installForHarness, normalizeVendor, readIdentityFile, writeIdentity } from "../harness.js";
7
7
  import { detectVendor } from "../vendor.js";
8
+ function repoRoot() {
9
+ const r = spawnSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
10
+ return r.status === 0 ? r.stdout.trim() : process.cwd();
11
+ }
12
+ async function fetchSkill(url) {
13
+ const get = async (p) => {
14
+ const res = await fetch(url + p);
15
+ if (!res.ok)
16
+ throw new CliError("fetch_failed", `${url}${p} returned ${res.status}`);
17
+ return res.text();
18
+ };
19
+ return { skill: await get("/skill/SKILL.md"), api: await get("/skill/references/api.md") };
20
+ }
21
+ function harnessList(p, vendor) {
22
+ const flag = str(p.flags, "harness");
23
+ if (!flag)
24
+ return [vendor];
25
+ if (flag === "all")
26
+ return [...HARNESSES];
27
+ return flag.split(",").map((h) => normalizeVendor(h.trim()));
28
+ }
29
+ /** Writes identity + harness files for an identity already saved on this machine. */
30
+ export async function setup(p, out) {
31
+ const name = str(p.flags, "as") ?? process.env.BSCHAT_AGENT;
32
+ if (!name)
33
+ throw new CliError("usage", "setup needs --as <agent-name> (an identity already saved with join or login)");
34
+ const creds = loadAgent(name);
35
+ const vendor = normalizeVendor(str(p.flags, "vendor") ?? creds.vendor);
36
+ const root = str(p.flags, "dir") ?? repoRoot();
37
+ const model = resolveModel(str(p.flags, "model"));
38
+ const idPath = writeIdentity(root, vendor, name, model, bool(p.flags, "replace"));
39
+ const files = await fetchSkill(creds.url);
40
+ const written = [];
41
+ for (const h of harnessList(p, vendor))
42
+ written.push(...installForHarness(root, h, files, name, creds.url));
43
+ const uniq = [...new Set(written)];
44
+ out.print(`Recorded ${vendor} -> ${name} in ${idPath}\nWrote: ${uniq.join(", ")}`, { identity: idPath, written: uniq });
45
+ }
8
46
  export async function join(p, out) {
9
47
  const name = str(p.flags, "as") ?? process.env.BSCHAT_AGENT;
10
48
  if (!name)
@@ -12,11 +50,16 @@ export async function join(p, out) {
12
50
  const token = need(p.flags, "token");
13
51
  if (!token.startsWith("bs_enroll_"))
14
52
  throw new CliError("usage", "--token must be an enrollment token (bs_enroll_...)");
15
- const vendor = str(p.flags, "vendor") ?? detectVendor();
16
- if (!vendor)
17
- throw new CliError("usage", "could not detect which agent product you are; pass --vendor <claude-code|codex|gemini-cli|cursor|aider|...>");
53
+ const rawVendor = str(p.flags, "vendor") ?? detectVendor();
54
+ if (!rawVendor)
55
+ throw new CliError("usage", `could not detect which agent product you are; pass --vendor <${HARNESSES.join("|")}>`);
56
+ const vendor = normalizeVendor(rawVendor);
18
57
  const url = (str(p.flags, "url") ?? process.env.BSCHAT_URL ?? DEFAULT_URL).replace(/\/$/, "");
19
58
  const machine = defaultMachine(str(p.flags, "label"));
59
+ const root = str(p.flags, "dir") ?? repoRoot();
60
+ const model = str(p.flags, "model") ?? process.env.BSCHAT_MODEL;
61
+ // Fail before touching the server if this repo already binds this vendor to another agent.
62
+ writeIdentityDryRun(root, vendor, name, bool(p.flags, "replace"));
20
63
  let res;
21
64
  try {
22
65
  res = await fetch(`${url}/enroll`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ token, name, vendor, machine }) });
@@ -28,27 +71,35 @@ export async function join(p, out) {
28
71
  if (!res.ok)
29
72
  throw new CliError(json.error ?? `http_${res.status}`, json.message ?? "join failed");
30
73
  const path = saveAgent({ url, key: json.key, account: json.project.id, project: json.project.name, agent: name, vendor, machine });
31
- let skillDir = null;
74
+ const idPath = writeIdentity(root, vendor, name, model, bool(p.flags, "replace"));
75
+ let written = [];
32
76
  if (!bool(p.flags, "no-skill")) {
33
- skillDir = str(p.flags, "skill-dir") ?? pjoin(process.cwd(), ".claude", "skills", "bschat");
34
- const main = await (await fetch(`${url}/skill/SKILL.md`)).text();
35
- const api = await (await fetch(`${url}/skill/references/api.md`)).text();
36
- mkdirSync(pjoin(skillDir, "references"), { recursive: true });
37
- writeFileSync(pjoin(skillDir, "SKILL.md"), main);
38
- writeFileSync(pjoin(skillDir, "references", "api.md"), api);
77
+ const files = await fetchSkill(url);
78
+ for (const h of harnessList(p, vendor))
79
+ written.push(...installForHarness(root, h, files, name, url));
80
+ written = [...new Set(written)];
39
81
  }
40
- const model = str(p.flags, "model") ?? process.env.BSCHAT_MODEL ?? "<your model id, e.g. claude-fable-5-1>";
41
82
  const human = [
42
83
  `Joined project "${json.project.name}" as agent "${name}" (${vendor})${json.agent.role_name ? ` with role ${json.agent.role_name}` : ""}.`,
43
84
  `Credentials saved to ${path} (private to this machine).`,
44
- skillDir ? `Skill installed to ${skillDir}.` : null,
85
+ `This repo now maps ${vendor} -> ${name} in ${idPath}, so bschat commands here know who you are, even after a reboot.`,
86
+ written.length ? `Harness files written: ${written.join(", ")}` : null,
45
87
  ``,
46
- `Now run these in your shell for the rest of this session:`,
88
+ `Optional for this shell session (the identity file already covers you in this repo):`,
47
89
  ``,
48
90
  ` export BSCHAT_AGENT=${name}`,
49
- ` export BSCHAT_MODEL=${model}`,
91
+ ` export BSCHAT_MODEL=${model ?? "<your model id, e.g. claude-fable-5-1>"}`,
50
92
  ``,
51
- `Then: bschat me && bschat who && bschat read general`,
93
+ `Next: bschat me && bschat who && bschat read general`,
52
94
  ].filter((l) => l !== null).join("\n");
53
- out.print(human, { agent: json.agent, project: json.project, saved: path, skill_dir: skillDir, export: { BSCHAT_AGENT: name, BSCHAT_MODEL: model } });
95
+ out.print(human, { agent: json.agent, project: json.project, saved: path, identity: idPath, written, export: { BSCHAT_AGENT: name, BSCHAT_MODEL: model ?? null } });
96
+ }
97
+ function writeIdentityDryRun(root, vendor, name, replace) {
98
+ const p = findIdentityFile(root);
99
+ if (!p || !p.startsWith(root))
100
+ return;
101
+ const existing = readIdentityFile(p).agents[vendor];
102
+ if (existing && existing.name !== name && !replace) {
103
+ throw new CliError("identity_exists", `${p} already maps ${vendor} to agent "${existing.name}"`, `Pass --replace to switch this repo's ${vendor} identity to "${name}", or join from a different repo.`);
104
+ }
54
105
  }
package/dist/config.js CHANGED
@@ -2,6 +2,8 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { CliError } from "./errors.js";
5
+ import { findIdentityFile, readIdentityFile } from "./harness.js";
6
+ import { detectVendor } from "./vendor.js";
5
7
  export const DEFAULT_URL = "https://api.benignswarm.org";
6
8
  export function configRoot(env = process.env) {
7
9
  return env.BSCHAT_HOME ?? join(homedir(), ".bschat");
@@ -40,15 +42,26 @@ export function listSaved(kind, env = process.env) {
40
42
  return readdirSync(dir).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5)).sort();
41
43
  }
42
44
  /**
43
- * Resolves which agent identity to use. Only --as or BSCHAT_AGENT count. There is deliberately no
44
- * default and no "only one configured" shortcut: several agents share this machine and cwd.
45
+ * Resolves which agent identity to use, in order: --as, BSCHAT_AGENT, then the repo-local
46
+ * `.bschat/identity.json` entry for the detected vendor (written by `bschat join`). There is
47
+ * no "only one saved identity" shortcut: several agents share this machine and cwd, so the
48
+ * only file-based answer accepted is an explicit per-vendor mapping for this repo.
45
49
  */
46
- export function resolveAgentName(asFlag, env = process.env) {
47
- const name = asFlag ?? env.BSCHAT_AGENT;
50
+ export function resolveAgentName(asFlag, env = process.env, cwd = process.cwd()) {
51
+ let name = asFlag ?? env.BSCHAT_AGENT;
52
+ if (!name) {
53
+ const file = findIdentityFile(cwd);
54
+ const vendor = detectVendor(env);
55
+ if (file && vendor) {
56
+ const entry = readIdentityFile(file).agents[vendor];
57
+ if (entry)
58
+ name = entry.name;
59
+ }
60
+ }
48
61
  if (!name) {
49
62
  const saved = listSaved("agents", env);
50
63
  throw new CliError("no_identity", "BSCHAT_AGENT is not set and --as was not given; bschat cannot tell which agent you are", saved.length
51
- ? `Saved identities on this machine: ${saved.join(", ")}. Ask the user which one is yours, then: export BSCHAT_AGENT=<name>`
64
+ ? `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)`
52
65
  : "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>");
53
66
  }
54
67
  return name;
@@ -75,3 +88,15 @@ export function loadAdmin(name, env = process.env) {
75
88
  throw new CliError("not_logged_in", `no saved admin credentials named ${name}`, "Run: bschat admin login --as <name> --key <bs_admin_...>");
76
89
  return JSON.parse(readFileSync(p, "utf8"));
77
90
  }
91
+ /** Model id: --model, BSCHAT_MODEL, then the repo identity file's entry for the detected vendor. */
92
+ export function resolveModel(flag, env = process.env, cwd = process.cwd()) {
93
+ if (flag)
94
+ return flag;
95
+ if (env.BSCHAT_MODEL)
96
+ return env.BSCHAT_MODEL;
97
+ const file = findIdentityFile(cwd);
98
+ const vendor = detectVendor(env);
99
+ if (file && vendor)
100
+ return readIdentityFile(file).agents[vendor]?.model;
101
+ return undefined;
102
+ }
@@ -0,0 +1,156 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { CliError } from "./errors.js";
4
+ /** Canonical vendor ids and the aliases people type. */
5
+ const VENDOR_ALIASES = {
6
+ "claude-code": "claude-code", claude: "claude-code", anthropic: "claude-code",
7
+ codex: "codex", chatgpt: "codex", openai: "codex", gpt: "codex",
8
+ antigravity: "antigravity", google: "antigravity", gemini: "gemini-cli", "gemini-cli": "gemini-cli",
9
+ dsh: "dsh", deepseek: "dsh", "deepseek-harness": "dsh",
10
+ cursor: "cursor", aider: "aider", copilot: "copilot",
11
+ };
12
+ export const HARNESSES = ["claude-code", "codex", "antigravity", "dsh", "gemini-cli", "cursor", "aider", "copilot"];
13
+ export function normalizeVendor(v) {
14
+ return VENDOR_ALIASES[v.toLowerCase()] ?? v.toLowerCase();
15
+ }
16
+ export const IDENTITY_FILE = join(".bschat", "identity.json");
17
+ /** Walks up from cwd to find the repo-local identity file. */
18
+ export function findIdentityFile(start = process.cwd()) {
19
+ let dir = start;
20
+ for (;;) {
21
+ const p = join(dir, IDENTITY_FILE);
22
+ if (existsSync(p))
23
+ return p;
24
+ const parent = dirname(dir);
25
+ if (parent === dir)
26
+ return null;
27
+ dir = parent;
28
+ }
29
+ }
30
+ export function readIdentityFile(path) {
31
+ const raw = JSON.parse(readFileSync(path, "utf8"));
32
+ if (!raw.agents || typeof raw.agents !== "object")
33
+ throw new CliError("bad_identity_file", `${path} is not a bschat identity file`);
34
+ return raw;
35
+ }
36
+ /**
37
+ * Records which agent name a given vendor uses in this repo. Refuses to silently replace an
38
+ * existing different name: two agents of one vendor in one worktree must use BSCHAT_AGENT.
39
+ */
40
+ export function writeIdentity(root, vendor, name, model, replace) {
41
+ const p = join(root, IDENTITY_FILE);
42
+ let file = { agents: {} };
43
+ if (existsSync(p))
44
+ file = readIdentityFile(p);
45
+ const existing = file.agents[vendor];
46
+ if (existing && existing.name !== name && !replace) {
47
+ throw new CliError("identity_exists", `${p} already maps ${vendor} to agent "${existing.name}"`, `Pass --replace to switch this repo's ${vendor} identity to "${name}", or set BSCHAT_AGENT per session.`);
48
+ }
49
+ file.agents[vendor] = { name, ...(model ? { model } : {}) };
50
+ mkdirSync(dirname(p), { recursive: true });
51
+ writeFileSync(p, JSON.stringify(file, null, 2) + "\n");
52
+ ensureGitignore(root, [".bschat/", ".claude/settings.local.json"]);
53
+ return p;
54
+ }
55
+ export function ensureGitignore(root, entries) {
56
+ if (!existsSync(join(root, ".git")))
57
+ return;
58
+ const p = join(root, ".gitignore");
59
+ const current = existsSync(p) ? readFileSync(p, "utf8") : "";
60
+ const lines = current.split("\n");
61
+ const missing = entries.filter((e) => !lines.includes(e));
62
+ if (!missing.length)
63
+ return;
64
+ writeFileSync(p, (current.endsWith("\n") || current === "" ? current : current + "\n") + "# bschat (per-machine agent identity)\n" + missing.join("\n") + "\n");
65
+ }
66
+ const START = "<!-- bschat:start -->";
67
+ const END = "<!-- bschat:end -->";
68
+ /** Replaces or appends a marked block in a markdown file. Idempotent. */
69
+ export function upsertBlock(path, body) {
70
+ const block = `${START}\n${body.trim()}\n${END}\n`;
71
+ if (!existsSync(path)) {
72
+ mkdirSync(dirname(path), { recursive: true });
73
+ writeFileSync(path, block);
74
+ return;
75
+ }
76
+ const cur = readFileSync(path, "utf8");
77
+ const s = cur.indexOf(START);
78
+ const e = cur.indexOf(END);
79
+ if (s >= 0 && e > s)
80
+ writeFileSync(path, cur.slice(0, s) + block + cur.slice(e + END.length).replace(/^\n/, ""));
81
+ else
82
+ writeFileSync(path, (cur.endsWith("\n") || cur === "" ? cur : cur + "\n") + "\n" + block);
83
+ }
84
+ function pointer(_name, apiUrl) {
85
+ return [
86
+ `## bschat: agent chat for this project`,
87
+ ``,
88
+ `This repo's agents coordinate on BenignSwarm. Your identity for this repo is recorded in`,
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>.`,
91
+ ``,
92
+ `Start of session: \`npx -y benignswarm me\`, \`npx -y benignswarm who\`, \`npx -y benignswarm inbox\`.`,
93
+ `Full instructions: \`.bschat/SKILL.md\` in this repo, or ${apiUrl}/skill.`,
94
+ ].join("\n");
95
+ }
96
+ /**
97
+ * Writes the files a given harness needs to discover bschat in this repo.
98
+ * Returns the paths written. Each writer is idempotent.
99
+ */
100
+ export function installForHarness(root, harness, files, agentName, apiUrl) {
101
+ const written = [];
102
+ const neutral = join(root, ".bschat");
103
+ mkdirSync(join(neutral, "references"), { recursive: true });
104
+ writeFileSync(join(neutral, "SKILL.md"), files.skill);
105
+ writeFileSync(join(neutral, "references", "api.md"), files.api);
106
+ written.push(join(neutral, "SKILL.md"));
107
+ switch (harness) {
108
+ case "claude-code": {
109
+ const dir = join(root, ".claude", "skills", "bschat");
110
+ mkdirSync(join(dir, "references"), { recursive: true });
111
+ writeFileSync(join(dir, "SKILL.md"), files.skill);
112
+ writeFileSync(join(dir, "references", "api.md"), files.api);
113
+ written.push(join(dir, "SKILL.md"));
114
+ const sp = join(root, ".claude", "settings.local.json");
115
+ let settings = {};
116
+ if (existsSync(sp)) {
117
+ try {
118
+ settings = JSON.parse(readFileSync(sp, "utf8"));
119
+ }
120
+ catch {
121
+ throw new CliError("bad_settings", `${sp} is not valid JSON; fix it or remove it`);
122
+ }
123
+ }
124
+ const env = { ...(settings.env ?? {}), BSCHAT_AGENT: agentName };
125
+ writeFileSync(sp, JSON.stringify({ ...settings, env }, null, 2) + "\n");
126
+ written.push(sp);
127
+ break;
128
+ }
129
+ case "codex":
130
+ case "dsh":
131
+ case "aider":
132
+ case "copilot":
133
+ case "cursor": {
134
+ const p = join(root, "AGENTS.md");
135
+ upsertBlock(p, pointer(agentName, apiUrl));
136
+ written.push(p);
137
+ break;
138
+ }
139
+ case "antigravity": {
140
+ const p = join(root, ".agents", "rules", "bschat.md");
141
+ mkdirSync(dirname(p), { recursive: true });
142
+ writeFileSync(p, `---\ntrigger: always_on\ndescription: "How to use bschat, the agent chat for this project"\n---\n\n${pointer(agentName, apiUrl)}\n`);
143
+ written.push(p);
144
+ break;
145
+ }
146
+ case "gemini-cli": {
147
+ const p = join(root, "GEMINI.md");
148
+ upsertBlock(p, pointer(agentName, apiUrl));
149
+ written.push(p);
150
+ break;
151
+ }
152
+ default:
153
+ throw new CliError("unknown_harness", `no harness integration for "${harness}"`, `Known: ${HARNESSES.join(", ")}. The neutral copy at .bschat/SKILL.md was still written.`);
154
+ }
155
+ return written;
156
+ }
package/dist/help.js CHANGED
@@ -5,7 +5,10 @@ Model (recommended): --model <id> or BSCHAT_MODEL=<id>
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] [--skill-dir d] [--no-skill]
8
+ bschat join --as <name> --token <bs_enroll_...> [--vendor v] [--model m] [--label l] [--harness h1,h2|all] [--no-skill] [--replace]
9
+ bschat setup --as <name> [--harness ...] [--replace] (re)write this repo's identity file and harness files
10
+ Harnesses: claude-code (.claude/skills + settings env), codex and dsh (AGENTS.md), antigravity (.agents/rules),
11
+ gemini-cli (GEMINI.md). Identity persists in .bschat/identity.json per repo and vendor, surviving reboots.
9
12
 
10
13
  Agent commands
11
14
  bschat login --as <name> --key <bs_agent_...> [--url <api>] [--label <machine-label>]
package/dist/index.js CHANGED
@@ -3,12 +3,13 @@ import { bool, parse } from "./args.js";
3
3
  import { admin } from "./commands/admin.js";
4
4
  import * as a from "./commands/agent.js";
5
5
  import { evidence } from "./commands/evidence.js";
6
- import { join } from "./commands/join.js";
6
+ import { join, setup } from "./commands/join.js";
7
7
  import { CliError } from "./errors.js";
8
8
  import { HELP } from "./help.js";
9
9
  import { makeOut } from "./output.js";
10
10
  const COMMANDS = {
11
11
  join,
12
+ setup,
12
13
  login: a.login,
13
14
  logout: a.logout,
14
15
  identities: a.identities,
@@ -30,7 +31,7 @@ async function main(argv) {
30
31
  const p = parse(argv);
31
32
  const out = makeOut(bool(p.flags, "json"));
32
33
  if (bool(p.flags, "version")) {
33
- process.stdout.write("bschat 0.3.1\n");
34
+ process.stdout.write("bschat 0.3.2\n");
34
35
  return 0;
35
36
  }
36
37
  const cmd = p.positionals[0];
package/dist/vendor.js CHANGED
@@ -1,10 +1,11 @@
1
+ import { normalizeVendor } from "./harness.js";
1
2
  /**
2
3
  * Detects which agent product is running the CLI from signals those products set themselves.
3
4
  * Returns null when nothing is recognised; the caller must then require --vendor.
4
5
  */
5
6
  export function detectVendor(env = process.env) {
6
7
  if (env.BSCHAT_VENDOR)
7
- return env.BSCHAT_VENDOR;
8
+ return normalizeVendor(env.BSCHAT_VENDOR);
8
9
  if (env.CLAUDECODE === "1" || env.CLAUDE_CODE_ENTRYPOINT)
9
10
  return "claude-code";
10
11
  if (env.CODEX_SANDBOX || env.CODEX_THREAD_ID || env.OPENAI_CODEX)
@@ -15,6 +16,10 @@ export function detectVendor(env = process.env) {
15
16
  return "cursor";
16
17
  if (env.AIDER_MODEL)
17
18
  return "aider";
19
+ if (env.ANTIGRAVITY_AGENT || env.ANTIGRAVITY_SESSION_ID)
20
+ return "antigravity";
21
+ if (env.DSH_SESSION_ID || env.DEEPSEEK_HARNESS)
22
+ return "dsh";
18
23
  if (env.COPILOT_AGENT || env.GITHUB_COPILOT_CLI)
19
24
  return "copilot";
20
25
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "benignswarm",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "bschat: chat, status board and evidence store for coding agents (benignswarm.org)",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {