@kybernesis/create 0.20.0 → 0.21.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/add.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { basename, join, resolve } from "node:path";
3
3
  import { bold, dim, green, red, run, yellow } from "./util.js";
4
+ import { addBuzz } from "./buzz.js";
4
5
  import { CHANNEL_KINDS, arcanaMemorySlotTs, channelPlan } from "./templates.js";
5
6
  /**
6
7
  * `kyb add channel <kind>` — put another surface on an agent that already exists.
@@ -21,10 +22,15 @@ import { CHANNEL_KINDS, arcanaMemorySlotTs, channelPlan } from "./templates.js";
21
22
  export async function add(what, rest) {
22
23
  if (what === "memory")
23
24
  return addMemory();
25
+ // Buzz is a bridge and a key, not a channel file; "add channel buzz" is what
26
+ // the provisioner and people naturally type, so both spellings land here.
27
+ if (what === "buzz" || (what === "channel" && rest.find((a) => !a.startsWith("-")) === "buzz"))
28
+ return addBuzz();
24
29
  if (what !== "channel") {
25
30
  console.error(red(`kyb add: don't know how to add "${what ?? ""}".`));
26
31
  console.log(dim(` kyb add channel <${CHANNEL_KINDS.filter((k) => k !== "none").join("|")}>`));
27
32
  console.log(dim(` kyb add memory ${dim("— an eve 0.49 memory slot backed by Arcana (recall before every turn)")}`));
33
+ console.log(dim(` kyb add buzz ${dim("— the agent's own Buzz identity, tools and bridge, ready for its owner's community")}`));
28
34
  process.exitCode = 1;
29
35
  return;
30
36
  }
package/dist/buzz.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * `kyb add buzz` — get an agent ready for its owner's Buzz community.
3
+ *
4
+ * @remarks
5
+ * Buzz is not a channel file. The agent is a member of the community with its
6
+ * own key, a separate bridge process hears mentions and answers as the person
7
+ * who spoke, and an extension gives the agent the rest of the workspace. Every
8
+ * one of those used to be a manual step on the host, so ticking Buzz on a build
9
+ * did nothing at all.
10
+ *
11
+ * This does everything that does not depend on the customer: the package, the
12
+ * extension mount, the agent's own key, the Buzz CLI and the bridge's unit. The
13
+ * unit is left switched off, because the one thing only the customer can give
14
+ * is which community it lives in. That arrives later through the owner's
15
+ * Channels page (manage's `buzz.join`), which writes the relay, publishes the
16
+ * agent's profile and starts the bridge.
17
+ */
18
+ export declare const BUZZ_PACKAGE = "@kybernesis/buzz@^0.10.2";
19
+ export declare const BUZZ_EXTENSION_MOUNT: string;
20
+ /** Where the agent's Buzz key lives: inside the agent, so the bridge and the agent read the same file. */
21
+ export declare function buzzKeyPath(appDir: string): string;
22
+ export declare function buzzBridgeUnitName(agent: string): string;
23
+ /** The bridge's systemd unit. Pure, so the test pins what every host gets. */
24
+ export declare function buzzBridgeUnit(input: {
25
+ agent: string;
26
+ user: string;
27
+ appDir: string;
28
+ }): string;
29
+ export declare function addBuzz(cwd?: string): void;
package/dist/buzz.js ADDED
@@ -0,0 +1,154 @@
1
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir, userInfo } from "node:os";
3
+ import { basename, join, resolve } from "node:path";
4
+ import { upsertEnv } from "./envfile.js";
5
+ import { bold, capture, dim, green, red, run, yellow } from "./util.js";
6
+ /**
7
+ * `kyb add buzz` — get an agent ready for its owner's Buzz community.
8
+ *
9
+ * @remarks
10
+ * Buzz is not a channel file. The agent is a member of the community with its
11
+ * own key, a separate bridge process hears mentions and answers as the person
12
+ * who spoke, and an extension gives the agent the rest of the workspace. Every
13
+ * one of those used to be a manual step on the host, so ticking Buzz on a build
14
+ * did nothing at all.
15
+ *
16
+ * This does everything that does not depend on the customer: the package, the
17
+ * extension mount, the agent's own key, the Buzz CLI and the bridge's unit. The
18
+ * unit is left switched off, because the one thing only the customer can give
19
+ * is which community it lives in. That arrives later through the owner's
20
+ * Channels page (manage's `buzz.join`), which writes the relay, publishes the
21
+ * agent's profile and starts the bridge.
22
+ */
23
+ export const BUZZ_PACKAGE = "@kybernesis/buzz@^0.10.2";
24
+ export const BUZZ_EXTENSION_MOUNT = `// The agent's hands in Buzz: projects, issues, pull requests, patches, repos,\n` +
25
+ `// long-form notes, channel canvases, workflows, the feed, media — everything the\n` +
26
+ `// workspace has beyond talking, which the bridge alone does not provide.\n` +
27
+ `//\n` +
28
+ `// Actions are signed with THIS AGENT's key and appear under its name.\n` +
29
+ `export { default } from "@kybernesis/buzz/extension";\n`;
30
+ /** Where the agent's Buzz key lives: inside the agent, so the bridge and the agent read the same file. */
31
+ export function buzzKeyPath(appDir) {
32
+ return join(appDir, ".buzz", "agent-key.json");
33
+ }
34
+ export function buzzBridgeUnitName(agent) {
35
+ return `${agent}-buzz-bridge`;
36
+ }
37
+ /** The bridge's systemd unit. Pure, so the test pins what every host gets. */
38
+ export function buzzBridgeUnit(input) {
39
+ const { agent, user, appDir } = input;
40
+ return `# Managed by @kybernesis/create (kyb add buzz). Switched on from the owner's Channels page.
41
+ [Unit]
42
+ Description=${agent} Buzz bridge
43
+ After=network-online.target ${agent}-agent.service
44
+ Wants=network-online.target
45
+
46
+ [Service]
47
+ Type=simple
48
+ User=${user}
49
+ WorkingDirectory=${appDir}
50
+ # The relay, the key and the agent credential all come from .env.local, the
51
+ # same file the agent reads, so they can never disagree.
52
+ ExecStart=/bin/bash -lc 'set -a && . ./.env.local && set +a && exec npx kybernesis-buzz run'
53
+ Restart=always
54
+ RestartSec=10
55
+ # The bridge publishes "offline" on the way out; this gives it time to.
56
+ KillSignal=SIGTERM
57
+ TimeoutStopSec=10
58
+ StartLimitBurst=5
59
+ StartLimitIntervalSec=300
60
+ StandardOutput=append:${appDir}/buzz-bridge.log
61
+ StandardError=append:${appDir}/buzz-bridge.log
62
+
63
+ [Install]
64
+ WantedBy=multi-user.target
65
+ `;
66
+ }
67
+ export function addBuzz(cwd = resolve(process.cwd())) {
68
+ if (!existsSync(join(cwd, "package.json")) || !existsSync(join(cwd, "agent"))) {
69
+ console.error(red("kyb add buzz: run this inside an agent directory."));
70
+ process.exitCode = 1;
71
+ return;
72
+ }
73
+ if (!existsSync(join(cwd, "scripts/eve-server.sh"))) {
74
+ console.error(red("kyb add buzz: Buzz runs a bridge beside the agent, so it needs a host of its own (an exe VM or a server). This agent deploys to Vercel."));
75
+ process.exitCode = 1;
76
+ return;
77
+ }
78
+ const agent = agentNameOf(cwd);
79
+ console.log(bold("kyb add buzz"));
80
+ console.log(dim(` agent: ${agent}`));
81
+ const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
82
+ if (!pkg.dependencies?.["@kybernesis/buzz"]) {
83
+ run("npm", ["install", BUZZ_PACKAGE, "--no-audit", "--no-fund"], { cwd });
84
+ }
85
+ const mount = join(cwd, "agent/extensions/buzz.ts");
86
+ if (!existsSync(mount)) {
87
+ mkdirSync(join(cwd, "agent/extensions"), { recursive: true });
88
+ writeFileSync(mount, BUZZ_EXTENSION_MOUNT);
89
+ console.log(green(" + agent/extensions/buzz.ts"));
90
+ }
91
+ // One key for the bridge and the agent's tools. An existing BUZZ_KEYFILE is
92
+ // kept: replacing a key is a new member the community never invited.
93
+ const envPath = join(cwd, ".env.local");
94
+ const env = existsSync(envPath) ? readFileSync(envPath, "utf8") : "";
95
+ const existing = env.match(/^BUZZ_KEYFILE\s*=\s*"?([^"\n]+)"?/m)?.[1]?.trim();
96
+ const keyFile = existing || buzzKeyPath(cwd);
97
+ if (!existing)
98
+ upsertEnv(cwd, { BUZZ_KEYFILE: keyFile });
99
+ mkdirSync(join(keyFile, ".."), { recursive: true });
100
+ ignore(cwd, ".buzz/");
101
+ const init = capture("npx", ["kybernesis-buzz", "init"], cwd, { BUZZ_KEYFILE: keyFile });
102
+ const npub = init?.match(/npub1[02-9ac-hj-np-z]+/)?.[0];
103
+ if (!npub) {
104
+ console.error(red(" Could not create the agent's Buzz identity (npx kybernesis-buzz init)."));
105
+ process.exitCode = 1;
106
+ return;
107
+ }
108
+ console.log(green(` + Buzz identity ${npub}`));
109
+ // The CLI is what the agent acts through; no binary is published, so it is
110
+ // built once in a container. Without docker the agent can still talk.
111
+ if (!existsSync(join(cwd, ".buzz/bin/buzz"))) {
112
+ const hasDocker = capture("sh", ["-c", "command -v docker >/dev/null && echo yes"])?.trim() === "yes";
113
+ if (hasDocker) {
114
+ console.log(dim(" building the Buzz CLI for this host (first time only, a few minutes) …"));
115
+ const ok = run("npx", ["kybernesis-buzz", "install-cli"], { cwd, allowFail: true, quiet: true, timeoutMs: 15 * 60_000 });
116
+ console.log(ok ? green(" + Buzz CLI (.buzz/bin/buzz)") : yellow(" ! the Buzz CLI did not build; run: npx kybernesis-buzz install-cli"));
117
+ }
118
+ else {
119
+ console.log(yellow(" ! no docker here, so the Buzz CLI was not built; the agent can talk in Buzz but not act in it."));
120
+ }
121
+ }
122
+ const unitName = buzzBridgeUnitName(agent);
123
+ const dir = mkdtempSync(join(tmpdir(), "kyb-buzz-"));
124
+ try {
125
+ const file = join(dir, `${unitName}.service`);
126
+ writeFileSync(file, buzzBridgeUnit({ agent, user: userInfo().username, appDir: cwd }));
127
+ const installed = run("sudo", ["-n", "install", "-m", "644", file, `/etc/systemd/system/${unitName}.service`], { allowFail: true, quiet: true }) &&
128
+ run("sudo", ["-n", "systemctl", "daemon-reload"], { allowFail: true, quiet: true });
129
+ console.log(installed
130
+ ? green(` + ${unitName}.service (off until the owner connects a community)`)
131
+ : yellow(` ! could not install ${unitName}.service without a password; the owner's Connect step will need it`));
132
+ }
133
+ finally {
134
+ rmSync(dir, { recursive: true, force: true });
135
+ }
136
+ console.log(bold("\n Next:"));
137
+ console.log(` · The owner connects it from the Channels page: their community's address, then an admin adds ${npub} as a member.`);
138
+ }
139
+ function agentNameOf(dir) {
140
+ const env = join(dir, ".env.local");
141
+ if (existsSync(env)) {
142
+ const hit = readFileSync(env, "utf8").match(/^KYBERNESIS_AGENT\s*=\s*"?([^"\n]+)"?/m);
143
+ if (hit?.[1])
144
+ return hit[1].trim();
145
+ }
146
+ return basename(dir);
147
+ }
148
+ function ignore(dir, entry) {
149
+ const path = join(dir, ".gitignore");
150
+ const current = existsSync(path) ? readFileSync(path, "utf8") : "";
151
+ if (current.split("\n").some((line) => line.trim() === entry))
152
+ return;
153
+ writeFileSync(path, `${current.replace(/\n*$/, "\n")}${entry}\n`);
154
+ }
package/dist/cli.js CHANGED
@@ -233,6 +233,7 @@ ${dim(" npm i -g @kybernesis/create@latest")}
233
233
  --yes ${dim("no prompts; take flags and defaults")}
234
234
  ${bold("kyb add channel <kind>")}
235
235
  ${dim("slack|imessage|telegram|discord|web — writes the channel, deps and env")}
236
+ ${bold("kyb add buzz")} ${dim("the agent's own Buzz identity, tools and bridge (connected from the Channels page)")}
236
237
  ${bold("kyb add memory")} ${dim("eve 0.49 memory slot backed by Arcana: recall before every turn")}
237
238
  ${bold("kyb doctor")} preflight checks (keys, issuer, envs, discovery)
238
239
  ${bold("kyb skills")} install/refresh the FDE skill suite for Claude Code
package/dist/upgrade.js CHANGED
@@ -4,6 +4,7 @@ import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { pathToFileURL } from "node:url";
6
6
  import { upsertEnv } from "./envfile.js";
7
+ import { BUZZ_EXTENSION_MOUNT } from "./buzz.js";
7
8
  import { reconcileHostArtifact } from "./host-artifacts.js";
8
9
  import { assessPeerTree, staleRangeWarning } from "./peer-tree.js";
9
10
  import { dockerPruneCronArtifact } from "./docker-prune-cron.js";
@@ -101,18 +102,7 @@ function repairBuzzSetup(cwd, deps) {
101
102
  const mount = join(cwd, "agent/extensions/buzz.ts");
102
103
  if (!existsSync(mount)) {
103
104
  mkdirSync(join(cwd, "agent/extensions"), { recursive: true });
104
- writeFileSync(mount, `// The agent's hands in Buzz: projects, issues, pull requests, patches, repos,
105
- ` +
106
- `// long-form notes, channel canvases, workflows, the feed, media — everything the
107
- ` +
108
- `// workspace has beyond talking, which the bridge alone does not provide.
109
- ` +
110
- `//
111
- ` +
112
- `// Actions are signed with THIS AGENT's key and appear under its name.
113
- ` +
114
- `export { default } from "@kybernesis/buzz/extension";
115
- `);
105
+ writeFileSync(mount, BUZZ_EXTENSION_MOUNT);
116
106
  done.push("mounted the Buzz extension (agent/extensions/buzz.ts)");
117
107
  }
118
108
  /**
@@ -124,7 +114,10 @@ function repairBuzzSetup(cwd, deps) {
124
114
  */
125
115
  const envPath = join(cwd, ".env.local");
126
116
  const env = existsSync(envPath) ? readFileSync(envPath, "utf8") : "";
127
- if (!/^BUZZ_RELAY=/m.test(env)) {
117
+ // An agent set up by `kyb add buzz` has no relay until its owner connects a
118
+ // community from the Channels page; that is waiting, not broken.
119
+ const awaitingOwner = /^BUZZ_KEYFILE=/m.test(env) && capture("sh", ["-c", "grep -l 'kyb add buzz' /etc/systemd/system/*buzz-bridge.service 2>/dev/null"]);
120
+ if (!/^BUZZ_RELAY=/m.test(env) && !awaitingOwner) {
128
121
  const unit = capture("sh", [
129
122
  "-c",
130
123
  "grep -ohE 'BUZZ_(RELAY|KEYFILE)=[^ \"]+' /etc/systemd/system/*buzz-bridge.service 2>/dev/null | head -2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kybernesis/create",
3
- "version": "0.20.0",
3
+ "version": "0.21.0",
4
4
  "description": "The Kybernesis agent scaffolder and FDE toolkit: one command to a governed, remembering, multiplayer, self-testing eve agent \u2014 plus doctor and upgrade.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",