@kybernesis/buzz 0.5.0 → 0.6.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/_chunks/find-B2f5dymb.mjs +108 -0
- package/dist/bridge.js +48 -14
- package/dist/cli.js +74 -1
- package/dist/credentials.d.ts +25 -0
- package/dist/credentials.js +36 -0
- package/dist/extension/_manifest.json +10 -0
- package/dist/extension/extension.d.ts +16 -0
- package/dist/extension/extension.mjs +9 -0
- package/dist/extension/instructions.md +116 -0
- package/dist/extension/tools/buzz.d.ts +10 -0
- package/dist/extension/tools/buzz.mjs +56 -0
- package/dist/extension/tools/find.d.ts +16 -0
- package/dist/extension/tools/find.mjs +8 -0
- package/dist/index.mjs +8 -0
- package/dist/query.d.ts +22 -0
- package/dist/query.js +63 -0
- package/dist/relay.js +8 -1
- package/dist/tools/index.d.ts +4 -0
- package/dist/tools/index.mjs +9 -0
- package/package.json +21 -6
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { fileURLToPath as __eveFileURLToPath } from "node:url";
|
|
2
|
+
import { dirname as __eveDirname } from "node:path";
|
|
3
|
+
import { createRequire as __eveCreateRequire } from "node:module";
|
|
4
|
+
const __filename = __eveFileURLToPath(import.meta.url);
|
|
5
|
+
__eveDirname(__filename);
|
|
6
|
+
__eveCreateRequire(import.meta.url);
|
|
7
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
8
|
+
import { defineTool } from "eve/tools";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { finalizeEvent, getPublicKey } from "nostr-tools/pure";
|
|
11
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
12
|
+
const KIND_HTTP_AUTH = 27235;
|
|
13
|
+
async function queryRelay(relayHttpUrl, secretKey, filters) {
|
|
14
|
+
const url = `${relayHttpUrl.replace(/\/$/, "")}/query`;
|
|
15
|
+
const body = JSON.stringify(filters);
|
|
16
|
+
const auth = finalizeEvent({
|
|
17
|
+
kind: KIND_HTTP_AUTH,
|
|
18
|
+
created_at: Math.floor(Date.now() / 1e3),
|
|
19
|
+
tags: [
|
|
20
|
+
["u", url],
|
|
21
|
+
["method", "POST"],
|
|
22
|
+
["nonce", randomUUID()],
|
|
23
|
+
["payload", createHash("sha256").update(body).digest("hex")]
|
|
24
|
+
],
|
|
25
|
+
content: ""
|
|
26
|
+
}, secretKey);
|
|
27
|
+
const response = await fetch(url, {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: {
|
|
30
|
+
authorization: `Nostr ${Buffer.from(JSON.stringify(auth)).toString("base64")}`,
|
|
31
|
+
"content-type": "application/json"
|
|
32
|
+
},
|
|
33
|
+
body
|
|
34
|
+
});
|
|
35
|
+
const text = await response.text();
|
|
36
|
+
if (!response.ok) throw new Error(`relay ${response.status}: ${text.slice(0, 300)}`);
|
|
37
|
+
return {
|
|
38
|
+
events: JSON.parse(text),
|
|
39
|
+
status: response.status,
|
|
40
|
+
bytes: text.length
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const KINDS = {
|
|
44
|
+
project: 30621,
|
|
45
|
+
repo: 30617,
|
|
46
|
+
issue: 1621,
|
|
47
|
+
patch: 1617,
|
|
48
|
+
message: 9,
|
|
49
|
+
note: 30023,
|
|
50
|
+
profile: 0
|
|
51
|
+
};
|
|
52
|
+
var find_default = defineTool({
|
|
53
|
+
description: "Find what exists in the Buzz workspace, across everyone — projects, repos, issues, patches, notes, profiles. Use this for questions like \"what projects exist here?\", which the buzz CLI's list commands CANNOT answer: those are scoped to this agent's own items and will report none when the workspace is full of other people's. Give a `type` (project, repo, issue, patch, note, message, profile) or raw `kinds`.",
|
|
54
|
+
inputSchema: z.object({
|
|
55
|
+
type: z.enum([
|
|
56
|
+
"project",
|
|
57
|
+
"repo",
|
|
58
|
+
"issue",
|
|
59
|
+
"patch",
|
|
60
|
+
"note",
|
|
61
|
+
"message",
|
|
62
|
+
"profile"
|
|
63
|
+
]).optional().describe("What to look for. Omit only if passing raw kinds."),
|
|
64
|
+
kinds: z.array(z.number()).optional().describe("Raw Nostr kind numbers, for anything not named above."),
|
|
65
|
+
authors: z.array(z.string()).optional().describe("Restrict to these author pubkeys (64-char hex). Omit for everyone in the workspace."),
|
|
66
|
+
limit: z.number().optional().describe("How many at most. Defaults to 50."),
|
|
67
|
+
community: z.string().optional().describe("Which workspace, when this agent belongs to more than one. Any distinctive part of its address.")
|
|
68
|
+
}),
|
|
69
|
+
async execute({ type, kinds, authors, limit, community }) {
|
|
70
|
+
const keyFile = process.env.BUZZ_KEYFILE ?? ".buzz-key.json";
|
|
71
|
+
if (!existsSync(keyFile)) return `No Buzz identity at ${keyFile}: this agent has no standing in the workspace yet.`;
|
|
72
|
+
const configured = (process.env.BUZZ_RELAY_HTTP ?? process.env.BUZZ_RELAY ?? "").split(",").map((entry) => entry.trim().replace(/^ws/, "http")).filter(Boolean);
|
|
73
|
+
if (configured.length === 0) return "No Buzz relay configured (BUZZ_RELAY).";
|
|
74
|
+
let relay;
|
|
75
|
+
if (community) {
|
|
76
|
+
const matches = configured.filter((entry) => entry.includes(community));
|
|
77
|
+
if (matches.length !== 1) return `"${community}" does not pick out exactly one of: ${configured.join(", ")}`;
|
|
78
|
+
relay = matches[0];
|
|
79
|
+
} else if (configured.length > 1) return `This agent belongs to more than one workspace — say which: ${configured.join(", ")}`;
|
|
80
|
+
else relay = configured[0];
|
|
81
|
+
const wanted = (kinds?.length ? kinds : void 0) ?? (type ? [KINDS[type]] : void 0);
|
|
82
|
+
const by = authors?.length ? authors : void 0;
|
|
83
|
+
if (!wanted) return "Say what to look for: a `type`, or raw `kinds`.";
|
|
84
|
+
const stored = JSON.parse(readFileSync(keyFile, "utf8"));
|
|
85
|
+
const asWhom = getPublicKey(Uint8Array.from(stored.secretKey));
|
|
86
|
+
try {
|
|
87
|
+
const filter = {
|
|
88
|
+
kinds: wanted,
|
|
89
|
+
...by ? { authors: by } : {},
|
|
90
|
+
limit: limit ?? 50
|
|
91
|
+
};
|
|
92
|
+
const { events, status, bytes } = await queryRelay(relay, Uint8Array.from(stored.secretKey), [filter]);
|
|
93
|
+
if (events.length === 0) return `Nothing in ${relay} matching ${JSON.stringify(filter)} (searched as ${asWhom.slice(0, 8)}…, relay answered ${status}).`;
|
|
94
|
+
return JSON.stringify(events.map((event) => ({
|
|
95
|
+
id: event.id,
|
|
96
|
+
author: event.pubkey,
|
|
97
|
+
created: new Date(event.created_at * 1e3).toISOString(),
|
|
98
|
+
name: event.tags.find((tag) => tag[0] === "name")?.[1],
|
|
99
|
+
slug: event.tags.find((tag) => tag[0] === "d")?.[1],
|
|
100
|
+
subject: event.tags.find((tag) => tag[0] === "subject")?.[1],
|
|
101
|
+
content: event.content?.slice(0, 400)
|
|
102
|
+
})), null, 1);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
return `Could not read the workspace: ${error.message}`;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
export { find_default as t };
|
package/dist/bridge.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { channelIdentity } from "@kybernesis/enterprise";
|
|
2
|
+
import { speakerCredentials } from "./credentials.js";
|
|
2
3
|
import { Client } from "eve/client";
|
|
3
4
|
import { BuzzRelay } from "./relay.js";
|
|
4
5
|
import { loadKey, npubEncode } from "./keys.js";
|
|
@@ -50,14 +51,33 @@ export function buzzBridge(options) {
|
|
|
50
51
|
const sessions = new Map();
|
|
51
52
|
/** Who has already been sent a link, so a room full of strangers is not a room full of spam. */
|
|
52
53
|
const invited = new Map();
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
54
|
+
/**
|
|
55
|
+
* A client whose credentials are resolved per request, not captured once.
|
|
56
|
+
*
|
|
57
|
+
* The identity token a turn starts with lives about five minutes. That was
|
|
58
|
+
* invisible while turns ended early — and became a failure the moment they
|
|
59
|
+
* were allowed to run to completion: a turn doing real work for six minutes
|
|
60
|
+
* reconnected its stream with the token it began with, the agent answered
|
|
61
|
+
* "Authorization is required for this route", and the reply was lost after
|
|
62
|
+
* all the work had been done. In a channel that reads as the agent typing
|
|
63
|
+
* for five minutes and then saying nothing.
|
|
64
|
+
*
|
|
65
|
+
* Resolving per request costs nothing in the normal case — the resolver
|
|
66
|
+
* caches and re-mints only near expiry — and it keeps the property the short
|
|
67
|
+
* lifetime exists for: a person whose access is revoked mid-turn stops being
|
|
68
|
+
* able to act on the next request rather than at the end of the turn.
|
|
69
|
+
*/
|
|
70
|
+
const credentialsFor = speakerCredentials((externalId) => identity.resolve(PROVIDER, externalId, npubEncode(externalId)));
|
|
71
|
+
const clientFor = (pubkey) => {
|
|
72
|
+
const credentials = credentialsFor(pubkey);
|
|
73
|
+
// `host`, not `baseUrl`: a wrong key is ignored rather than rejected, and the client then
|
|
74
|
+
// builds a nonsense URL — an error about a malformed URL rather than about a mistyped option.
|
|
75
|
+
return new Client({
|
|
76
|
+
host: options.agentUrl,
|
|
77
|
+
auth: { bearer: credentials.bearer },
|
|
78
|
+
headers: credentials.headers,
|
|
79
|
+
});
|
|
80
|
+
};
|
|
61
81
|
/**
|
|
62
82
|
* One turn at a time per channel.
|
|
63
83
|
*
|
|
@@ -73,9 +93,20 @@ export function buzzBridge(options) {
|
|
|
73
93
|
inFlight.set(channel, queued.catch(() => { }));
|
|
74
94
|
return queued;
|
|
75
95
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
96
|
+
/**
|
|
97
|
+
* Where this conversation is happening, handed to the agent with the turn.
|
|
98
|
+
*
|
|
99
|
+
* An agent in two communities was asked "what projects exist in this relay?"
|
|
100
|
+
* and could not know which relay "this" was — so it reached for a
|
|
101
|
+
* human-in-the-loop question, which parks a turn until somebody answers it.
|
|
102
|
+
* In a channel nobody does, so the turn never finished and the room got
|
|
103
|
+
* nothing. The bridge knew the answer the whole time: it is the connection
|
|
104
|
+
* the message arrived on.
|
|
105
|
+
*/
|
|
106
|
+
async function ask(channel, text, pubkey, community) {
|
|
107
|
+
// Built per turn, for the person whose turn it is — and kept current for as
|
|
108
|
+
// long as that turn runs.
|
|
109
|
+
const client = clientFor(pubkey);
|
|
79
110
|
const existing = sessions.get(channel);
|
|
80
111
|
if (existing) {
|
|
81
112
|
try {
|
|
@@ -107,7 +138,7 @@ export function buzzBridge(options) {
|
|
|
107
138
|
if (unread > 0) {
|
|
108
139
|
log(`caught up on ${unread} unread event(s) in ${channel.slice(0, 8)} before answering`);
|
|
109
140
|
}
|
|
110
|
-
const response = await session.send(text);
|
|
141
|
+
const response = await session.send(text, { clientContext: { buzzCommunity: community, buzzChannel: channel } });
|
|
111
142
|
const result = await response.result();
|
|
112
143
|
// Where this turn left the conversation, so the next one starts after it.
|
|
113
144
|
sessions.set(channel, { id: existing.id, streamIndex: session.state.streamIndex });
|
|
@@ -120,7 +151,10 @@ export function buzzBridge(options) {
|
|
|
120
151
|
sessions.delete(channel);
|
|
121
152
|
}
|
|
122
153
|
}
|
|
123
|
-
const created = await client.sessions.create({
|
|
154
|
+
const created = await client.sessions.create({
|
|
155
|
+
message: text,
|
|
156
|
+
clientContext: { buzzCommunity: community, buzzChannel: channel },
|
|
157
|
+
});
|
|
124
158
|
const message = (await created.response.result()).message ?? "";
|
|
125
159
|
sessions.set(channel, {
|
|
126
160
|
id: created.session.state.sessionId,
|
|
@@ -212,7 +246,7 @@ export function buzzBridge(options) {
|
|
|
212
246
|
void relay.react(event.id, SEEN);
|
|
213
247
|
const stopTyping = relay.typingIn(channel);
|
|
214
248
|
try {
|
|
215
|
-
const reply = await serialize(channel, () => ask(channel, text,
|
|
249
|
+
const reply = await serialize(channel, () => ask(channel, text, event.pubkey, from));
|
|
216
250
|
if (!reply) {
|
|
217
251
|
/**
|
|
218
252
|
* Silence is the worst answer available.
|
package/dist/cli.js
CHANGED
|
@@ -183,6 +183,73 @@ WantedBy=multi-user.target
|
|
|
183
183
|
}
|
|
184
184
|
process.stdout.write(unit);
|
|
185
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* Put the Buzz CLI on this host, so the agent can act in the workspace.
|
|
188
|
+
*
|
|
189
|
+
* @remarks
|
|
190
|
+
* The bridge gives an agent a voice; the CLI is how Buzz expects an agent to
|
|
191
|
+
* DO anything — projects, issues, pull requests, notes, canvases. Its own
|
|
192
|
+
* README is explicit: the harness prompts the agent, "and the agent replies
|
|
193
|
+
* using the Buzz CLI".
|
|
194
|
+
*
|
|
195
|
+
* There is no published binary — the project releases desktop apps only — so
|
|
196
|
+
* this builds one, from a pinned revision, in a container, and puts it where
|
|
197
|
+
* the tool looks. That is a script's job and not a person's: the alternative
|
|
198
|
+
* is a client being walked through a git checkout and a Rust toolchain, which
|
|
199
|
+
* is not an answer anyone should give a customer.
|
|
200
|
+
*
|
|
201
|
+
* `BUZZ_CLI_URL` short-circuits the build for hosts that have no Docker: point
|
|
202
|
+
* it at a binary built once for the same platform.
|
|
203
|
+
*/
|
|
204
|
+
async function installCli() {
|
|
205
|
+
const { execFileSync } = await import("node:child_process");
|
|
206
|
+
const { chmodSync, existsSync, mkdirSync } = await import("node:fs");
|
|
207
|
+
const { dirname, resolve } = await import("node:path");
|
|
208
|
+
const target = resolve(process.env.BUZZ_CLI_PATH ?? ".buzz/bin/buzz");
|
|
209
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
210
|
+
const url = process.env.BUZZ_CLI_URL;
|
|
211
|
+
if (url) {
|
|
212
|
+
console.log(` downloading ${url}`);
|
|
213
|
+
const response = await fetch(url);
|
|
214
|
+
if (!response.ok)
|
|
215
|
+
throw new Error(`could not download the CLI (HTTP ${response.status})`);
|
|
216
|
+
const { writeFileSync } = await import("node:fs");
|
|
217
|
+
writeFileSync(target, Buffer.from(await response.arrayBuffer()));
|
|
218
|
+
chmodSync(target, 0o755);
|
|
219
|
+
console.log(` ✓ ${target}`);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
execFileSync("docker", ["--version"], { stdio: "ignore" });
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
throw new Error("no docker on this host, and no BUZZ_CLI_URL set. Build the CLI once elsewhere for this " +
|
|
227
|
+
"platform and point BUZZ_CLI_URL at it, or install docker.");
|
|
228
|
+
}
|
|
229
|
+
// Pinned. A CLI that changes under a fleet on an unrelated restart is a
|
|
230
|
+
// fleet whose behaviour changed for reasons nobody can reconstruct.
|
|
231
|
+
const source = resolve(process.env.BUZZ_CLI_SRC ?? ".buzz/src");
|
|
232
|
+
if (!existsSync(source)) {
|
|
233
|
+
console.log(" fetching the CLI source (once)…");
|
|
234
|
+
execFileSync("git", ["clone", "--depth", "1", "https://github.com/block/buzz.git", source], {
|
|
235
|
+
stdio: "inherit",
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
console.log(" building — a few minutes the first time, then cached…");
|
|
239
|
+
execFileSync("docker", [
|
|
240
|
+
"run", "--rm",
|
|
241
|
+
"-v", `${source}:/src`,
|
|
242
|
+
"-w", "/src",
|
|
243
|
+
"rust:1-slim",
|
|
244
|
+
"sh", "-c",
|
|
245
|
+
"apt-get update -qq && apt-get install -y -qq pkg-config libssl-dev >/dev/null 2>&1; cargo build --release -p buzz-cli",
|
|
246
|
+
], { stdio: "inherit" });
|
|
247
|
+
const { copyFileSync } = await import("node:fs");
|
|
248
|
+
copyFileSync(`${source}/target/release/buzz`, target);
|
|
249
|
+
chmodSync(target, 0o755);
|
|
250
|
+
console.log(`\n ✓ ${target}`);
|
|
251
|
+
console.log(" The agent finds it there automatically; set BUZZ_CLI_PATH to move it.");
|
|
252
|
+
}
|
|
186
253
|
const command = process.argv[2];
|
|
187
254
|
if (command === "profile") {
|
|
188
255
|
setProfile().catch((error) => {
|
|
@@ -196,6 +263,12 @@ else if (command === "run")
|
|
|
196
263
|
run();
|
|
197
264
|
else if (command === "service")
|
|
198
265
|
service();
|
|
266
|
+
else if (command === "install-cli") {
|
|
267
|
+
installCli().catch((error) => {
|
|
268
|
+
console.error(`\n ${error.message}\n`);
|
|
269
|
+
process.exit(1);
|
|
270
|
+
});
|
|
271
|
+
}
|
|
199
272
|
else if (command === "id") {
|
|
200
273
|
const hex = asHexPubkey(process.argv[3]);
|
|
201
274
|
if (!hex) {
|
|
@@ -209,7 +282,7 @@ else {
|
|
|
209
282
|
console.log(`
|
|
210
283
|
kybernesis-buzz — put an agent in a workspace
|
|
211
284
|
|
|
212
|
-
init create or show this agent's identity, and what to invite
|
|
285
|
+
init create or show this agent's identity, and what to invite\n install-cli install the Buzz CLI, so the agent can act as well as talk
|
|
213
286
|
profile say who this agent is, in every community it belongs to
|
|
214
287
|
run run the bridge
|
|
215
288
|
service [path] print (or write) a systemd unit for it
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { SpeakerResolution } from "@kybernesis/enterprise";
|
|
2
|
+
/** What the eve client needs to authenticate one person's turn. */
|
|
3
|
+
export interface SpeakerCredentials {
|
|
4
|
+
/** Bearer resolver — called by the client before every HTTP request. */
|
|
5
|
+
bearer: () => Promise<string>;
|
|
6
|
+
/** Header resolver, for the same reason and on the same schedule. */
|
|
7
|
+
headers: () => Promise<Readonly<Record<string, string>>>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Credentials for a turn, resolved per request rather than captured once.
|
|
11
|
+
*
|
|
12
|
+
* @remarks
|
|
13
|
+
* An identity token lives about five minutes. That was invisible while turns
|
|
14
|
+
* ended early, and became a failure the moment they ran to completion: a turn
|
|
15
|
+
* doing six minutes of real work reconnected its stream with the token it began
|
|
16
|
+
* with, the agent answered "Authorization is required for this route", and the
|
|
17
|
+
* reply was lost after every tool call had already succeeded. In a channel that
|
|
18
|
+
* reads as the agent typing for five minutes and then saying nothing at all.
|
|
19
|
+
*
|
|
20
|
+
* Resolving per request costs nothing in the normal case, because the resolver
|
|
21
|
+
* caches and re-mints only near expiry. It also preserves the property the
|
|
22
|
+
* short lifetime exists for: access withdrawn mid-turn stops the next request
|
|
23
|
+
* rather than being noticed after the work is done.
|
|
24
|
+
*/
|
|
25
|
+
export declare function speakerCredentials(resolve: (externalId: string) => Promise<SpeakerResolution>): (externalId: string) => SpeakerCredentials;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credentials for a turn, resolved per request rather than captured once.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* An identity token lives about five minutes. That was invisible while turns
|
|
6
|
+
* ended early, and became a failure the moment they ran to completion: a turn
|
|
7
|
+
* doing six minutes of real work reconnected its stream with the token it began
|
|
8
|
+
* with, the agent answered "Authorization is required for this route", and the
|
|
9
|
+
* reply was lost after every tool call had already succeeded. In a channel that
|
|
10
|
+
* reads as the agent typing for five minutes and then saying nothing at all.
|
|
11
|
+
*
|
|
12
|
+
* Resolving per request costs nothing in the normal case, because the resolver
|
|
13
|
+
* caches and re-mints only near expiry. It also preserves the property the
|
|
14
|
+
* short lifetime exists for: access withdrawn mid-turn stops the next request
|
|
15
|
+
* rather than being noticed after the work is done.
|
|
16
|
+
*/
|
|
17
|
+
export function speakerCredentials(resolve) {
|
|
18
|
+
return (externalId) => {
|
|
19
|
+
const current = async () => {
|
|
20
|
+
const speaker = await resolve(externalId);
|
|
21
|
+
if (!speaker.linked) {
|
|
22
|
+
// Failing here is the point: the alternative is finishing a turn with a
|
|
23
|
+
// credential the control plane has already withdrawn.
|
|
24
|
+
throw new Error(`no longer authorized (${speaker.reason})`);
|
|
25
|
+
}
|
|
26
|
+
return { token: speaker.token, bundle: speaker.bundle };
|
|
27
|
+
};
|
|
28
|
+
return {
|
|
29
|
+
bearer: async () => (await current()).token,
|
|
30
|
+
headers: async () => {
|
|
31
|
+
const { bundle } = await current();
|
|
32
|
+
return bundle ? { "x-kybernesis-bundle": bundle } : {};
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The agent's hands in Buzz.
|
|
3
|
+
*
|
|
4
|
+
* The bridge in this package gives an agent a voice — it hears mentions and
|
|
5
|
+
* answers as the person who asked. This gives it everything else the platform
|
|
6
|
+
* has: projects, issues, pull requests, patches, repositories, long-form notes,
|
|
7
|
+
* channel canvases, workflows, the activity feed, media.
|
|
8
|
+
*
|
|
9
|
+
* Buzz's own design says an agent acts through its CLI — the harness prompts
|
|
10
|
+
* the agent, "and the agent replies using the Buzz CLI". We replaced the
|
|
11
|
+
* harness half to get governed per-person identity, and for a while shipped
|
|
12
|
+
* only that half: agents could talk in a workspace and do nothing in it, and
|
|
13
|
+
* would say Projects did not exist because nothing had told them otherwise.
|
|
14
|
+
*/
|
|
15
|
+
declare const _default: import("eve/extension").NoConfigExtensionHandle;
|
|
16
|
+
export default _default;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { fileURLToPath as __eveFileURLToPath } from "node:url";
|
|
2
|
+
import { dirname as __eveDirname } from "node:path";
|
|
3
|
+
import { createRequire as __eveCreateRequire } from "node:module";
|
|
4
|
+
const __filename = __eveFileURLToPath(import.meta.url);
|
|
5
|
+
__eveDirname(__filename);
|
|
6
|
+
__eveCreateRequire(import.meta.url);
|
|
7
|
+
import { defineExtension } from "eve/extension";
|
|
8
|
+
var extension_default = defineExtension();
|
|
9
|
+
export { extension_default as default };
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
## Acting in Buzz
|
|
2
|
+
|
|
3
|
+
You are a **member** of this workspace, with your own standing — not a visitor
|
|
4
|
+
driving someone else's account. Alongside talking in channels you can use the
|
|
5
|
+
`buzz` tool to work in it: projects, issues, pull requests, patches, repos,
|
|
6
|
+
long-form notes, channel canvases, workflows, the feed, media, custom emoji.
|
|
7
|
+
|
|
8
|
+
Run `buzz` with `help` (or a group's `--help`) to see exactly what a group takes
|
|
9
|
+
rather than guessing at flags. The surface is large and changes; the help output
|
|
10
|
+
is authoritative and this page is not.
|
|
11
|
+
|
|
12
|
+
The groups, and what each is for:
|
|
13
|
+
|
|
14
|
+
| group | use it for |
|
|
15
|
+
| --- | --- |
|
|
16
|
+
| `projects` | multi-repo projects — create, list, add or remove repos, update, delete |
|
|
17
|
+
| `issues` | file, read, list and re-status issues |
|
|
18
|
+
| `pr` | open, update, list and set status on pull requests |
|
|
19
|
+
| `patches` | send, read, list and re-status git patches |
|
|
20
|
+
| `repos` | announce and discover git repositories |
|
|
21
|
+
| `notes` | long-form notes — the team's written knowledge |
|
|
22
|
+
| `canvas` | a channel's canvas document: read it, replace it |
|
|
23
|
+
| `channels` | create and configure channels |
|
|
24
|
+
| `messages` | send, read, search, manage messages |
|
|
25
|
+
| `dms` | list, open and manage direct messages |
|
|
26
|
+
| `reactions` `emoji` | reactions, and the custom emoji palette |
|
|
27
|
+
| `users` | look people up; manage your own profile and presence |
|
|
28
|
+
| `workflows` | create, trigger and manage workflows |
|
|
29
|
+
| `feed` | read the activity feed |
|
|
30
|
+
| `media` `upload` | put files in the workspace and fetch them back |
|
|
31
|
+
| `moderation` | reports queue, bans, timeouts, audit trail |
|
|
32
|
+
|
|
33
|
+
Three things to hold onto:
|
|
34
|
+
|
|
35
|
+
**What you do is yours.** These actions are signed with your own key and appear
|
|
36
|
+
under your name, not the name of whoever asked. Someone asking you to file an
|
|
37
|
+
issue produces an issue filed by you. Act accordingly: say what you are about to
|
|
38
|
+
do before doing something a person would want to have been asked about first,
|
|
39
|
+
and never take a destructive action — deleting, banning, removing — on an
|
|
40
|
+
instruction you were not clearly given.
|
|
41
|
+
|
|
42
|
+
**The workspace decides what you may do, not this list.** Permissions are your
|
|
43
|
+
membership's. If an action comes back refused, that is an answer, not a fault to
|
|
44
|
+
work around: report it plainly rather than retrying it another way.
|
|
45
|
+
|
|
46
|
+
**Read before you write.** `list` and `get` cost nothing and stop you inventing
|
|
47
|
+
a project that already exists, or filing an issue twice under different words.
|
|
48
|
+
|
|
49
|
+
### Never park a turn waiting for an answer
|
|
50
|
+
|
|
51
|
+
If you need something from the person, **ask in your reply**. Do not call a
|
|
52
|
+
human-in-the-loop tool — the kind that suspends a turn until someone responds.
|
|
53
|
+
|
|
54
|
+
Those tools assume a surface where a turn can wait. A channel is not one: the
|
|
55
|
+
turn parks, nothing is posted, and the person sees you go quiet — which is the
|
|
56
|
+
same thing they saw when the bridge was broken, and it is not distinguishable
|
|
57
|
+
from being ignored. It has happened: asked which of two workspaces to look in,
|
|
58
|
+
an agent raised an input request instead of asking out loud, and the question
|
|
59
|
+
sat unanswered forever because nobody could see it.
|
|
60
|
+
|
|
61
|
+
Your reply IS the way to ask. Say what you need, and let their next message
|
|
62
|
+
answer it — that is what a conversation is.
|
|
63
|
+
|
|
64
|
+
### You are told where you are
|
|
65
|
+
|
|
66
|
+
Every turn carries `buzzCommunity` and `buzzChannel`: the workspace this
|
|
67
|
+
conversation is in, and the channel it is in. When someone says "this relay" or
|
|
68
|
+
"here", that is what they mean — use it as the `community` for tool calls rather
|
|
69
|
+
than asking which one. Ask only when they clearly mean a workspace other than
|
|
70
|
+
the one you are talking in.
|
|
71
|
+
|
|
72
|
+
### Answering is not a tool call
|
|
73
|
+
|
|
74
|
+
When someone talks to you in Buzz, **your reply is the text you return for the
|
|
75
|
+
turn**. The bridge posts it: threaded to the message you are answering, with the
|
|
76
|
+
seen mark and the typing indicator already handled.
|
|
77
|
+
|
|
78
|
+
Do not use `messages send` to answer the message you are currently answering.
|
|
79
|
+
Both paths post as you, so the room gets the same answer twice — once threaded
|
|
80
|
+
and once loose — and the two arrive in an order nobody chose. Answering twice is
|
|
81
|
+
a bug people have already lived through here; do not recreate it from the other
|
|
82
|
+
side.
|
|
83
|
+
|
|
84
|
+
`messages send` is for messages that are **not** this turn's answer:
|
|
85
|
+
|
|
86
|
+
- posting into a *different* channel ("tell #eng the deploy is done")
|
|
87
|
+
- starting a conversation nobody prompted — a scheduled summary, a heads-up
|
|
88
|
+
- following up later, after the turn that was asked of you has ended
|
|
89
|
+
|
|
90
|
+
The same distinction applies to reactions: the bridge marks the message you are
|
|
91
|
+
answering, so use `reactions` only on *other* messages.
|
|
92
|
+
|
|
93
|
+
### Projects and repos overlap
|
|
94
|
+
|
|
95
|
+
The Projects view people look at lists **both**: entries with a project record of
|
|
96
|
+
their own, and entries that exist only as a repository. So "what projects do we
|
|
97
|
+
have?" is answered by looking at both kinds, not just projects — answering with
|
|
98
|
+
one of them is how you tell someone an item they are looking at right now does
|
|
99
|
+
not exist.
|
|
100
|
+
|
|
101
|
+
Say which is which when it matters ("three, though one is a repo without a
|
|
102
|
+
project record"), and never correct someone's own vocabulary at the cost of
|
|
103
|
+
answering their question.
|
|
104
|
+
|
|
105
|
+
### Which workspace
|
|
106
|
+
|
|
107
|
+
If this agent belongs to more than one community, every command must say which
|
|
108
|
+
one — pass `community` with any distinctive part of its address. There is no
|
|
109
|
+
default: a project created in the wrong company's workspace is not a mistake
|
|
110
|
+
that can be taken back.
|
|
111
|
+
|
|
112
|
+
### When it is not installed
|
|
113
|
+
|
|
114
|
+
If the tool says the CLI is missing, say so plainly and stop. It is one command
|
|
115
|
+
on the host (`kybernesis-buzz install-cli`) and it is not something to work
|
|
116
|
+
around by posting a message asking someone else to do the task.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
declare const _default: import("eve/tools").ToolDefinition<{
|
|
2
|
+
args: string[];
|
|
3
|
+
community?: string | undefined;
|
|
4
|
+
}, string> & {
|
|
5
|
+
execute(input: {
|
|
6
|
+
args: string[];
|
|
7
|
+
community?: string | undefined;
|
|
8
|
+
}, ctx: import("eve/tools").ToolContext): Promise<string>;
|
|
9
|
+
};
|
|
10
|
+
export default _default;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { fileURLToPath as __eveFileURLToPath } from "node:url";
|
|
2
|
+
import { dirname as __eveDirname } from "node:path";
|
|
3
|
+
import { createRequire as __eveCreateRequire } from "node:module";
|
|
4
|
+
const __filename = __eveFileURLToPath(import.meta.url);
|
|
5
|
+
__eveDirname(__filename);
|
|
6
|
+
__eveCreateRequire(import.meta.url);
|
|
7
|
+
import { execFile } from "node:child_process";
|
|
8
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
9
|
+
import { resolve } from "node:path";
|
|
10
|
+
import { promisify } from "node:util";
|
|
11
|
+
import { defineTool } from "eve/tools";
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
const run = promisify(execFile);
|
|
14
|
+
function secretKeyHex(keyFile) {
|
|
15
|
+
const stored = JSON.parse(readFileSync(keyFile, "utf8"));
|
|
16
|
+
return Buffer.from(Uint8Array.from(stored.secretKey)).toString("hex");
|
|
17
|
+
}
|
|
18
|
+
var buzz_default = defineTool({
|
|
19
|
+
description: "Act in the Buzz workspace: projects, issues, pull requests, patches, repos, long-form notes, channel canvases, workflows, the activity feed, media, emoji, moderation. Pass the command as a list of arguments, e.g. [\"projects\",\"list\"] or [\"issues\",\"create\",\"--repo\",\"acme/api\",\"--title\",\"Login fails on Safari\"]. Use [\"help\"] or [\"<group>\",\"--help\"] to see what a group takes instead of guessing at flags. Actions are signed as THIS AGENT and appear under its name. Do NOT use it to answer the message you are currently answering — returning your reply for the turn does that, threaded and marked; sending as well posts the same answer twice. Use messages/dms only for a DIFFERENT channel, an unprompted message, or a later follow-up.",
|
|
20
|
+
inputSchema: z.object({
|
|
21
|
+
args: z.array(z.string()).min(1).describe("The command as separate arguments, without the leading \"buzz\". Example: [\"projects\",\"get\",\"--slug\",\"atlas\"]."),
|
|
22
|
+
community: z.string().optional().describe("Which workspace to act in, when this agent belongs to more than one. Any distinctive part of its address is enough. Omit it when there is only one.")
|
|
23
|
+
}),
|
|
24
|
+
async execute({ args, community }) {
|
|
25
|
+
const local = ".buzz/bin/buzz";
|
|
26
|
+
const binary = process.env.BUZZ_CLI_PATH ?? (existsSync(local) ? resolve(local) : "buzz");
|
|
27
|
+
const keyFile = process.env.BUZZ_KEYFILE ?? ".buzz-key.json";
|
|
28
|
+
const configured = (process.env.BUZZ_RELAY_HTTP ?? process.env.BUZZ_RELAY ?? "").split(",").map((entry) => entry.trim().replace(/^ws/, "http")).filter(Boolean);
|
|
29
|
+
if (configured.length === 0) return "No Buzz relay configured: set BUZZ_RELAY (the bridge's websocket URL) or BUZZ_RELAY_HTTP.";
|
|
30
|
+
let relay;
|
|
31
|
+
if (community) {
|
|
32
|
+
const matches = configured.filter((entry) => entry.includes(community));
|
|
33
|
+
if (matches.length !== 1) return `"${community}" ${matches.length === 0 ? "matches none of" : "matches more than one of"} this agent's workspaces: ${configured.join(", ")}`;
|
|
34
|
+
relay = matches[0];
|
|
35
|
+
} else if (configured.length > 1) return `This agent belongs to more than one workspace, so say which one to act in: ${configured.join(", ")}. Pass it as "community".`;
|
|
36
|
+
else relay = configured[0];
|
|
37
|
+
if (!existsSync(keyFile)) return `No Buzz identity at ${keyFile}. This agent has no standing in the workspace until \`kybernesis-buzz init\` has run on this host.`;
|
|
38
|
+
try {
|
|
39
|
+
const { stdout } = await run(binary, args, {
|
|
40
|
+
env: {
|
|
41
|
+
...process.env,
|
|
42
|
+
BUZZ_RELAY_URL: relay,
|
|
43
|
+
BUZZ_PRIVATE_KEY: secretKeyHex(keyFile)
|
|
44
|
+
},
|
|
45
|
+
timeout: 12e4,
|
|
46
|
+
maxBuffer: 4194304
|
|
47
|
+
});
|
|
48
|
+
return stdout.trim() || "(no output)";
|
|
49
|
+
} catch (error) {
|
|
50
|
+
const failure = error;
|
|
51
|
+
if (failure.code === "ENOENT") return `The Buzz CLI is not installed on this host (looked for "${binary}"). Install it with \`kybernesis-buzz install-cli\`, or set BUZZ_CLI_PATH to where it lives.`;
|
|
52
|
+
return (failure.stderr ?? failure.message ?? "").trim() || "The Buzz CLI failed without saying why.";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
export { buzz_default as default };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
declare const _default: import("eve/tools").ToolDefinition<{
|
|
2
|
+
type?: "issue" | "message" | "note" | "patch" | "profile" | "project" | "repo" | undefined;
|
|
3
|
+
kinds?: number[] | undefined;
|
|
4
|
+
authors?: string[] | undefined;
|
|
5
|
+
limit?: number | undefined;
|
|
6
|
+
community?: string | undefined;
|
|
7
|
+
}, string> & {
|
|
8
|
+
execute(input: {
|
|
9
|
+
type?: "issue" | "message" | "note" | "patch" | "profile" | "project" | "repo" | undefined;
|
|
10
|
+
kinds?: number[] | undefined;
|
|
11
|
+
authors?: string[] | undefined;
|
|
12
|
+
limit?: number | undefined;
|
|
13
|
+
community?: string | undefined;
|
|
14
|
+
}, ctx: import("eve/tools").ToolContext): Promise<string>;
|
|
15
|
+
};
|
|
16
|
+
export default _default;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { fileURLToPath as __eveFileURLToPath } from "node:url";
|
|
2
|
+
import { dirname as __eveDirname } from "node:path";
|
|
3
|
+
import { createRequire as __eveCreateRequire } from "node:module";
|
|
4
|
+
const __filename = __eveFileURLToPath(import.meta.url);
|
|
5
|
+
__eveDirname(__filename);
|
|
6
|
+
__eveCreateRequire(import.meta.url);
|
|
7
|
+
import { t as find_default } from "../../_chunks/find-B2f5dymb.mjs";
|
|
8
|
+
export { find_default as default };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { fileURLToPath as __eveFileURLToPath } from "node:url";
|
|
2
|
+
import { dirname as __eveDirname } from "node:path";
|
|
3
|
+
import { createRequire as __eveCreateRequire } from "node:module";
|
|
4
|
+
const __filename = __eveFileURLToPath(import.meta.url);
|
|
5
|
+
__eveDirname(__filename);
|
|
6
|
+
__eveCreateRequire(import.meta.url);
|
|
7
|
+
import extension_default from "./extension/extension.mjs";
|
|
8
|
+
export { extension_default as buzz, extension_default as default };
|
package/dist/query.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type NostrEvent } from "./relay.js";
|
|
2
|
+
/**
|
|
3
|
+
* Read the whole workspace, not just your own corner of it.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* The Buzz CLI's list commands are owner-scoped — `--owner` defaults to the
|
|
7
|
+
* current identity — which is right for "my repos" and wrong for the question
|
|
8
|
+
* people actually ask an agent: *what projects exist here?* Asked that in a
|
|
9
|
+
* workspace holding three, an agent answered "none", because it had listed its
|
|
10
|
+
* own and had none. It was not broken and it was not lying; it had been given a
|
|
11
|
+
* tool that could only see one author and no way to know that mattered.
|
|
12
|
+
*
|
|
13
|
+
* There is no CLI command for the general case, but the relay has the endpoint
|
|
14
|
+
* the CLI itself uses: `POST /query`, NIP-98 signed, taking an array of Nostr
|
|
15
|
+
* filters. That is one request away from every kind the platform stores —
|
|
16
|
+
* projects, repos, issues, patches, notes — across everyone in the workspace.
|
|
17
|
+
*/
|
|
18
|
+
export declare function queryRelay(relayHttpUrl: string, secretKey: Uint8Array, filters: readonly Record<string, unknown>[]): Promise<{
|
|
19
|
+
events: NostrEvent[];
|
|
20
|
+
status: number;
|
|
21
|
+
bytes: number;
|
|
22
|
+
}>;
|
package/dist/query.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { finalizeEvent } from "nostr-tools/pure";
|
|
3
|
+
import { KIND_HTTP_AUTH } from "./relay.js";
|
|
4
|
+
/**
|
|
5
|
+
* Read the whole workspace, not just your own corner of it.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* The Buzz CLI's list commands are owner-scoped — `--owner` defaults to the
|
|
9
|
+
* current identity — which is right for "my repos" and wrong for the question
|
|
10
|
+
* people actually ask an agent: *what projects exist here?* Asked that in a
|
|
11
|
+
* workspace holding three, an agent answered "none", because it had listed its
|
|
12
|
+
* own and had none. It was not broken and it was not lying; it had been given a
|
|
13
|
+
* tool that could only see one author and no way to know that mattered.
|
|
14
|
+
*
|
|
15
|
+
* There is no CLI command for the general case, but the relay has the endpoint
|
|
16
|
+
* the CLI itself uses: `POST /query`, NIP-98 signed, taking an array of Nostr
|
|
17
|
+
* filters. That is one request away from every kind the platform stores —
|
|
18
|
+
* projects, repos, issues, patches, notes — across everyone in the workspace.
|
|
19
|
+
*/
|
|
20
|
+
export async function queryRelay(relayHttpUrl, secretKey, filters) {
|
|
21
|
+
const url = `${relayHttpUrl.replace(/\/$/, "")}/query`;
|
|
22
|
+
const body = JSON.stringify(filters);
|
|
23
|
+
// Signed exactly as the socket path signs: the signature binds the body, so a
|
|
24
|
+
// filter cannot be swapped in transit.
|
|
25
|
+
const auth = finalizeEvent({
|
|
26
|
+
kind: KIND_HTTP_AUTH,
|
|
27
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
28
|
+
tags: [
|
|
29
|
+
["u", url],
|
|
30
|
+
["method", "POST"],
|
|
31
|
+
// A nonce, because the relay rejects a repeated auth event as a replay.
|
|
32
|
+
// Without it two identical requests — the same query twice, the same
|
|
33
|
+
// reaction twice — sign byte-identically and the second is refused. The
|
|
34
|
+
// refusal does not always look like one either: a duplicated read came
|
|
35
|
+
// back as an empty list, so an agent reported an empty workspace instead
|
|
36
|
+
// of a rejected request.
|
|
37
|
+
["nonce", randomUUID()],
|
|
38
|
+
["payload", createHash("sha256").update(body).digest("hex")],
|
|
39
|
+
],
|
|
40
|
+
content: "",
|
|
41
|
+
}, secretKey);
|
|
42
|
+
// Bound to this module deliberately: eve's runtime replaces the global fetch
|
|
43
|
+
// in the deployed server, and a request that goes through a different client
|
|
44
|
+
// than the one that signed it can arrive unauthenticated — which this relay
|
|
45
|
+
// answers with an empty list rather than a refusal, so it reads as "nothing
|
|
46
|
+
// here" instead of "you are not who you said you were".
|
|
47
|
+
const response = await fetch(url, {
|
|
48
|
+
method: "POST",
|
|
49
|
+
headers: {
|
|
50
|
+
authorization: `Nostr ${Buffer.from(JSON.stringify(auth)).toString("base64")}`,
|
|
51
|
+
"content-type": "application/json",
|
|
52
|
+
},
|
|
53
|
+
body,
|
|
54
|
+
});
|
|
55
|
+
const text = await response.text();
|
|
56
|
+
if (!response.ok) {
|
|
57
|
+
throw new Error(`relay ${response.status}: ${text.slice(0, 300)}`);
|
|
58
|
+
}
|
|
59
|
+
// The status and size travel with the answer because an empty list and a
|
|
60
|
+
// rejected request look identical to a caller that only sees the events, and
|
|
61
|
+
// this relay answers some refusals with 200 and an empty array.
|
|
62
|
+
return { events: JSON.parse(text), status: response.status, bytes: text.length };
|
|
63
|
+
}
|
package/dist/relay.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { finalizeEvent } from "nostr-tools/pure";
|
|
3
3
|
/**
|
|
4
4
|
* The workspace protocol, as far as a member needs it.
|
|
@@ -235,6 +235,13 @@ export class BuzzRelay {
|
|
|
235
235
|
tags: [
|
|
236
236
|
["u", url],
|
|
237
237
|
["method", "POST"],
|
|
238
|
+
// A nonce, because the relay rejects a repeated auth event as a replay.
|
|
239
|
+
// Without it two identical requests — the same query twice, the same
|
|
240
|
+
// reaction twice — sign byte-identically and the second is refused. The
|
|
241
|
+
// refusal does not always look like one either: a duplicated read came
|
|
242
|
+
// back as an empty list, so an agent reported an empty workspace instead
|
|
243
|
+
// of a rejected request.
|
|
244
|
+
["nonce", randomUUID()],
|
|
238
245
|
["payload", createHash("sha256").update(body).digest("hex")],
|
|
239
246
|
],
|
|
240
247
|
content: "",
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { fileURLToPath as __eveFileURLToPath } from "node:url";
|
|
2
|
+
import { dirname as __eveDirname } from "node:path";
|
|
3
|
+
import { createRequire as __eveCreateRequire } from "node:module";
|
|
4
|
+
const __filename = __eveFileURLToPath(import.meta.url);
|
|
5
|
+
__eveDirname(__filename);
|
|
6
|
+
__eveCreateRequire(import.meta.url);
|
|
7
|
+
import buzz_default from "../extension/tools/buzz.mjs";
|
|
8
|
+
import { t as find_default } from "../_chunks/find-B2f5dymb.mjs";
|
|
9
|
+
export { buzz_default as buzz, find_default as find };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kybernesis/buzz",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "Put an eve agent in a Buzz workspace as a member: per-speaker verified identity resolved through the control plane, self-service identity linking, presence, typing and seen signals.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -24,20 +24,29 @@
|
|
|
24
24
|
"exports": {
|
|
25
25
|
".": {
|
|
26
26
|
"types": "./dist/index.d.ts",
|
|
27
|
-
"default": "./dist/index.
|
|
27
|
+
"default": "./dist/index.mjs"
|
|
28
28
|
},
|
|
29
|
-
"./package.json": "./package.json"
|
|
29
|
+
"./package.json": "./package.json",
|
|
30
|
+
"./tools": {
|
|
31
|
+
"types": "./dist/tools/index.d.ts",
|
|
32
|
+
"default": "./dist/tools/index.mjs"
|
|
33
|
+
},
|
|
34
|
+
"./extension": {
|
|
35
|
+
"types": "./dist/extension/extension.d.ts",
|
|
36
|
+
"default": "./dist/extension/extension.mjs"
|
|
37
|
+
}
|
|
30
38
|
},
|
|
31
39
|
"scripts": {
|
|
32
|
-
"build": "tsc -p tsconfig.build.json",
|
|
33
|
-
"prepack": "tsc -p tsconfig.build.json",
|
|
40
|
+
"build": "eve extension build && tsc -p tsconfig.build.json",
|
|
41
|
+
"prepack": "eve extension build && tsc -p tsconfig.build.json",
|
|
34
42
|
"typecheck": "tsc --noEmit",
|
|
35
43
|
"prepublishOnly": "node ../../scripts/prepublish.mjs",
|
|
36
44
|
"test": "node --test \"test/**/*.test.mjs\""
|
|
37
45
|
},
|
|
38
46
|
"dependencies": {
|
|
39
47
|
"@kybernesis/enterprise": "^0.7.0",
|
|
40
|
-
"nostr-tools": "^2.10.0"
|
|
48
|
+
"nostr-tools": "^2.10.0",
|
|
49
|
+
"zod": "^4"
|
|
41
50
|
},
|
|
42
51
|
"devDependencies": {
|
|
43
52
|
"@types/node": "24.x",
|
|
@@ -54,5 +63,11 @@
|
|
|
54
63
|
"type": "git",
|
|
55
64
|
"url": "https://github.com/KybernesisAI/platform.git",
|
|
56
65
|
"directory": "packages/buzz"
|
|
66
|
+
},
|
|
67
|
+
"eve": {
|
|
68
|
+
"extension": {
|
|
69
|
+
"source": "./extension",
|
|
70
|
+
"dist": "./dist/extension"
|
|
71
|
+
}
|
|
57
72
|
}
|
|
58
73
|
}
|