@butlerbot/sdk 0.0.18-alpha.2 → 0.0.18
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/index.d.ts +10 -0
- package/dist/index.js +27 -25
- package/dist/link/hook.d.ts +70 -0
- package/dist/link/hook.js +60 -0
- package/dist/link/index.d.ts +11 -0
- package/dist/link/index.js +15 -0
- package/dist/link/link.d.ts +142 -0
- package/dist/link/link.js +427 -0
- package/dist/link/protocol.d.ts +186 -0
- package/dist/link/protocol.js +25 -0
- package/dist/link/schema.d.ts +80 -0
- package/dist/link/schema.js +88 -0
- package/dist/link/socket.d.ts +33 -0
- package/dist/link/socket.js +70 -0
- package/dist/link/tool.d.ts +95 -0
- package/dist/link/tool.js +52 -0
- package/dist/modules/conversation.d.ts +28 -9
- package/dist/modules/conversation.js +104 -122
- package/dist/modules/transport.d.ts +101 -0
- package/dist/modules/transport.js +78 -0
- package/dist/modules/transport_link.d.ts +28 -0
- package/dist/modules/transport_link.js +143 -0
- package/dist/modules/transport_sse.d.ts +28 -0
- package/dist/modules/transport_sse.js +58 -0
- package/dist/modules/usage.js +16 -27
- package/dist/types/conversation/v4/conversation_v4.d.ts +50 -0
- package/dist/types/conversation/v4/conversation_v4.js +2 -0
- package/dist/types/conversation/v4/message_v4.d.ts +85 -0
- package/dist/types/conversation/v4/message_v4.js +2 -0
- package/dist/types/state/convo_state_response.d.ts +2 -1
- package/dist/types/type_registry.d.ts +2 -0
- package/dist/types/type_registry.js +2 -0
- package/dist/util/emitter.d.ts +15 -0
- package/dist/util/emitter.js +51 -0
- package/dist/util/url_formatter.js +4 -1
- package/package.json +34 -10
- package/readme.md +126 -1
|
@@ -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
|
+
}
|
package/dist/modules/usage.js
CHANGED
|
@@ -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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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,50 @@
|
|
|
1
|
+
import type { MessagePayload, ReasoningPayload, FilePayload, ToolPayload, ConvoStatusPayload, ResponseStatusPayload, BaseResponseMetadata, DisplayEntity } from "../../response/v5/ai_response_v5";
|
|
2
|
+
import type { ConversationMessage } from "./message_v4";
|
|
3
|
+
export type DialogueEntry = {
|
|
4
|
+
from: "user" | "assistant";
|
|
5
|
+
display?: DisplayEntity;
|
|
6
|
+
timestamp: number;
|
|
7
|
+
metadata?: BaseResponseMetadata;
|
|
8
|
+
} & ({
|
|
9
|
+
type: "message";
|
|
10
|
+
payload: MessagePayload;
|
|
11
|
+
} | {
|
|
12
|
+
type: "reasoning";
|
|
13
|
+
payload: ReasoningPayload;
|
|
14
|
+
} | {
|
|
15
|
+
type: "file";
|
|
16
|
+
payload: FilePayload;
|
|
17
|
+
} | {
|
|
18
|
+
type: "tool";
|
|
19
|
+
payload: ToolPayload;
|
|
20
|
+
} | {
|
|
21
|
+
type: "convo_status";
|
|
22
|
+
payload: ConvoStatusPayload;
|
|
23
|
+
} | {
|
|
24
|
+
type: "response_status";
|
|
25
|
+
payload: ResponseStatusPayload;
|
|
26
|
+
});
|
|
27
|
+
export type ConversationState = {
|
|
28
|
+
settings: {
|
|
29
|
+
model?: string;
|
|
30
|
+
title?: string;
|
|
31
|
+
system?: string;
|
|
32
|
+
location?: string;
|
|
33
|
+
locationInstructions?: string;
|
|
34
|
+
temperature?: number;
|
|
35
|
+
disableAutoTitle?: boolean;
|
|
36
|
+
};
|
|
37
|
+
summary?: {
|
|
38
|
+
shortSummary?: string;
|
|
39
|
+
longSummary?: string;
|
|
40
|
+
};
|
|
41
|
+
messages: ConversationMessage[];
|
|
42
|
+
dialogue: DialogueEntry[];
|
|
43
|
+
};
|
|
44
|
+
export interface ConversationV4 {
|
|
45
|
+
version: "4.0";
|
|
46
|
+
conversationId: string;
|
|
47
|
+
ownerUserId: string;
|
|
48
|
+
disableView?: boolean;
|
|
49
|
+
state: ConversationState;
|
|
50
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export type ConversationMessageContentText = {
|
|
2
|
+
type: "input_text";
|
|
3
|
+
text: string;
|
|
4
|
+
};
|
|
5
|
+
export type ConversationMessageContentOutputText = {
|
|
6
|
+
type: "output_text";
|
|
7
|
+
text: string;
|
|
8
|
+
};
|
|
9
|
+
export type ConversationMessageMeta = {
|
|
10
|
+
participantId?: string;
|
|
11
|
+
attributedUserId?: string;
|
|
12
|
+
summarized?: unknown;
|
|
13
|
+
timestamp?: number;
|
|
14
|
+
};
|
|
15
|
+
export type ConversationMessage = {
|
|
16
|
+
type: "user_message";
|
|
17
|
+
role: "user";
|
|
18
|
+
content: string | ConversationMessageContentText[];
|
|
19
|
+
ai4?: ConversationMessageMeta;
|
|
20
|
+
} | {
|
|
21
|
+
type: "system_message";
|
|
22
|
+
role: "system";
|
|
23
|
+
content: string;
|
|
24
|
+
ai4?: ConversationMessageMeta;
|
|
25
|
+
} | {
|
|
26
|
+
type: "assistant_message";
|
|
27
|
+
role: "assistant";
|
|
28
|
+
content: ConversationMessageContentOutputText[];
|
|
29
|
+
id: string;
|
|
30
|
+
ai4?: ConversationMessageMeta;
|
|
31
|
+
} | {
|
|
32
|
+
type: "function_call";
|
|
33
|
+
name: string;
|
|
34
|
+
arguments: string;
|
|
35
|
+
callId: string;
|
|
36
|
+
id?: string;
|
|
37
|
+
ai4?: ConversationMessageMeta;
|
|
38
|
+
} | {
|
|
39
|
+
type: "function_call_output";
|
|
40
|
+
callId: string;
|
|
41
|
+
output: string;
|
|
42
|
+
id?: string;
|
|
43
|
+
ai4?: ConversationMessageMeta;
|
|
44
|
+
} | {
|
|
45
|
+
type: "reasoning";
|
|
46
|
+
content: Array<{
|
|
47
|
+
text: string;
|
|
48
|
+
type?: string;
|
|
49
|
+
}>;
|
|
50
|
+
summary?: Array<{
|
|
51
|
+
text: string;
|
|
52
|
+
type?: string;
|
|
53
|
+
}>;
|
|
54
|
+
id: string;
|
|
55
|
+
ai4?: ConversationMessageMeta;
|
|
56
|
+
} | {
|
|
57
|
+
type: "web_search_call";
|
|
58
|
+
id: string;
|
|
59
|
+
status: string;
|
|
60
|
+
action: Record<string, unknown>;
|
|
61
|
+
ai4?: ConversationMessageMeta;
|
|
62
|
+
} | {
|
|
63
|
+
type: "file_search_call";
|
|
64
|
+
id: string;
|
|
65
|
+
queries: string[];
|
|
66
|
+
status: string;
|
|
67
|
+
ai4?: ConversationMessageMeta;
|
|
68
|
+
} | {
|
|
69
|
+
type: "image_generation_call";
|
|
70
|
+
id: string;
|
|
71
|
+
result?: string;
|
|
72
|
+
status: string;
|
|
73
|
+
image?: {
|
|
74
|
+
b64_json?: string;
|
|
75
|
+
url?: string;
|
|
76
|
+
media_type?: string;
|
|
77
|
+
};
|
|
78
|
+
ai4?: ConversationMessageMeta;
|
|
79
|
+
} | {
|
|
80
|
+
type: "server_tool_item";
|
|
81
|
+
id: string;
|
|
82
|
+
outputType: string;
|
|
83
|
+
output: Record<string, unknown>;
|
|
84
|
+
ai4?: ConversationMessageMeta;
|
|
85
|
+
};
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { ConversationV1 } from "../conversation/v1/conversation_v1";
|
|
2
2
|
import { ConversationV2 } from "../conversation/v2/conversation_v2";
|
|
3
3
|
import { ConversationV3 } from "../conversation/v3/conversation_v3";
|
|
4
|
+
import { ConversationV4 } from "../conversation/v4/conversation_v4";
|
|
4
5
|
export type ConversationStateResponse = {
|
|
5
6
|
success: true;
|
|
6
|
-
conversation: ConversationV1 | ConversationV2 | ConversationV3;
|
|
7
|
+
conversation: ConversationV1 | ConversationV2 | ConversationV3 | ConversationV4;
|
|
7
8
|
} | {
|
|
8
9
|
success: false;
|
|
9
10
|
error: string;
|
|
@@ -17,4 +17,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
17
17
|
__exportStar(require("./response/v3"), exports);
|
|
18
18
|
__exportStar(require("./response/v4"), exports);
|
|
19
19
|
__exportStar(require("./response/v5"), exports);
|
|
20
|
+
__exportStar(require("./state/convo_state_response"), exports);
|
|
21
|
+
__exportStar(require("./conversation/v4/conversation_v4"), exports);
|
|
20
22
|
__exportStar(require("./error"), exports);
|
|
@@ -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 =
|
|
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
|
|
3
|
+
"version": "0.0.18",
|
|
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": "
|
|
15
|
+
"test": "bun test",
|
|
16
|
+
"typecheck": "tsc --noEmit -p tsconfig.check.json"
|
|
16
17
|
},
|
|
17
18
|
"keywords": [
|
|
18
|
-
"
|
|
19
|
+
"agents",
|
|
20
|
+
"ai",
|
|
19
21
|
"alfred",
|
|
20
|
-
"
|
|
22
|
+
"butler",
|
|
23
|
+
"butlerbot",
|
|
21
24
|
"chatbot",
|
|
22
|
-
"
|
|
23
|
-
"
|
|
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/
|
|
33
|
+
"url": "git+https://github.com/butlerbots/alfred5_sdk.git"
|
|
30
34
|
},
|
|
31
35
|
"bugs": {
|
|
32
|
-
"url": "https://github.com/
|
|
36
|
+
"url": "https://github.com/butlerbots/alfred5_sdk/issues"
|
|
33
37
|
},
|
|
34
38
|
"homepage": "https://butlerbot.net",
|
|
35
39
|
"engines": {
|
|
36
|
-
"node": ">=
|
|
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
|
@@ -16,7 +16,7 @@ npm i @butlerbot/sdk
|
|
|
16
16
|
|
|
17
17
|
- ButlerBot API key
|
|
18
18
|
|
|
19
|
-
##
|
|
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
|
+
```
|