@localhostdevs/sdk 0.18.0 → 0.19.1
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/README.md +383 -356
- package/dist/bot.d.ts +27 -0
- package/dist/bot.d.ts.map +1 -1
- package/dist/bot.js +87 -1
- package/dist/bot.js.map +1 -1
- package/dist/collectStore.js +21 -21
- package/dist/sso.d.ts +28 -0
- package/dist/sso.d.ts.map +1 -1
- package/dist/sso.js +58 -3
- package/dist/sso.js.map +1 -1
- package/dist/transport.d.ts +16 -0
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +81 -9
- package/dist/transport.js.map +1 -1
- package/dist/types.d.ts +11 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +64 -64
- package/dist/poll.d.ts +0 -31
- package/dist/poll.d.ts.map +0 -1
- package/dist/poll.js +0 -48
- package/dist/poll.js.map +0 -1
- package/dist/pollStore.d.ts +0 -26
- package/dist/pollStore.d.ts.map +0 -1
- package/dist/pollStore.js +0 -79
- package/dist/pollStore.js.map +0 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
package/README.md
CHANGED
|
@@ -1,356 +1,383 @@
|
|
|
1
|
-
# @localhostdevs/sdk
|
|
2
|
-
|
|
3
|
-
The JavaScript SDK for building bots on **[localhostdevs](https://localhostdevs.com)** — run your app on your own laptop and publish it as a command-driven bot that anyone can use through a chat-style interface.
|
|
4
|
-
|
|
5
|
-
> Pre-1.0: API surface may break in minor versions (`0.x → 0.y`). Once the platform launches with external users, we'll cut a 1.0 that promises stability.
|
|
6
|
-
|
|
7
|
-
## Install
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
npm install @localhostdevs/sdk
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
## Quickstart
|
|
14
|
-
|
|
15
|
-
After creating a bot in the [dashboard](https://localhostdevs.com/dashboard/bots) and copying your API key, write this to `app.js`:
|
|
16
|
-
|
|
17
|
-
```js
|
|
18
|
-
import { Bot } from '@localhostdevs/sdk';
|
|
19
|
-
|
|
20
|
-
const bot = new Bot({
|
|
21
|
-
id: 'price-bot', // your bot's handle (matches the @handle on the marketplace)
|
|
22
|
-
// apiKey: process.env.BOT_SECRET, // optional; auto-read from env if unset
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
bot.cmd({ name: 'ping' }, async (ctx) => {
|
|
26
|
-
await ctx.reply({ text: 'pong' });
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
bot.cmd({ name: 'echo' }, async (ctx) => {
|
|
30
|
-
await ctx.reply({ text: `echo: ${JSON.stringify(ctx.args)}` });
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
await bot.connect();
|
|
34
|
-
console.log('bot online — waiting for commands');
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
Then:
|
|
38
|
-
|
|
39
|
-
```bash
|
|
40
|
-
BOT_SECRET=<your Bot Secret from the dashboard> node app.js
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
The bot stays online for as long as the process runs. The gateway keeps the connection healthy via heartbeats; consumers see your bot as **online** while it's connected.
|
|
44
|
-
|
|
45
|
-
## API
|
|
46
|
-
|
|
47
|
-
### `new Bot(opts)`
|
|
48
|
-
|
|
49
|
-
| Option | Type | Default | Notes |
|
|
50
|
-
| -------------------- | ------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
|
|
51
|
-
| `id` | `string` (required) | — | Your bot's handle. Lowercase letters / digits / dashes. |
|
|
52
|
-
| `apiKey` | `string` (required) | `process.env.BOT_SECRET` | Per-bot **Bot Secret** from the dashboard. The gateway validates it and scopes you to your own bot only. The constructor option name stays `apiKey` for backward compatibility with older SDK versions; the dashboard label and env var moved to "Bot Secret" / `BOT_SECRET` to disambiguate from user-level API keys (`lhduser_…`) used against the public REST API. |
|
|
53
|
-
| `serverUrl` | `string` | `wss://bot-api.localhostdevs.com/bot` (override with `LHD_SERVER_URL`) | Production gateway is baked in — most users leave this alone. Only override for local dev or self-hosted gateways. |
|
|
54
|
-
| `idempotencyLruSize` | `number` | `1024` | How many recent message IDs to remember for deduplication. |
|
|
55
|
-
|
|
56
|
-
### `bot.cmd(def, handler)`
|
|
57
|
-
|
|
58
|
-
Register a handler for a command. Must be called before `connect()`. The first argument is a **command definition object** with the following fields:
|
|
59
|
-
|
|
60
|
-
| Field | Type | Notes |
|
|
61
|
-
| ------------- | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
62
|
-
| `name` | `string` (required) | Command name consumers dispatch (e.g. `'ping'`, `'topic'`). |
|
|
63
|
-
| `description` | `string` | Human-readable description shown on the marketplace/workflow picker. |
|
|
64
|
-
| `args` | `Record<string, ArgSpec>` | Declare the expected arguments (see below). **Advertise-only** by default — fills the marketplace and workflow picker but does NOT validate at runtime unless `validate: true`. |
|
|
65
|
-
| `returns` | `{ kind: ReplyKind, data?: Record<string, 'string' \| 'number' \| 'boolean'> }` | Declare the reply kind. Enforced: the handler must call the matching reply method or the SDK throws (see Reply-kind enforcement below). |
|
|
66
|
-
| `validate` | `boolean` | When `true`, the SDK validates `ctx.args` against the declared `args` before running your handler. A bad dispatch is rejected with a structured `invalid_args` error. Also narrows TypeScript types: `required` fields become typed-present in `ctx.args`. Without `validate`, all arg fields are typed optional. |
|
|
67
|
-
| `streaming` | `boolean` | Override the bot-level streaming flag for this command only. |
|
|
68
|
-
| `mimeType` | `string` | MIME-type hint advertised in IDENTIFY (advertising-only, doesn't affect runtime). |
|
|
69
|
-
|
|
70
|
-
#### `ArgSpec` shape
|
|
71
|
-
|
|
72
|
-
```ts
|
|
73
|
-
interface ArgSpec {
|
|
74
|
-
type: 'string' | 'number' | 'boolean' | 'enum';
|
|
75
|
-
required?: boolean;
|
|
76
|
-
description?: string;
|
|
77
|
-
values?: readonly string[]; // required when type === 'enum'
|
|
78
|
-
}
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
#### `returns.kind` vocabulary
|
|
82
|
-
|
|
83
|
-
`'text'` | `'list'` | `'table'` | `'image'` | `'buttons'` | `'collect'`
|
|
84
|
-
|
|
85
|
-
The declared kind is **enforced at runtime**: if the handler calls a reply method that doesn't match the declaration (e.g. `returns: { kind: 'list' }` but the handler calls `ctx.reply()`), the SDK throws a hard error. TypeScript also narrows `ctx`'s available reply methods to just the ones valid for the declared kind.
|
|
86
|
-
|
|
87
|
-
#### Example
|
|
88
|
-
|
|
89
|
-
```ts
|
|
90
|
-
bot.cmd({
|
|
91
|
-
name: 'topic',
|
|
92
|
-
description: 'Scrape trends for a query, then pick one',
|
|
93
|
-
args: {
|
|
94
|
-
query: { type: 'string', required: true, description: 'Search query' },
|
|
95
|
-
format: { type: 'string', description: 'e.g. short_30s' },
|
|
96
|
-
voice: { type: 'enum', values: ['on', 'off'] },
|
|
97
|
-
},
|
|
98
|
-
returns: { kind: 'list', data: { jobId: 'string', state: 'string' } },
|
|
99
|
-
validate: false, // opt-in: when true, args are validated AND required fields typed-present
|
|
100
|
-
}, async (ctx) => {
|
|
101
|
-
// ctx.args is typed from `args`; ctx reply methods are narrowed to returns.kind
|
|
102
|
-
await ctx.replyList([{ title: ctx.args.query ?? '' }]);
|
|
103
|
-
});
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
Handler receives a `CommandContext`:
|
|
107
|
-
|
|
108
|
-
```ts
|
|
109
|
-
bot.cmd({ name: 'greet' }, async (ctx) => {
|
|
110
|
-
// ctx.args: parsed args from the consumer
|
|
111
|
-
// ctx.command: 'greet'
|
|
112
|
-
// ctx.msgId: unique id for this invocation
|
|
113
|
-
|
|
114
|
-
await ctx.reply({ text: `Hello, ${ctx.args.name ?? 'world'}!` });
|
|
115
|
-
|
|
116
|
-
// ctx.reply also supports:
|
|
117
|
-
// data: any — structured payload alongside text
|
|
118
|
-
// dispatch: { to, command, args } — chain to another bot (max depth 5)
|
|
119
|
-
});
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
### `
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
v0.
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
1
|
+
# @localhostdevs/sdk
|
|
2
|
+
|
|
3
|
+
The JavaScript SDK for building bots on **[localhostdevs](https://localhostdevs.com)** — run your app on your own laptop and publish it as a command-driven bot that anyone can use through a chat-style interface.
|
|
4
|
+
|
|
5
|
+
> Pre-1.0: API surface may break in minor versions (`0.x → 0.y`). Once the platform launches with external users, we'll cut a 1.0 that promises stability.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @localhostdevs/sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quickstart
|
|
14
|
+
|
|
15
|
+
After creating a bot in the [dashboard](https://localhostdevs.com/dashboard/bots) and copying your API key, write this to `app.js`:
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
import { Bot } from '@localhostdevs/sdk';
|
|
19
|
+
|
|
20
|
+
const bot = new Bot({
|
|
21
|
+
id: 'price-bot', // your bot's handle (matches the @handle on the marketplace)
|
|
22
|
+
// apiKey: process.env.BOT_SECRET, // optional; auto-read from env if unset
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
bot.cmd({ name: 'ping' }, async (ctx) => {
|
|
26
|
+
await ctx.reply({ text: 'pong' });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
bot.cmd({ name: 'echo' }, async (ctx) => {
|
|
30
|
+
await ctx.reply({ text: `echo: ${JSON.stringify(ctx.args)}` });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
await bot.connect();
|
|
34
|
+
console.log('bot online — waiting for commands');
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Then:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
BOT_SECRET=<your Bot Secret from the dashboard> node app.js
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The bot stays online for as long as the process runs. The gateway keeps the connection healthy via heartbeats; consumers see your bot as **online** while it's connected.
|
|
44
|
+
|
|
45
|
+
## API
|
|
46
|
+
|
|
47
|
+
### `new Bot(opts)`
|
|
48
|
+
|
|
49
|
+
| Option | Type | Default | Notes |
|
|
50
|
+
| -------------------- | ------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
|
|
51
|
+
| `id` | `string` (required) | — | Your bot's handle. Lowercase letters / digits / dashes. |
|
|
52
|
+
| `apiKey` | `string` (required) | `process.env.BOT_SECRET` | Per-bot **Bot Secret** from the dashboard. The gateway validates it and scopes you to your own bot only. The constructor option name stays `apiKey` for backward compatibility with older SDK versions; the dashboard label and env var moved to "Bot Secret" / `BOT_SECRET` to disambiguate from user-level API keys (`lhduser_…`) used against the public REST API. |
|
|
53
|
+
| `serverUrl` | `string` | `wss://bot-api.localhostdevs.com/bot` (override with `LHD_SERVER_URL`) | Production gateway is baked in — most users leave this alone. Only override for local dev or self-hosted gateways. |
|
|
54
|
+
| `idempotencyLruSize` | `number` | `1024` | How many recent message IDs to remember for deduplication. |
|
|
55
|
+
|
|
56
|
+
### `bot.cmd(def, handler)`
|
|
57
|
+
|
|
58
|
+
Register a handler for a command. Must be called before `connect()`. The first argument is a **command definition object** with the following fields:
|
|
59
|
+
|
|
60
|
+
| Field | Type | Notes |
|
|
61
|
+
| ------------- | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
62
|
+
| `name` | `string` (required) | Command name consumers dispatch (e.g. `'ping'`, `'topic'`). |
|
|
63
|
+
| `description` | `string` | Human-readable description shown on the marketplace/workflow picker. |
|
|
64
|
+
| `args` | `Record<string, ArgSpec>` | Declare the expected arguments (see below). **Advertise-only** by default — fills the marketplace and workflow picker but does NOT validate at runtime unless `validate: true`. |
|
|
65
|
+
| `returns` | `{ kind: ReplyKind, data?: Record<string, 'string' \| 'number' \| 'boolean'> }` | Declare the reply kind. Enforced: the handler must call the matching reply method or the SDK throws (see Reply-kind enforcement below). |
|
|
66
|
+
| `validate` | `boolean` | When `true`, the SDK validates `ctx.args` against the declared `args` before running your handler. A bad dispatch is rejected with a structured `invalid_args` error. Also narrows TypeScript types: `required` fields become typed-present in `ctx.args`. Without `validate`, all arg fields are typed optional. |
|
|
67
|
+
| `streaming` | `boolean` | Override the bot-level streaming flag for this command only. |
|
|
68
|
+
| `mimeType` | `string` | MIME-type hint advertised in IDENTIFY (advertising-only, doesn't affect runtime). |
|
|
69
|
+
|
|
70
|
+
#### `ArgSpec` shape
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
interface ArgSpec {
|
|
74
|
+
type: 'string' | 'number' | 'boolean' | 'enum';
|
|
75
|
+
required?: boolean;
|
|
76
|
+
description?: string;
|
|
77
|
+
values?: readonly string[]; // required when type === 'enum'
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
#### `returns.kind` vocabulary
|
|
82
|
+
|
|
83
|
+
`'text'` | `'list'` | `'table'` | `'image'` | `'buttons'` | `'collect'`
|
|
84
|
+
|
|
85
|
+
The declared kind is **enforced at runtime**: if the handler calls a reply method that doesn't match the declaration (e.g. `returns: { kind: 'list' }` but the handler calls `ctx.reply()`), the SDK throws a hard error. TypeScript also narrows `ctx`'s available reply methods to just the ones valid for the declared kind.
|
|
86
|
+
|
|
87
|
+
#### Example
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
bot.cmd({
|
|
91
|
+
name: 'topic',
|
|
92
|
+
description: 'Scrape trends for a query, then pick one',
|
|
93
|
+
args: {
|
|
94
|
+
query: { type: 'string', required: true, description: 'Search query' },
|
|
95
|
+
format: { type: 'string', description: 'e.g. short_30s' },
|
|
96
|
+
voice: { type: 'enum', values: ['on', 'off'] },
|
|
97
|
+
},
|
|
98
|
+
returns: { kind: 'list', data: { jobId: 'string', state: 'string' } },
|
|
99
|
+
validate: false, // opt-in: when true, args are validated AND required fields typed-present
|
|
100
|
+
}, async (ctx) => {
|
|
101
|
+
// ctx.args is typed from `args`; ctx reply methods are narrowed to returns.kind
|
|
102
|
+
await ctx.replyList([{ title: ctx.args.query ?? '' }]);
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Handler receives a `CommandContext`:
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
bot.cmd({ name: 'greet' }, async (ctx) => {
|
|
110
|
+
// ctx.args: parsed args from the consumer
|
|
111
|
+
// ctx.command: 'greet'
|
|
112
|
+
// ctx.msgId: unique id for this invocation
|
|
113
|
+
|
|
114
|
+
await ctx.reply({ text: `Hello, ${ctx.args.name ?? 'world'}!` });
|
|
115
|
+
|
|
116
|
+
// ctx.reply also supports:
|
|
117
|
+
// data: any — structured payload alongside text
|
|
118
|
+
// dispatch: { to, command, args } — chain to another bot (max depth 5)
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### `bot.register(...descriptors)`
|
|
123
|
+
|
|
124
|
+
Register one or more commands from **descriptor objects** — the composition-root
|
|
125
|
+
API. A descriptor is `{ command, handler }`, where `command` is the same object
|
|
126
|
+
`cmd()` takes and `handler` is the same handler. This lets a command live in its
|
|
127
|
+
own file and `app.js` just wire it up:
|
|
128
|
+
|
|
129
|
+
```js
|
|
130
|
+
// commands/ping.js
|
|
131
|
+
export default {
|
|
132
|
+
command: { name: 'ping', description: 'Replies with pong', returns: { kind: 'text' } },
|
|
133
|
+
handler: async (ctx) => { await ctx.reply({ text: 'pong' }) },
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// app.js — the composition root
|
|
137
|
+
import ping from './commands/ping.js'
|
|
138
|
+
import echo from './commands/echo.js'
|
|
139
|
+
|
|
140
|
+
const bot = new Bot({ id: 'my-bot' })
|
|
141
|
+
bot.register(ping).register(echo) // chainable; or bot.register(ping, echo)
|
|
142
|
+
await bot.connect()
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`register` is chainable (returns the bot) and variadic. It forwards to `cmd()`,
|
|
146
|
+
so all the same rules apply (must be called before `connect()`; duplicate names
|
|
147
|
+
throw). `cmd()` remains fully supported for inline commands.
|
|
148
|
+
|
|
149
|
+
### `await bot.connect()`
|
|
150
|
+
|
|
151
|
+
Open the WebSocket to the gateway, authenticate, and start receiving commands. Resolves after the gateway sends `HELLO` (the SDK is ready for traffic).
|
|
152
|
+
|
|
153
|
+
### `await bot.disconnect()`
|
|
154
|
+
|
|
155
|
+
Close the WebSocket cleanly.
|
|
156
|
+
|
|
157
|
+
## Idempotency
|
|
158
|
+
|
|
159
|
+
Every command from the gateway carries a `msgId`. The SDK deduplicates redelivered messages via a fixed-size LRU keyed by `msgId`. If the gateway happens to deliver the same command twice, your handler runs once.
|
|
160
|
+
|
|
161
|
+
## Online / offline status
|
|
162
|
+
|
|
163
|
+
The gateway pings the bot every 30 seconds and the SDK responds. If the gateway stops hearing from you for >60 seconds it marks the bot offline. Reconnect simply by starting the process again.
|
|
164
|
+
|
|
165
|
+
## Bot-to-bot composition
|
|
166
|
+
|
|
167
|
+
A reply can carry a `dispatch` directive to chain a command to another bot:
|
|
168
|
+
|
|
169
|
+
```js
|
|
170
|
+
bot.cmd({ name: 'summary' }, async (ctx) => {
|
|
171
|
+
await ctx.reply({
|
|
172
|
+
text: 'fetching price first…',
|
|
173
|
+
dispatch: { to: 'price-bot', command: 'price', args: { ticker: 'AAPL' } },
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Chains run up to depth 5 with loop detection. The original consumer pays for every step.
|
|
179
|
+
|
|
180
|
+
## Structured replies (0.5.0+)
|
|
181
|
+
|
|
182
|
+
By default `ctx.reply({ text })` sends a plain-text body — the consumer
|
|
183
|
+
renders it as text. To get richer rendering (source-badge lists, striped
|
|
184
|
+
tables) on the dashboard Test Console and the public Try widget, return a
|
|
185
|
+
**kinded** body via the helpers:
|
|
186
|
+
|
|
187
|
+
```js
|
|
188
|
+
// List output
|
|
189
|
+
bot.cmd({
|
|
190
|
+
name: 'headlines',
|
|
191
|
+
returns: { kind: 'list' },
|
|
192
|
+
}, async (ctx) => {
|
|
193
|
+
await ctx.replyList(
|
|
194
|
+
[
|
|
195
|
+
{ label: 'AP', title: 'Story one', secondary: '2m', href: '…' },
|
|
196
|
+
{ label: 'Reuters', title: 'Story two', secondary: '5m' },
|
|
197
|
+
],
|
|
198
|
+
{ header: '2 headlines · global' }, // optional
|
|
199
|
+
);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Table output
|
|
203
|
+
bot.cmd({
|
|
204
|
+
name: 'top5',
|
|
205
|
+
returns: { kind: 'table' },
|
|
206
|
+
}, async (ctx) => {
|
|
207
|
+
await ctx.replyTable({
|
|
208
|
+
headers: ['ticker', 'price', 'change'],
|
|
209
|
+
rows: [
|
|
210
|
+
['AAPL', 188.12, 1.4],
|
|
211
|
+
['MSFT', 412.50, null],
|
|
212
|
+
],
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**Body shapes on the wire** (the gateway also wraps legacy `{ text }`
|
|
218
|
+
replies in `{ kind: 'text', text }` so consumers always see a
|
|
219
|
+
discriminated union):
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
type BotResponse =
|
|
223
|
+
| { kind: 'text'; text: string; mimeType?: string; data?: unknown }
|
|
224
|
+
| { kind: 'list'; header?: string; items: ListItem[] }
|
|
225
|
+
| { kind: 'table'; headers: string[]; rows: Array<Array<string | number | null>> };
|
|
226
|
+
|
|
227
|
+
interface ListItem {
|
|
228
|
+
label?: string; // small badge on the left, e.g. "AP"
|
|
229
|
+
title: string; // primary line
|
|
230
|
+
secondary?: string; // trailing text, e.g. "2m"
|
|
231
|
+
href?: string; // optional click-through
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
**Validation.** `replyList()` rejects empty `items` and items missing a
|
|
236
|
+
`title`. `replyTable()` rejects empty `headers` and rows whose cell count
|
|
237
|
+
doesn't match `headers.length`. Validation runs BEFORE the once-only reply
|
|
238
|
+
guard flips, so if you catch the error you can still send a corrected
|
|
239
|
+
`ctx.reply()`. After a successful reply (via any of the three methods),
|
|
240
|
+
further reply calls throw — the SDK guarantees exactly-one reply per
|
|
241
|
+
command.
|
|
242
|
+
|
|
243
|
+
**Reply-kind enforcement.** When `returns.kind` is declared on the command
|
|
244
|
+
definition, the SDK enforces it at runtime: calling a mismatched reply
|
|
245
|
+
method (e.g. `ctx.reply()` when `returns: { kind: 'list' }`) throws a hard
|
|
246
|
+
error. TypeScript also narrows `ctx`'s available reply methods to the ones
|
|
247
|
+
valid for the declared kind, so mismatches are caught at compile time.
|
|
248
|
+
|
|
249
|
+
## Streaming progress (`ctx.update`)
|
|
250
|
+
|
|
251
|
+
Long-running commands can emit intermediate progress frames before the
|
|
252
|
+
final reply. Enable per-bot or per-command:
|
|
253
|
+
|
|
254
|
+
```js
|
|
255
|
+
const bot = new Bot({
|
|
256
|
+
id: 'analyze-bot',
|
|
257
|
+
streaming: true, // enables ctx.update across all commands
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
bot.cmd({ name: 'analyze' }, async (ctx) => {
|
|
261
|
+
await ctx.update({ progress: 10, text: 'Fetching…' });
|
|
262
|
+
await ctx.update({ progress: 60, text: 'Crunching…' });
|
|
263
|
+
await ctx.reply({
|
|
264
|
+
text: 'done',
|
|
265
|
+
mimeType: 'image/png',
|
|
266
|
+
data: chartBytesBase64,
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Or per-command using `streaming` in the definition:
|
|
272
|
+
|
|
273
|
+
```js
|
|
274
|
+
bot.cmd({ name: 'analyze', streaming: true }, async (ctx) => { /* … */ });
|
|
275
|
+
bot.cmd({ name: 'ping' }, async (ctx) => { /* no ctx.update here */ });
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
**Progress shapes** (use whichever fits the work):
|
|
279
|
+
|
|
280
|
+
- `number` — percent (0–100). Renders as a horizontal bar.
|
|
281
|
+
- `{ current, total }` — step counter. Renders as "5 / 30".
|
|
282
|
+
- `{ label: string }` — indeterminate. Renders as a spinner + label.
|
|
283
|
+
|
|
284
|
+
**MIME-type hints** (`mimeType` on both update and reply bodies) let the
|
|
285
|
+
consumer choose how to render the body. v1 chat UI handles
|
|
286
|
+
`text/plain` (default) and `text/markdown`; others fall back to a code
|
|
287
|
+
block of the data field.
|
|
288
|
+
|
|
289
|
+
**Consumer contract**: when the public REST API caller hits
|
|
290
|
+
`POST /api/v1/bots/{handle}/dispatch` with
|
|
291
|
+
`Accept: text/event-stream` or `Accept: application/x-ndjson`, they
|
|
292
|
+
receive each frame as it arrives. Default `Accept: application/json`
|
|
293
|
+
(or no Accept) returns only the final frame as a single JSON — backward
|
|
294
|
+
compatible with everything written before this release. See
|
|
295
|
+
`/docs/api` on localhostdevs.com for full details.
|
|
296
|
+
|
|
297
|
+
### Declaring mimeType per command (0.4.1+)
|
|
298
|
+
|
|
299
|
+
Tell the platform what each command's reply looks like so the bot
|
|
300
|
+
detail page can show consumers what to expect:
|
|
301
|
+
|
|
302
|
+
```js
|
|
303
|
+
bot.cmd({ name: 'chart', streaming: true, mimeType: 'image/png' }, async (ctx) => {
|
|
304
|
+
await ctx.reply({ data: chartBytes, mimeType: 'image/png' });
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
bot.cmd({ name: 'summarize', mimeType: 'text/markdown' }, async (ctx) => {
|
|
308
|
+
await ctx.reply({ text: '# Summary\n…', mimeType: 'text/markdown' });
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
bot.cmd({ name: 'ping' }, async (ctx) => { // no mimeType — displays as text/plain
|
|
312
|
+
await ctx.reply({ text: 'pong' });
|
|
313
|
+
});
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
The declaration is **advertising-only** — the actual mimeType on the
|
|
317
|
+
wire is whatever `ctx.reply({ mimeType })` passes. Mismatches don't
|
|
318
|
+
crash anything; the bot detail page shows the declared value and the
|
|
319
|
+
consumer renders whatever the runtime reply specifies.
|
|
320
|
+
|
|
321
|
+
## v0.3.5: Bot Secret env-var rename
|
|
322
|
+
|
|
323
|
+
The env var name moved from `LHD_API_KEY` to `BOT_SECRET` to disambiguate from
|
|
324
|
+
user-level public-API keys (`lhduser_…`). Rename the env var in your bot's
|
|
325
|
+
deployment — no code change needed if you used the env-var path:
|
|
326
|
+
|
|
327
|
+
```diff
|
|
328
|
+
- LHD_API_KEY=lhd_live_… node app.js
|
|
329
|
+
+ BOT_SECRET=lhd_live_… node app.js
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
If you pass `apiKey` explicitly into the constructor, no change at all —
|
|
333
|
+
the option name on `BotOptions` is preserved.
|
|
334
|
+
|
|
335
|
+
## Migrating from v0.2 → v0.3 (gateway pivot)
|
|
336
|
+
|
|
337
|
+
v0.3 is a breaking change vs v0.2 — the SDK no longer talks to NATS directly. It opens a WebSocket to the managed gateway and auths with a per-bot Bot Secret:
|
|
338
|
+
|
|
339
|
+
| Was (v0.2) | Now (v0.3+) |
|
|
340
|
+
| -------------------------------------- | ------------------------------ |
|
|
341
|
+
| `new Bot({ id, natsUrl: 'nats://…' })` | `new Bot({ id, apiKey: '…' })` |
|
|
342
|
+
| `LHD_NATS_TOKEN=… node app.js` | `BOT_SECRET=… node app.js` |
|
|
343
|
+
|
|
344
|
+
Drop the `natsUrl` and `natsToken` options entirely — the SDK figures out the right gateway URL on its own. Use your bot's **Bot Secret** from the dashboard (not the old shared broker token).
|
|
345
|
+
|
|
346
|
+
## v0.3.x → v0.4 (streaming opt-in)
|
|
347
|
+
|
|
348
|
+
v0.4 is additive — nothing breaks for bots calling only `ctx.reply()`.
|
|
349
|
+
New surface:
|
|
350
|
+
|
|
351
|
+
- `new Bot({ …, streaming: true })` to enable `ctx.update()`
|
|
352
|
+
- `streaming: true` inside the command definition to flip the flag per command
|
|
353
|
+
- `ctx.update({ progress?, text?, data?, mimeType? })` — send 1+
|
|
354
|
+
intermediate frames before `ctx.reply()`
|
|
355
|
+
- `mimeType` on `ctx.reply()` for the existing final-reply path
|
|
356
|
+
|
|
357
|
+
No env-var changes, no protocol breakage, no upstream re-auth.
|
|
358
|
+
|
|
359
|
+
## v0.4.0 → v0.4.1 (per-command mimeType)
|
|
360
|
+
|
|
361
|
+
Additive — no breaking changes. The `mimeType?` field in the command definition
|
|
362
|
+
is optional; bots that ignore it work exactly the same. The SDK auto-builds an
|
|
363
|
+
IDENTIFY-time `commands` array from your registered handlers so the platform can
|
|
364
|
+
display them on the bot detail page.
|
|
365
|
+
|
|
366
|
+
## v0.14 → v0.15 (object `cmd()`)
|
|
367
|
+
|
|
368
|
+
**Breaking.** See [MIGRATION.md](./MIGRATION.md) for the full guide.
|
|
369
|
+
|
|
370
|
+
`cmd()` now takes a **command definition object** as its first argument. The
|
|
371
|
+
string-form is removed.
|
|
372
|
+
|
|
373
|
+
```diff
|
|
374
|
+
- bot.cmd("ping", async (ctx) => { … })
|
|
375
|
+
+ bot.cmd({ name: "ping" }, async (ctx) => { … })
|
|
376
|
+
|
|
377
|
+
- bot.cmd("analyze", { streaming: true }, async (ctx) => { … })
|
|
378
|
+
+ bot.cmd({ name: "analyze", streaming: true }, async (ctx) => { … })
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
## License
|
|
382
|
+
|
|
383
|
+
MIT
|