@butlerbot/sdk 0.0.18-alpha.3 → 0.0.19

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.
@@ -0,0 +1,28 @@
1
+ import { EventSource } from "eventsource";
2
+ import { ConversationStream, ConversationTransport, TransportHandlers, TransportTurnRequest } from "./transport";
3
+ type StreamOptions = {
4
+ debug?: boolean;
5
+ onPayload(payload: {
6
+ success: boolean;
7
+ data?: {
8
+ convoId?: string;
9
+ quitStream?: boolean;
10
+ };
11
+ }): void;
12
+ };
13
+ /**
14
+ * Streams a server-sent-events endpoint, closing when the server says the stream is
15
+ * done. Shared by turns and by the progress stream, which is SSE-only.
16
+ */
17
+ export declare function streamSSE(url: string, options: StreamOptions): EventSource;
18
+ /** Carries a turn over the HTTP chat endpoint. The default. */
19
+ export declare class SSEConversationTransport implements ConversationTransport {
20
+ private readonly config;
21
+ constructor(config: {
22
+ endpoint(): string;
23
+ apiKey: string;
24
+ debug?: boolean;
25
+ });
26
+ send(request: TransportTurnRequest, handlers: TransportHandlers): ConversationStream;
27
+ }
28
+ export {};
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SSEConversationTransport = void 0;
4
+ exports.streamSSE = streamSSE;
5
+ const eventsource_1 = require("eventsource");
6
+ const url_formatter_1 = require("../util/url_formatter");
7
+ /**
8
+ * Streams a server-sent-events endpoint, closing when the server says the stream is
9
+ * done. Shared by turns and by the progress stream, which is SSE-only.
10
+ */
11
+ function streamSSE(url, options) {
12
+ const sse = new eventsource_1.EventSource(url);
13
+ sse.addEventListener("message", (event) => {
14
+ const payload = JSON.parse(event.data);
15
+ options.onPayload(payload);
16
+ if (payload.data?.quitStream)
17
+ sse.close();
18
+ });
19
+ sse.addEventListener("error", (event) => {
20
+ if (options.debug)
21
+ console.warn(`[Stream Error: ${url}]`, event);
22
+ });
23
+ return sse;
24
+ }
25
+ /** Carries a turn over the HTTP chat endpoint. The default. */
26
+ class SSEConversationTransport {
27
+ constructor(config) {
28
+ this.config = config;
29
+ }
30
+ send(request, handlers) {
31
+ const url = (0, url_formatter_1.formatURL)(this.config.endpoint(), asQuery(request), { apiKey: this.config.apiKey, debug: this.config.debug });
32
+ const sse = streamSSE(url, {
33
+ debug: this.config.debug,
34
+ onPayload: (payload) => {
35
+ const convoId = payload.success ? payload.data?.convoId : undefined;
36
+ if (convoId)
37
+ handlers.convoId(convoId);
38
+ handlers.payload(payload);
39
+ },
40
+ });
41
+ return { close: () => sse.close(), source: sse };
42
+ }
43
+ }
44
+ exports.SSEConversationTransport = SSEConversationTransport;
45
+ function asQuery(request) {
46
+ const query = { message: request.message };
47
+ if (request.chatId)
48
+ query.chatId = request.chatId;
49
+ if (request.model)
50
+ query.model = request.model;
51
+ if (request.instructions)
52
+ query.instructions = request.instructions;
53
+ if (request.platform)
54
+ query.platform = request.platform;
55
+ if (request.personality)
56
+ query.personality = request.personality;
57
+ return query;
58
+ }
@@ -1,34 +1,23 @@
1
1
  "use strict";
2
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
- return new (P || (P = Promise))(function (resolve, reject) {
5
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
- step((generator = generator.apply(thisArg, _arguments || [])).next());
9
- });
10
- };
11
2
  Object.defineProperty(exports, "__esModule", { value: true });
12
3
  exports.getUsagePolicyData = getUsagePolicyData;
13
4
  const config_1 = require("../config");
14
5
  const url_formatter_1 = require("../util/url_formatter");
15
6
  /** Fetches the current usage policy data from the server */
16
- function getUsagePolicyData(options) {
17
- return __awaiter(this, void 0, void 0, function* () {
18
- const useEndpoint = (options.serverURL || config_1.CONFIG.server) + (options.path || config_1.CONFIG.paths.usage.policy.v3.base);
19
- const url = (0, url_formatter_1.formatURL)(useEndpoint, {}, { apiKey: options.apiKey, debug: options.debug });
20
- const response = yield fetch(url);
21
- const data = yield response.json();
22
- if (!response.ok) {
23
- const errorText = yield response.text();
24
- throw new Error(`Failed to fetch usage policy data: ${response.status} ${response.statusText} - ${errorText}`);
25
- }
26
- if (!data.success) {
27
- throw new Error(`API error while fetching usage policy data: ${data.error || 'Unknown error'}`);
28
- }
29
- if (!data.policy) {
30
- throw new Error(`API error while fetching usage policy data: ${data.error || 'Unknown error'}`);
31
- }
32
- return data.policy;
33
- });
7
+ async function getUsagePolicyData(options) {
8
+ const useEndpoint = (options.serverURL || config_1.CONFIG.server) + (options.path || config_1.CONFIG.paths.usage.policy.v3.base);
9
+ const url = (0, url_formatter_1.formatURL)(useEndpoint, {}, { apiKey: options.apiKey, debug: options.debug });
10
+ const response = await fetch(url);
11
+ const data = await response.json();
12
+ if (!response.ok) {
13
+ const errorText = await response.text();
14
+ throw new Error(`Failed to fetch usage policy data: ${response.status} ${response.statusText} - ${errorText}`);
15
+ }
16
+ if (!data.success) {
17
+ throw new Error(`API error while fetching usage policy data: ${data.error || 'Unknown error'}`);
18
+ }
19
+ if (!data.policy) {
20
+ throw new Error(`API error while fetching usage policy data: ${data.error || 'Unknown error'}`);
21
+ }
22
+ return data.policy;
34
23
  }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * A tiny typed event emitter.
3
+ *
4
+ * `on` returns an id used to remove the listener again, which is the convention
5
+ * the SDK already used for conversation events.
6
+ */
7
+ export declare class Emitter<Events extends Record<string, unknown[]>> {
8
+ private listeners;
9
+ private nextId;
10
+ on<K extends keyof Events>(event: K, listener: (...args: Events[K]) => unknown): string;
11
+ once<K extends keyof Events>(event: K, listener: (...args: Events[K]) => unknown): string;
12
+ off<K extends keyof Events>(event: K, id: string): void;
13
+ emit<K extends keyof Events>(event: K, ...args: Events[K]): void;
14
+ clear(event?: keyof Events): void;
15
+ }
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Emitter = void 0;
4
+ /**
5
+ * A tiny typed event emitter.
6
+ *
7
+ * `on` returns an id used to remove the listener again, which is the convention
8
+ * the SDK already used for conversation events.
9
+ */
10
+ class Emitter {
11
+ constructor() {
12
+ this.listeners = new Map();
13
+ this.nextId = 0;
14
+ }
15
+ on(event, listener) {
16
+ let forEvent = this.listeners.get(event);
17
+ if (!forEvent) {
18
+ forEvent = new Map();
19
+ this.listeners.set(event, forEvent);
20
+ }
21
+ const id = `l${++this.nextId}`;
22
+ forEvent.set(id, listener);
23
+ return id;
24
+ }
25
+ once(event, listener) {
26
+ const id = this.on(event, ((...args) => {
27
+ this.off(event, id);
28
+ return listener(...args);
29
+ }));
30
+ return id;
31
+ }
32
+ off(event, id) {
33
+ this.listeners.get(event)?.delete(id);
34
+ }
35
+ emit(event, ...args) {
36
+ const forEvent = this.listeners.get(event);
37
+ if (!forEvent)
38
+ return;
39
+ // Copied first: a listener may remove itself, or another, while we iterate.
40
+ for (const listener of Array.from(forEvent.values())) {
41
+ listener(...args);
42
+ }
43
+ }
44
+ clear(event) {
45
+ if (event === undefined)
46
+ this.listeners.clear();
47
+ else
48
+ this.listeners.delete(event);
49
+ }
50
+ }
51
+ exports.Emitter = Emitter;
@@ -2,7 +2,10 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.formatURL = formatURL;
4
4
  function formatURL(url, params = {}, config) {
5
- const fParams = Object.assign({ api_key: config.apiKey }, params);
5
+ const fParams = {
6
+ api_key: config.apiKey,
7
+ ...params
8
+ };
6
9
  const paramQuery = new URLSearchParams(fParams).toString();
7
10
  const fUrl = `${url}?${paramQuery}`;
8
11
  if (config.debug) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@butlerbot/sdk",
3
- "version": "0.0.18-alpha.3",
3
+ "version": "0.0.19",
4
4
  "description": "The official ButlerBot SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -12,34 +12,58 @@
12
12
  "pack": "npm run build && npm pack --pack-destination=\"G:\\tarballs\"",
13
13
  "update": "npm run build && npm publish --access public",
14
14
  "update:alpha": "npm run build && npm publish --access public --tag alpha",
15
- "test": "jest"
15
+ "test": "bun test",
16
+ "typecheck": "tsc --noEmit -p tsconfig.check.json"
16
17
  },
17
18
  "keywords": [
18
- "butlerbot",
19
+ "agents",
20
+ "ai",
19
21
  "alfred",
20
- "sdk",
22
+ "butler",
23
+ "butlerbot",
21
24
  "chatbot",
22
- "ai",
23
- "butler"
25
+ "sdk",
26
+ "tools",
27
+ "websocket"
24
28
  ],
25
29
  "author": "Fragly",
26
30
  "license": "MIT",
27
31
  "repository": {
28
32
  "type": "git",
29
- "url": "git+https://github.com/isdevco/alfred5_sdk.git"
33
+ "url": "git+https://github.com/butlerbots/alfred5_sdk.git"
30
34
  },
31
35
  "bugs": {
32
- "url": "https://github.com/isdevco/alfred5_sdk/issues"
36
+ "url": "https://github.com/butlerbots/alfred5_sdk/issues"
33
37
  },
34
- "homepage": "https://butlerbot.net",
38
+ "homepage": "https://butler.now",
35
39
  "engines": {
36
- "node": ">=14.0.0"
40
+ "node": ">=18.0.0"
37
41
  },
38
42
  "devDependencies": {
43
+ "@types/bun": "^1.1.0",
39
44
  "@types/node": "^22.13.10",
40
- "typescript": "^5.8.2"
45
+ "typescript": "^5.8.2",
46
+ "zod": "^4.0.0"
41
47
  },
42
48
  "dependencies": {
43
49
  "eventsource": "^3.0.5"
50
+ },
51
+ "exports": {
52
+ ".": {
53
+ "types": "./dist/index.d.ts",
54
+ "default": "./dist/index.js"
55
+ }
56
+ },
57
+ "peerDependencies": {
58
+ "zod": ">=3.24.0",
59
+ "ws": ">=8.0.0"
60
+ },
61
+ "peerDependenciesMeta": {
62
+ "zod": {
63
+ "optional": true
64
+ },
65
+ "ws": {
66
+ "optional": true
67
+ }
44
68
  }
45
69
  }
package/readme.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # ButlerBot SDK
2
2
 
3
- ButlerBot SDK is a JavaScript library that provides a simple way to interact with the [ButlerBot](https://butlerbot.net/) API.
3
+ ButlerBot SDK is a JavaScript library that provides a simple way to interact with the [ButlerBot](https://butler.now/) API.
4
4
 
5
5
  ## Quickstart
6
6
 
7
- Grab an API key at [ButlerBot](https://butlerbot.net/) and install the package:
7
+ Grab an API key at [ButlerBot](https://butler.now/) and install the package:
8
8
 
9
9
  ```bash
10
10
  npm i @butlerbot/sdk
@@ -16,7 +16,7 @@ npm i @butlerbot/sdk
16
16
 
17
17
  - ButlerBot API key
18
18
 
19
- ## Example
19
+ ## Talking to Alfred
20
20
 
21
21
  ```typescript
22
22
  import { ButlerBotClient } from "@butlerbot/sdk";
@@ -34,3 +34,128 @@ convo.send("Hey there Alfred!", (res) => {
34
34
  console.log(type, payload); // message { message: "Good day", ... }
35
35
  });
36
36
  ```
37
+
38
+ Or, when you only want the answer:
39
+
40
+ ```typescript
41
+ const { text } = await convo.ask("Hey there Alfred!");
42
+ ```
43
+
44
+ ## Link
45
+
46
+ A Link is a live connection to Alfred. It does three things:
47
+
48
+ - **Tools** — Alfred calls code that runs on your machine
49
+ - **Hooks** — your code wakes the user's background agents when something happens
50
+ - **Conversations** — turns are carried over the same connection instead of an HTTP stream
51
+
52
+ ```typescript
53
+ import { ButlerBotClient, Tool, Hook } from "@butlerbot/sdk";
54
+ import { z } from "zod";
55
+
56
+ const client = new ButlerBotClient({ apiKey: "your_api_key_here" });
57
+ const link = client.createLink({ linkId: "coffee-machine" });
58
+
59
+ link.addTool(new Tool({
60
+ id: "brew",
61
+ description: "Brew a coffee for the user",
62
+ schema: z.object({ cups: z.number().int().min(1).max(4) }),
63
+ run: async ({ args, status }) => {
64
+ status.update("Grinding beans");
65
+ return `Brewed ${args.cups} cup(s).`; // args is typed from the schema
66
+ },
67
+ }));
68
+
69
+ const waterLow = new Hook({
70
+ id: "water-low",
71
+ name: "Water tank low",
72
+ description: "Fires when the water tank drops below a quarter full",
73
+ events: [{ name: "low", description: "The tank needs refilling" }],
74
+ });
75
+ link.addHook(waterLow);
76
+
77
+ await link.connect();
78
+ await waterLow.emit("low", { level: 0.2 });
79
+ ```
80
+
81
+ See [`examples/link.ts`](./examples/link.ts) for a fuller version.
82
+
83
+ ### linkId is permanent
84
+
85
+ `linkId` is yours to choose and must never change. Every id the link creates is derived
86
+ from it (`link:coffee-machine/brew`), and those ids are what the user's saved tool
87
+ settings and background agent subscriptions point at — so changing it silently orphans
88
+ both. Pick a deliberate constant; never a hostname, a version, or something generated at
89
+ startup.
90
+
91
+ Nothing else is stored on either side: the server keeps no record of a link between
92
+ connections, and the SDK re-declares everything on connect. A link can reconnect from
93
+ anywhere and land on the same settings.
94
+
95
+ Two live connections using the same `linkId` is last-writer-wins — the newer one takes
96
+ over and the older one's registrations are released. That is deliberate, so a half-dead
97
+ socket cannot lock out a fresh one during a deploy, but it does mean two genuinely
98
+ different clients must not share an id.
99
+
100
+ ### Tools belong to the user, not to a conversation
101
+
102
+ Once a tool is registered, Alfred can call it anywhere that user talks to it — the web
103
+ app and Discord included, not just conversations you started. `defaultEnabled` decides
104
+ whether it is on before the user has touched it; after that their own setting wins.
105
+
106
+ ### Schemas
107
+
108
+ `schema` takes a [zod](https://zod.dev) 4 schema, any
109
+ [Standard Schema](https://standardschema.dev), or a plain JSON Schema object. The SDK has
110
+ no dependency on any of them.
111
+
112
+ With a schema that can validate, arguments are checked before your tool runs (the server
113
+ deliberately doesn't — you wrote the schema, so you own the check) and `args` is typed
114
+ from it. With zod 3, pass `jsonSchema` alongside `schema`, since zod 3 cannot produce
115
+ JSON Schema itself.
116
+
117
+ ## Conversations over a Link
118
+
119
+ Pass a connected Link as the transport. Everything else is identical — the same methods,
120
+ the same payloads — so nothing that consumes a conversation needs to change:
121
+
122
+ ```typescript
123
+ const convo = client.createConversation({ transport: link }); // over the websocket
124
+ const overHttp = client.createConversation(); // over SSE, the default
125
+ ```
126
+
127
+ Which to use:
128
+
129
+ - **SSE** is the simplest thing that works and needs no connection to manage. Best for a
130
+ one-off request, a serverless function, or a page that just wants an answer.
131
+ - **A Link** reuses a connection you already have and avoids a new HTTP stream per turn.
132
+ Best when you are already running a link for tools or hooks, or holding many
133
+ conversations at once — one socket carries them all.
134
+
135
+ Two differences to know about:
136
+
137
+ - Sessions are ephemeral. If the connection drops mid-turn the SDK reopens the session
138
+ and resends transparently; the conversation itself is persisted server-side, so
139
+ nothing is lost.
140
+ - The HTTP transport replays your own message back to you (it exists so a browser
141
+ reconnecting mid-turn sees it). A Link does not, since it has nothing to replay.
142
+
143
+ Neither transport can cancel a turn: `close()` stops delivery locally, and the reply is
144
+ still generated and stored.
145
+
146
+ ## Environment
147
+
148
+ Node 18+. Node 22 and every browser have a built-in WebSocket; on older Node, install
149
+ `ws` or pass your own `socketFactory`.
150
+
151
+ Browsers are supported: a websocket handshake cannot carry headers there, so the SDK
152
+ sends the service and credential as subprotocols instead of putting the key in the URL.
153
+
154
+ ## Development
155
+
156
+ ```bash
157
+ npm install
158
+ npm test # bun test
159
+ npm run typecheck
160
+ npm run build
161
+ ```