@kithjs/client 0.3.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/package.json +32 -0
- package/src/index.ts +190 -0
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kithjs/client",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "TypeScript client for @kithjs/server — connect, speak, listen to voice events.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"types": "./src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.ts"
|
|
10
|
+
},
|
|
11
|
+
"files": ["src", "README.md"],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"typecheck": "tsc --noEmit",
|
|
14
|
+
"test": "bun test"
|
|
15
|
+
},
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/wbaxterh/kith",
|
|
20
|
+
"directory": "packages/client"
|
|
21
|
+
},
|
|
22
|
+
"keywords": ["kith", "voice", "tts", "client", "websocket", "sse"],
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@kithjs/core": "^0.2.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"typescript": "^5.6.0"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @kithjs/client — TypeScript client for @kithjs/server.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* import { KithClient } from "@kithjs/client";
|
|
6
|
+
*
|
|
7
|
+
* const kith = new KithClient({ baseUrl: "http://localhost:3040" });
|
|
8
|
+
* await kith.connect();
|
|
9
|
+
*
|
|
10
|
+
* kith.on((event) => {
|
|
11
|
+
* if (event.type === "tts_audio_chunk") playAudio(event.audioB64);
|
|
12
|
+
* });
|
|
13
|
+
*
|
|
14
|
+
* await kith.speak("Hello from Kith!");
|
|
15
|
+
* await kith.disconnect();
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { KithEvent, EventHandler, Unsubscribe } from "@kithjs/core";
|
|
19
|
+
|
|
20
|
+
export interface KithClientOptions {
|
|
21
|
+
/** Base URL of the Kith server (e.g. "http://localhost:3040") */
|
|
22
|
+
baseUrl: string;
|
|
23
|
+
/** Character ID to use when creating sessions */
|
|
24
|
+
characterId?: string;
|
|
25
|
+
/** Auto-connect on construction. Default: false */
|
|
26
|
+
autoConnect?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class KithClient {
|
|
30
|
+
private baseUrl: string;
|
|
31
|
+
private characterId?: string;
|
|
32
|
+
private _sessionId: string | null = null;
|
|
33
|
+
private ws: WebSocket | null = null;
|
|
34
|
+
private handlers = new Set<EventHandler>();
|
|
35
|
+
private _connected = false;
|
|
36
|
+
|
|
37
|
+
constructor(options: KithClientOptions) {
|
|
38
|
+
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
39
|
+
this.characterId = options.characterId;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get sessionId(): string | null {
|
|
43
|
+
return this._sessionId;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
get connected(): boolean {
|
|
47
|
+
return this._connected;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Create a session and open a WebSocket connection. */
|
|
51
|
+
async connect(): Promise<string> {
|
|
52
|
+
// Create session via HTTP
|
|
53
|
+
const res = await fetch(`${this.baseUrl}/sessions`, {
|
|
54
|
+
method: "POST",
|
|
55
|
+
headers: { "Content-Type": "application/json" },
|
|
56
|
+
body: JSON.stringify({ characterId: this.characterId }),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (!res.ok) {
|
|
60
|
+
const err = await res.json().catch(() => ({ error: "unknown" }));
|
|
61
|
+
throw new Error(`Failed to create session: ${(err as any).error}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const data = (await res.json()) as { sessionId: string; wsUrl: string };
|
|
65
|
+
this._sessionId = data.sessionId;
|
|
66
|
+
|
|
67
|
+
// Build WS URL from base URL
|
|
68
|
+
const wsBase = this.baseUrl.replace(/^http/, "ws");
|
|
69
|
+
const wsUrl = `${wsBase}/ws?sessionId=${this._sessionId}`;
|
|
70
|
+
|
|
71
|
+
// Open WebSocket
|
|
72
|
+
await new Promise<void>((resolve, reject) => {
|
|
73
|
+
const ws = new WebSocket(wsUrl);
|
|
74
|
+
this.ws = ws;
|
|
75
|
+
|
|
76
|
+
ws.onopen = () => {
|
|
77
|
+
this._connected = true;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
ws.onmessage = (e) => {
|
|
81
|
+
let event: KithEvent;
|
|
82
|
+
try {
|
|
83
|
+
event = JSON.parse(typeof e.data === "string" ? e.data : String(e.data));
|
|
84
|
+
} catch {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if ((event as any).type === "_ready") {
|
|
89
|
+
resolve();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
for (const h of this.handlers) {
|
|
94
|
+
try {
|
|
95
|
+
h(event);
|
|
96
|
+
} catch {
|
|
97
|
+
// handler error
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
ws.onclose = () => {
|
|
103
|
+
this._connected = false;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
ws.onerror = (err) => {
|
|
107
|
+
reject(new Error(`WebSocket error: ${err}`));
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Timeout after 10s
|
|
111
|
+
setTimeout(() => reject(new Error("WebSocket connect timeout")), 10000);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
return this._sessionId;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Disconnect and destroy the session. */
|
|
118
|
+
async disconnect(): Promise<void> {
|
|
119
|
+
if (this._sessionId) {
|
|
120
|
+
await fetch(`${this.baseUrl}/sessions/${this._sessionId}`, {
|
|
121
|
+
method: "DELETE",
|
|
122
|
+
}).catch(() => {});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (this.ws) {
|
|
126
|
+
this.ws.onclose = null;
|
|
127
|
+
this.ws.close();
|
|
128
|
+
this.ws = null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
this._connected = false;
|
|
132
|
+
this._sessionId = null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Send text to be spoken. */
|
|
136
|
+
async speak(text: string): Promise<void> {
|
|
137
|
+
if (!this._sessionId) throw new Error("Not connected");
|
|
138
|
+
|
|
139
|
+
// Send via WS for lowest latency
|
|
140
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
141
|
+
this.ws.send(JSON.stringify({ type: "speak", text }));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Fallback to HTTP
|
|
146
|
+
const res = await fetch(`${this.baseUrl}/sessions/${this._sessionId}/speak`, {
|
|
147
|
+
method: "POST",
|
|
148
|
+
headers: { "Content-Type": "application/json" },
|
|
149
|
+
body: JSON.stringify({ text }),
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
if (!res.ok) {
|
|
153
|
+
throw new Error(`speak failed: ${res.status}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Stop any in-flight TTS. */
|
|
158
|
+
async bargeIn(): Promise<void> {
|
|
159
|
+
if (!this._sessionId) throw new Error("Not connected");
|
|
160
|
+
|
|
161
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
162
|
+
this.ws.send(JSON.stringify({ type: "barge-in" }));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
await fetch(`${this.baseUrl}/sessions/${this._sessionId}/barge-in`, {
|
|
167
|
+
method: "POST",
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Subscribe to KithEvents. */
|
|
172
|
+
on(handler: EventHandler): Unsubscribe {
|
|
173
|
+
this.handlers.add(handler);
|
|
174
|
+
return () => {
|
|
175
|
+
this.handlers.delete(handler);
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Get server health status. */
|
|
180
|
+
async health(): Promise<{ ok: boolean; sessions: number; uptime: number }> {
|
|
181
|
+
const res = await fetch(`${this.baseUrl}/health`);
|
|
182
|
+
return res.json() as any;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** List available character profiles. */
|
|
186
|
+
async characters(): Promise<{ id: string; personaMode?: string }[]> {
|
|
187
|
+
const res = await fetch(`${this.baseUrl}/characters`);
|
|
188
|
+
return res.json() as any;
|
|
189
|
+
}
|
|
190
|
+
}
|