@localhostdevs/sdk 0.14.0 → 0.16.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/README.md +97 -29
- package/dist/argSchema.d.ts +7 -0
- package/dist/argSchema.d.ts.map +1 -0
- package/dist/argSchema.js +63 -0
- package/dist/argSchema.js.map +1 -0
- package/dist/bot.d.ts +5 -5
- package/dist/bot.d.ts.map +1 -1
- package/dist/bot.js +112 -16
- package/dist/bot.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/jsonSchema.d.ts +5 -0
- package/dist/jsonSchema.d.ts.map +1 -0
- package/dist/jsonSchema.js +7 -0
- package/dist/jsonSchema.js.map +1 -0
- package/dist/transport.d.ts +4 -0
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types.d.ts +104 -33
- 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 -61
package/README.md
CHANGED
|
@@ -22,11 +22,11 @@ const bot = new Bot({
|
|
|
22
22
|
// apiKey: process.env.BOT_SECRET, // optional; auto-read from env if unset
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
-
bot.cmd('ping', async (ctx) => {
|
|
25
|
+
bot.cmd({ name: 'ping' }, async (ctx) => {
|
|
26
26
|
await ctx.reply({ text: 'pong' });
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
-
bot.cmd('echo', async (ctx) => {
|
|
29
|
+
bot.cmd({ name: 'echo' }, async (ctx) => {
|
|
30
30
|
await ctx.reply({ text: `echo: ${JSON.stringify(ctx.args)}` });
|
|
31
31
|
});
|
|
32
32
|
|
|
@@ -53,15 +53,63 @@ The bot stays online for as long as the process runs. The gateway keeps the conn
|
|
|
53
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
54
|
| `idempotencyLruSize` | `number` | `1024` | How many recent message IDs to remember for deduplication. |
|
|
55
55
|
|
|
56
|
-
### `bot.cmd(
|
|
56
|
+
### `bot.cmd(def, handler)`
|
|
57
57
|
|
|
58
|
-
Register a handler for a command. Must be called before `connect()`.
|
|
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`:
|
|
59
107
|
|
|
60
108
|
```ts
|
|
61
|
-
bot.cmd('greet', async (ctx) => {
|
|
62
|
-
// ctx.args:
|
|
109
|
+
bot.cmd({ name: 'greet' }, async (ctx) => {
|
|
110
|
+
// ctx.args: parsed args from the consumer
|
|
63
111
|
// ctx.command: 'greet'
|
|
64
|
-
// ctx.msgId:
|
|
112
|
+
// ctx.msgId: unique id for this invocation
|
|
65
113
|
|
|
66
114
|
await ctx.reply({ text: `Hello, ${ctx.args.name ?? 'world'}!` });
|
|
67
115
|
|
|
@@ -92,7 +140,7 @@ The gateway pings the bot every 30 seconds and the SDK responds. If the gateway
|
|
|
92
140
|
A reply can carry a `dispatch` directive to chain a command to another bot:
|
|
93
141
|
|
|
94
142
|
```js
|
|
95
|
-
bot.cmd('summary', async (ctx) => {
|
|
143
|
+
bot.cmd({ name: 'summary' }, async (ctx) => {
|
|
96
144
|
await ctx.reply({
|
|
97
145
|
text: 'fetching price first…',
|
|
98
146
|
dispatch: { to: 'price-bot', command: 'price', args: { ticker: 'AAPL' } },
|
|
@@ -107,11 +155,14 @@ Chains run up to depth 5 with loop detection. The original consumer pays for eve
|
|
|
107
155
|
By default `ctx.reply({ text })` sends a plain-text body — the consumer
|
|
108
156
|
renders it as text. To get richer rendering (source-badge lists, striped
|
|
109
157
|
tables) on the dashboard Test Console and the public Try widget, return a
|
|
110
|
-
**kinded** body via the
|
|
158
|
+
**kinded** body via the helpers:
|
|
111
159
|
|
|
112
160
|
```js
|
|
113
161
|
// List output
|
|
114
|
-
bot.cmd(
|
|
162
|
+
bot.cmd({
|
|
163
|
+
name: 'headlines',
|
|
164
|
+
returns: { kind: 'list' },
|
|
165
|
+
}, async (ctx) => {
|
|
115
166
|
await ctx.replyList(
|
|
116
167
|
[
|
|
117
168
|
{ label: 'AP', title: 'Story one', secondary: '2m', href: '…' },
|
|
@@ -122,7 +173,10 @@ bot.cmd('headlines', async (ctx) => {
|
|
|
122
173
|
});
|
|
123
174
|
|
|
124
175
|
// Table output
|
|
125
|
-
bot.cmd(
|
|
176
|
+
bot.cmd({
|
|
177
|
+
name: 'top5',
|
|
178
|
+
returns: { kind: 'table' },
|
|
179
|
+
}, async (ctx) => {
|
|
126
180
|
await ctx.replyTable({
|
|
127
181
|
headers: ['ticker', 'price', 'change'],
|
|
128
182
|
rows: [
|
|
@@ -159,10 +213,11 @@ guard flips, so if you catch the error you can still send a corrected
|
|
|
159
213
|
further reply calls throw — the SDK guarantees exactly-one reply per
|
|
160
214
|
command.
|
|
161
215
|
|
|
162
|
-
**
|
|
163
|
-
|
|
164
|
-
(`
|
|
165
|
-
|
|
216
|
+
**Reply-kind enforcement.** When `returns.kind` is declared on the command
|
|
217
|
+
definition, the SDK enforces it at runtime: calling a mismatched reply
|
|
218
|
+
method (e.g. `ctx.reply()` when `returns: { kind: 'list' }`) throws a hard
|
|
219
|
+
error. TypeScript also narrows `ctx`'s available reply methods to the ones
|
|
220
|
+
valid for the declared kind, so mismatches are caught at compile time.
|
|
166
221
|
|
|
167
222
|
## Streaming progress (`ctx.update`)
|
|
168
223
|
|
|
@@ -175,7 +230,7 @@ const bot = new Bot({
|
|
|
175
230
|
streaming: true, // enables ctx.update across all commands
|
|
176
231
|
});
|
|
177
232
|
|
|
178
|
-
bot.cmd('analyze', async (ctx) => {
|
|
233
|
+
bot.cmd({ name: 'analyze' }, async (ctx) => {
|
|
179
234
|
await ctx.update({ progress: 10, text: 'Fetching…' });
|
|
180
235
|
await ctx.update({ progress: 60, text: 'Crunching…' });
|
|
181
236
|
await ctx.reply({
|
|
@@ -186,11 +241,11 @@ bot.cmd('analyze', async (ctx) => {
|
|
|
186
241
|
});
|
|
187
242
|
```
|
|
188
243
|
|
|
189
|
-
Or per-command:
|
|
244
|
+
Or per-command using `streaming` in the definition:
|
|
190
245
|
|
|
191
246
|
```js
|
|
192
|
-
bot.cmd('analyze',
|
|
193
|
-
bot.cmd('ping', async (ctx) => { /* no ctx.update here */ });
|
|
247
|
+
bot.cmd({ name: 'analyze', streaming: true }, async (ctx) => { /* … */ });
|
|
248
|
+
bot.cmd({ name: 'ping' }, async (ctx) => { /* no ctx.update here */ });
|
|
194
249
|
```
|
|
195
250
|
|
|
196
251
|
**Progress shapes** (use whichever fits the work):
|
|
@@ -218,15 +273,15 @@ Tell the platform what each command's reply looks like so the bot
|
|
|
218
273
|
detail page can show consumers what to expect:
|
|
219
274
|
|
|
220
275
|
```js
|
|
221
|
-
bot.cmd('chart',
|
|
276
|
+
bot.cmd({ name: 'chart', streaming: true, mimeType: 'image/png' }, async (ctx) => {
|
|
222
277
|
await ctx.reply({ data: chartBytes, mimeType: 'image/png' });
|
|
223
278
|
});
|
|
224
279
|
|
|
225
|
-
bot.cmd('summarize',
|
|
280
|
+
bot.cmd({ name: 'summarize', mimeType: 'text/markdown' }, async (ctx) => {
|
|
226
281
|
await ctx.reply({ text: '# Summary\n…', mimeType: 'text/markdown' });
|
|
227
282
|
});
|
|
228
283
|
|
|
229
|
-
bot.cmd('ping', async (ctx) => { // no
|
|
284
|
+
bot.cmd({ name: 'ping' }, async (ctx) => { // no mimeType — displays as text/plain
|
|
230
285
|
await ctx.reply({ text: 'pong' });
|
|
231
286
|
});
|
|
232
287
|
```
|
|
@@ -267,8 +322,7 @@ v0.4 is additive — nothing breaks for bots calling only `ctx.reply()`.
|
|
|
267
322
|
New surface:
|
|
268
323
|
|
|
269
324
|
- `new Bot({ …, streaming: true })` to enable `ctx.update()`
|
|
270
|
-
- `
|
|
271
|
-
command without touching bot-init
|
|
325
|
+
- `streaming: true` inside the command definition to flip the flag per command
|
|
272
326
|
- `ctx.update({ progress?, text?, data?, mimeType? })` — send 1+
|
|
273
327
|
intermediate frames before `ctx.reply()`
|
|
274
328
|
- `mimeType` on `ctx.reply()` for the existing final-reply path
|
|
@@ -277,11 +331,25 @@ No env-var changes, no protocol breakage, no upstream re-auth.
|
|
|
277
331
|
|
|
278
332
|
## v0.4.0 → v0.4.1 (per-command mimeType)
|
|
279
333
|
|
|
280
|
-
Additive — no breaking changes. The `mimeType?` field in
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
334
|
+
Additive — no breaking changes. The `mimeType?` field in the command definition
|
|
335
|
+
is optional; bots that ignore it work exactly the same. The SDK auto-builds an
|
|
336
|
+
IDENTIFY-time `commands` array from your registered handlers so the platform can
|
|
337
|
+
display them on the bot detail page.
|
|
338
|
+
|
|
339
|
+
## v0.14 → v0.15 (object `cmd()`)
|
|
340
|
+
|
|
341
|
+
**Breaking.** See [MIGRATION.md](./MIGRATION.md) for the full guide.
|
|
342
|
+
|
|
343
|
+
`cmd()` now takes a **command definition object** as its first argument. The
|
|
344
|
+
string-form is removed.
|
|
345
|
+
|
|
346
|
+
```diff
|
|
347
|
+
- bot.cmd("ping", async (ctx) => { … })
|
|
348
|
+
+ bot.cmd({ name: "ping" }, async (ctx) => { … })
|
|
349
|
+
|
|
350
|
+
- bot.cmd("analyze", { streaming: true }, async (ctx) => { … })
|
|
351
|
+
+ bot.cmd({ name: "analyze", streaming: true }, async (ctx) => { … })
|
|
352
|
+
```
|
|
285
353
|
|
|
286
354
|
## License
|
|
287
355
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ZodType } from 'zod';
|
|
2
|
+
import type { ArgSpec, ReturnsSpec } from './types.js';
|
|
3
|
+
export declare function argsToJsonSchema(args: Record<string, ArgSpec>): Record<string, unknown>;
|
|
4
|
+
export declare function returnsDataToJsonSchema(data: ReturnsSpec['data']): Record<string, unknown> | undefined;
|
|
5
|
+
export declare function returnsDataToZod(data: ReturnsSpec['data']): ZodType | undefined;
|
|
6
|
+
export declare function argsToZod(args: Record<string, ArgSpec>): ZodType;
|
|
7
|
+
//# sourceMappingURL=argSchema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"argSchema.d.ts","sourceRoot":"","sources":["../src/argSchema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAK,KAAK,OAAO,EAAE,MAAM,KAAK,CAAC;AACtC,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAavD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAUvF;AAED,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,GACxB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAKrC;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,OAAO,GAAG,SAAS,CAO/E;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAkBhE"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
function propSchema(s) {
|
|
3
|
+
if (s.type === 'enum') {
|
|
4
|
+
const p = { type: 'string', enum: [...(s.values ?? [])] };
|
|
5
|
+
if (s.description)
|
|
6
|
+
p.description = s.description;
|
|
7
|
+
return p;
|
|
8
|
+
}
|
|
9
|
+
const p = { type: s.type };
|
|
10
|
+
if (s.description)
|
|
11
|
+
p.description = s.description;
|
|
12
|
+
return p;
|
|
13
|
+
}
|
|
14
|
+
export function argsToJsonSchema(args) {
|
|
15
|
+
const properties = {};
|
|
16
|
+
const required = [];
|
|
17
|
+
for (const [name, spec] of Object.entries(args)) {
|
|
18
|
+
properties[name] = propSchema(spec);
|
|
19
|
+
if (spec.required)
|
|
20
|
+
required.push(name);
|
|
21
|
+
}
|
|
22
|
+
const out = { type: 'object', properties };
|
|
23
|
+
if (required.length)
|
|
24
|
+
out.required = required;
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
export function returnsDataToJsonSchema(data) {
|
|
28
|
+
if (!data)
|
|
29
|
+
return undefined;
|
|
30
|
+
const properties = {};
|
|
31
|
+
for (const [name, t] of Object.entries(data))
|
|
32
|
+
properties[name] = { type: t };
|
|
33
|
+
return { type: 'object', properties };
|
|
34
|
+
}
|
|
35
|
+
export function returnsDataToZod(data) {
|
|
36
|
+
if (!data)
|
|
37
|
+
return undefined;
|
|
38
|
+
const shape = {};
|
|
39
|
+
for (const [name, t] of Object.entries(data)) {
|
|
40
|
+
shape[name] = t === 'number' ? z.number() : t === 'boolean' ? z.boolean() : z.string();
|
|
41
|
+
}
|
|
42
|
+
return z.object(shape);
|
|
43
|
+
}
|
|
44
|
+
export function argsToZod(args) {
|
|
45
|
+
const shape = {};
|
|
46
|
+
for (const [name, spec] of Object.entries(args)) {
|
|
47
|
+
if (spec.type === 'enum' && (!spec.values || spec.values.length === 0)) {
|
|
48
|
+
throw new Error(`enum arg requires a non-empty \`values\` array`);
|
|
49
|
+
}
|
|
50
|
+
let field = spec.type === 'string'
|
|
51
|
+
? z.string()
|
|
52
|
+
: spec.type === 'number'
|
|
53
|
+
? z.number()
|
|
54
|
+
: spec.type === 'boolean'
|
|
55
|
+
? z.boolean()
|
|
56
|
+
: z.enum([...(spec.values ?? [])]);
|
|
57
|
+
if (!spec.required)
|
|
58
|
+
field = field.optional();
|
|
59
|
+
shape[name] = field;
|
|
60
|
+
}
|
|
61
|
+
return z.object(shape);
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=argSchema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"argSchema.js","sourceRoot":"","sources":["../src/argSchema.ts"],"names":[],"mappings":"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"}
|
package/dist/bot.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { nanoid } from 'nanoid';
|
|
2
|
-
import type { BotOptions, CommandHandler,
|
|
2
|
+
import type { ArgsOf, BotOptions, CommandContext, CommandDef, CommandHandler, ReturnsOf } from './types.js';
|
|
3
3
|
export { SDK_PACKAGE_VERSION } from './version.js';
|
|
4
4
|
export declare class Bot {
|
|
5
5
|
private readonly opts;
|
|
@@ -18,10 +18,10 @@ export declare class Bot {
|
|
|
18
18
|
private reconnectAttempt;
|
|
19
19
|
private cachedApiKey;
|
|
20
20
|
constructor(opts: BotOptions);
|
|
21
|
-
/** Register a
|
|
22
|
-
cmd(
|
|
23
|
-
/**
|
|
24
|
-
|
|
21
|
+
/** Register a command from its definition object. The ONLY public way to add a command. */
|
|
22
|
+
cmd<const D extends CommandDef>(def: D, handler: (ctx: CommandContext<ArgsOf<D>, ReturnsOf<D>>) => void | Promise<void>): this;
|
|
23
|
+
/** Internal: the single registration path. Not part of the public API. */
|
|
24
|
+
private registerHandler;
|
|
25
25
|
/** Register an internal action handler (Interactive Prompts). Stored in the
|
|
26
26
|
* same handler map `cmd()` uses — so dispatch routing is unchanged — but
|
|
27
27
|
* flagged `internal: true` so it is EXCLUDED from the IDENTIFY advertised
|
package/dist/bot.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bot.d.ts","sourceRoot":"","sources":["../src/bot.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"bot.d.ts","sourceRoot":"","sources":["../src/bot.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAWhC,OAAO,KAAK,EACV,MAAM,EACN,UAAU,EAGV,cAAc,EACd,UAAU,EACV,cAAc,EAMd,SAAS,EAGV,MAAM,YAAY,CAAC;AAKpB,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AA8CnD,qBAAa,GAAG;IACd,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAqB;IAC1C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,SAAS,CAA0B;IAC3C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqC;IAC9D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA4C;IACpE,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA0B;IAC/C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgB;IAC1C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAG1B;IACF,OAAO,CAAC,YAAY,CAA6B;IACjD,OAAO,CAAC,YAAY,CAA+C;IACnE,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,cAAc,CAA8C;IACpE,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,YAAY,CAAuB;gBAE/B,IAAI,EAAE,UAAU;IA2C5B,2FAA2F;IAC3F,GAAG,CAAC,KAAK,CAAC,CAAC,SAAS,UAAU,EAC5B,GAAG,EAAE,CAAC,EACN,OAAO,EAAE,CAAC,GAAG,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAC9E,IAAI;IAiCP,0EAA0E;IAC1E,OAAO,CAAC,eAAe;IASvB;;;;wEAIoE;IACpE,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI;IAIjD,OAAO,CAAC,cAAc;IAMtB,2FAA2F;IACrF,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAsB9B,iFAAiF;YACnE,aAAa;IAmE3B,OAAO,CAAC,iBAAiB;YAcX,YAAY;IAe1B;;;;;OAKG;YACW,aAAa;IAoB3B,0DAA0D;IACpD,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAoBjC;;yEAEqE;IACrE,OAAO,CAAC,oBAAoB;YAYd,QAAQ;IAgGtB,OAAO,CAAC,oBAAoB;IAQ5B,4EAA4E;IAC5E,OAAO,CAAC,kBAAkB;IAc1B;;iBAEa;IACb,OAAO,CAAC,WAAW;IAcnB,OAAO,CAAC,gBAAgB;IAYxB,OAAO,CAAC,WAAW;IA2WnB,OAAO,CAAC,WAAW;IAInB,OAAO,CAAC,UAAU;CAQnB;AAGD,OAAO,EAAE,MAAM,EAAE,CAAC"}
|
package/dist/bot.js
CHANGED
|
@@ -7,6 +7,8 @@ import { readCredentialFor, writeCredentialFor } from './credentials.js';
|
|
|
7
7
|
import { runDeviceFlow } from './sso.js';
|
|
8
8
|
import { CollectStore } from './collectStore.js';
|
|
9
9
|
import { closeDueByTime, makeSubmitHandler } from './collect.js';
|
|
10
|
+
import { toJsonSchema } from './jsonSchema.js';
|
|
11
|
+
import { argsToJsonSchema, returnsDataToJsonSchema, argsToZod, returnsDataToZod } from './argSchema.js';
|
|
10
12
|
// Version constant is generated from package.json at build time by
|
|
11
13
|
// scripts/generate-version.mjs — keeps the IDENTIFY-frame `sdkVersion`
|
|
12
14
|
// field in sync with the published npm version instead of relying on a
|
|
@@ -93,19 +95,57 @@ export class Bot {
|
|
|
93
95
|
// touch sqlite.
|
|
94
96
|
this.action('__collect_submit', (ctx) => makeSubmitHandler(this.ensureCollectStore(), this.collectSink())(ctx));
|
|
95
97
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
98
|
+
/** Register a command from its definition object. The ONLY public way to add a command. */
|
|
99
|
+
cmd(def, handler) {
|
|
100
|
+
if (typeof def === 'string' || def === null || typeof def !== 'object') {
|
|
101
|
+
throw new Error("cmd() takes a command object, e.g. cmd({ name: 'x' }, handler)");
|
|
99
102
|
}
|
|
100
|
-
if (
|
|
101
|
-
throw new Error(
|
|
103
|
+
if (!def.name || typeof def.name !== 'string') {
|
|
104
|
+
throw new Error('cmd() command object requires a string `name`');
|
|
102
105
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (
|
|
107
|
-
|
|
106
|
+
const opts = {};
|
|
107
|
+
if (def.streaming !== undefined)
|
|
108
|
+
opts.streaming = def.streaming;
|
|
109
|
+
if (def.mimeType !== undefined)
|
|
110
|
+
opts.mimeType = def.mimeType;
|
|
111
|
+
if (def.description !== undefined)
|
|
112
|
+
opts.description = def.description;
|
|
113
|
+
// Advertise: args descriptor → inputSchema (args wins over legacy input zod).
|
|
114
|
+
if (def.args)
|
|
115
|
+
opts.advertiseInput = argsToJsonSchema(def.args);
|
|
116
|
+
else if (def.input)
|
|
117
|
+
opts.advertiseInput = toJsonSchema(def.input);
|
|
118
|
+
// returns → outputKind + outputSchema (returns wins over legacy output zod).
|
|
119
|
+
if (def.returns) {
|
|
120
|
+
opts.outputKind = def.returns.kind;
|
|
121
|
+
const outputSchema = returnsDataToJsonSchema(def.returns.data);
|
|
122
|
+
if (outputSchema !== undefined)
|
|
123
|
+
opts.advertiseOutput = outputSchema;
|
|
124
|
+
// Wire returns.data into the existing warn-only check path (L567).
|
|
125
|
+
const warnZod = returnsDataToZod(def.returns.data);
|
|
126
|
+
if (warnZod)
|
|
127
|
+
opts.output = warnZod;
|
|
108
128
|
}
|
|
129
|
+
else if (def.output) {
|
|
130
|
+
opts.advertiseOutput = toJsonSchema(def.output);
|
|
131
|
+
// Keep opts.output for the warn-only output validation in makeContext.
|
|
132
|
+
opts.output = def.output;
|
|
133
|
+
}
|
|
134
|
+
// Validate path: only when explicitly opted in, and only for the args descriptor.
|
|
135
|
+
if (def.validate && def.args)
|
|
136
|
+
opts.argsZod = argsToZod(def.args);
|
|
137
|
+
else if (def.input)
|
|
138
|
+
opts.argsZod = def.input; // legacy zod validates as before
|
|
139
|
+
return this.registerHandler(def.name, opts, handler);
|
|
140
|
+
}
|
|
141
|
+
/** Internal: the single registration path. Not part of the public API. */
|
|
142
|
+
registerHandler(name, opts, handler) {
|
|
143
|
+
if (this.transport)
|
|
144
|
+
throw new Error('cmd()/action() must be called before connect()');
|
|
145
|
+
if (this.handlers.has(name))
|
|
146
|
+
throw new Error(`duplicate handler for command ${name}`);
|
|
147
|
+
if (typeof handler !== 'function')
|
|
148
|
+
throw new Error(`handler for ${name} must be a function`);
|
|
109
149
|
this.handlers.set(name, handler);
|
|
110
150
|
this.cmdOpts.set(name, opts);
|
|
111
151
|
return this;
|
|
@@ -116,7 +156,7 @@ export class Bot {
|
|
|
116
156
|
* command list (capability advertising / marketplace / `commands`
|
|
117
157
|
* introspection). An action is simply a non-advertised command. */
|
|
118
158
|
action(id, handler) {
|
|
119
|
-
return this.
|
|
159
|
+
return this.registerHandler(id, { internal: true }, handler);
|
|
120
160
|
}
|
|
121
161
|
isStreamingFor(commandName) {
|
|
122
162
|
const cmdLevel = this.cmdOpts.get(commandName)?.streaming;
|
|
@@ -163,10 +203,17 @@ export class Bot {
|
|
|
163
203
|
const entry = {
|
|
164
204
|
name,
|
|
165
205
|
streaming,
|
|
206
|
+
description: opts.description ?? '',
|
|
166
207
|
};
|
|
167
208
|
if (typeof opts.mimeType === 'string' && opts.mimeType.length > 0) {
|
|
168
209
|
entry.mimeType = opts.mimeType;
|
|
169
210
|
}
|
|
211
|
+
if (opts.advertiseInput)
|
|
212
|
+
entry.inputSchema = opts.advertiseInput;
|
|
213
|
+
if (opts.advertiseOutput)
|
|
214
|
+
entry.outputSchema = opts.advertiseOutput;
|
|
215
|
+
if (opts.outputKind)
|
|
216
|
+
entry.outputKind = opts.outputKind;
|
|
170
217
|
return entry;
|
|
171
218
|
});
|
|
172
219
|
const transport = new Transport({
|
|
@@ -299,7 +346,7 @@ export class Bot {
|
|
|
299
346
|
this.recordSeen(cmd.msgId);
|
|
300
347
|
const handler = this.handlers.get(cmd.command);
|
|
301
348
|
const streaming = this.isStreamingFor(cmd.command);
|
|
302
|
-
const ctx = this.makeContext(cmd, streaming);
|
|
349
|
+
const { ctx, replyError } = this.makeContext(cmd, streaming);
|
|
303
350
|
// Built-in `commands` fallback — only used if the bot didn't register
|
|
304
351
|
// its own. Lets every bot answer "what can you do?" without ceremony.
|
|
305
352
|
if (!handler && cmd.command === 'commands') {
|
|
@@ -335,6 +382,24 @@ export class Bot {
|
|
|
335
382
|
}
|
|
336
383
|
return;
|
|
337
384
|
}
|
|
385
|
+
// Input validation: a declared zod schema (argsZod from cmd() or legacy input)
|
|
386
|
+
// rejects a malformed dispatch with a structured error and never runs the handler.
|
|
387
|
+
const inputSchema = this.cmdOpts.get(cmd.command)?.argsZod;
|
|
388
|
+
if (inputSchema) {
|
|
389
|
+
const parsed = inputSchema.safeParse(ctx.args);
|
|
390
|
+
if (!parsed.success) {
|
|
391
|
+
try {
|
|
392
|
+
replyError({
|
|
393
|
+
text: `Invalid arguments for ${cmd.command}: ${parsed.error.message}`,
|
|
394
|
+
data: { error: 'invalid_args', issues: parsed.error.issues },
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
catch (err) {
|
|
398
|
+
logError('invalid-args reply failed:', err);
|
|
399
|
+
}
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
338
403
|
try {
|
|
339
404
|
await handler(ctx);
|
|
340
405
|
}
|
|
@@ -342,10 +407,11 @@ export class Bot {
|
|
|
342
407
|
logError('command handler error:', err);
|
|
343
408
|
// If the handler threw before sending a reply, send a structured error
|
|
344
409
|
// frame so the consumer doesn't time out waiting for a reply that will
|
|
345
|
-
// never arrive.
|
|
346
|
-
//
|
|
410
|
+
// never arrive. replyError bypasses kind enforcement (this is an SDK
|
|
411
|
+
// diagnostic frame, not a handler reply) but still runs the double-reply
|
|
412
|
+
// guard — so if the handler already replied, we silently skip.
|
|
347
413
|
try {
|
|
348
|
-
|
|
414
|
+
replyError({
|
|
349
415
|
text: `handler threw: ${err instanceof Error ? err.message : String(err)}`,
|
|
350
416
|
});
|
|
351
417
|
}
|
|
@@ -405,6 +471,13 @@ export class Bot {
|
|
|
405
471
|
makeContext(cmd, streaming) {
|
|
406
472
|
let replied = false;
|
|
407
473
|
const transport = this.transport;
|
|
474
|
+
const outputSchema = this.cmdOpts.get(cmd.command)?.output;
|
|
475
|
+
const declaredKind = this.cmdOpts.get(cmd.command)?.outputKind;
|
|
476
|
+
const assertKind = (actual) => {
|
|
477
|
+
if (declaredKind && declaredKind !== actual) {
|
|
478
|
+
throw new Error(`command ${cmd.command} declared returns.kind='${declaredKind}' but sent a '${actual}' reply`);
|
|
479
|
+
}
|
|
480
|
+
};
|
|
408
481
|
// Interactive Prompts: the server injects the answering user's id as the
|
|
409
482
|
// reserved `args._user`. Lift it onto ctx.user and strip it from ctx.args
|
|
410
483
|
// so handlers never see the reserved key. Undefined for normal commands.
|
|
@@ -437,7 +510,17 @@ export class Bot {
|
|
|
437
510
|
...(answeringUser ? { user: answeringUser } : {}),
|
|
438
511
|
msgId: cmd.msgId,
|
|
439
512
|
reply: async (body) => {
|
|
513
|
+
assertKind((body.kind ?? 'text'));
|
|
440
514
|
guardReply();
|
|
515
|
+
// Warn-only output validation: a declared output schema that the
|
|
516
|
+
// structured `data` violates logs a warning but NEVER blocks the
|
|
517
|
+
// user-facing reply — schema drift must not break a response.
|
|
518
|
+
if (outputSchema) {
|
|
519
|
+
const res = outputSchema.safeParse(body.data);
|
|
520
|
+
if (!res.success) {
|
|
521
|
+
logWarn(`output for ${cmd.command} does not match its declared schema (sending anyway): ${res.error.message}`);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
441
524
|
// guardReply throws if transport is null, so the non-null assertion
|
|
442
525
|
// below is safe — TypeScript can't see through the closure.
|
|
443
526
|
transport.sendReply(cmd.msgId, body);
|
|
@@ -457,6 +540,7 @@ export class Bot {
|
|
|
457
540
|
if (header !== undefined && typeof header !== 'string') {
|
|
458
541
|
throw new Error(`replyList() header must be a string when provided (msgId=${cmd.msgId})`);
|
|
459
542
|
}
|
|
543
|
+
assertKind('list');
|
|
460
544
|
guardReply();
|
|
461
545
|
const body = {
|
|
462
546
|
kind: 'list',
|
|
@@ -485,6 +569,7 @@ export class Bot {
|
|
|
485
569
|
throw new Error(`replyTable() row[${r}] has ${Array.isArray(row) ? row.length : 'non-array'} cell(s), expected ${cols} (msgId=${cmd.msgId})`);
|
|
486
570
|
}
|
|
487
571
|
}
|
|
572
|
+
assertKind('table');
|
|
488
573
|
guardReply();
|
|
489
574
|
const out = {
|
|
490
575
|
kind: 'table',
|
|
@@ -516,6 +601,7 @@ export class Bot {
|
|
|
516
601
|
if (!base64) {
|
|
517
602
|
throw new Error(`replyImage() requires non-empty image data (msgId=${cmd.msgId})`);
|
|
518
603
|
}
|
|
604
|
+
assertKind('image');
|
|
519
605
|
guardReply();
|
|
520
606
|
const out = {
|
|
521
607
|
kind: 'image',
|
|
@@ -542,6 +628,7 @@ export class Bot {
|
|
|
542
628
|
throw new Error(`replyButtons() button[${i}] is missing a non-empty action (msgId=${cmd.msgId})`);
|
|
543
629
|
}
|
|
544
630
|
}
|
|
631
|
+
assertKind('buttons');
|
|
545
632
|
guardReply();
|
|
546
633
|
const out = {
|
|
547
634
|
kind: 'buttons',
|
|
@@ -640,6 +727,7 @@ export class Bot {
|
|
|
640
727
|
until,
|
|
641
728
|
origin: { msgId: cmd.msgId },
|
|
642
729
|
});
|
|
730
|
+
assertKind('collect');
|
|
643
731
|
guardReply();
|
|
644
732
|
transport.sendReply(cmd.msgId, {
|
|
645
733
|
kind: 'collect',
|
|
@@ -675,7 +763,15 @@ export class Bot {
|
|
|
675
763
|
transport.sendUpdate(cmd.msgId, progress, body);
|
|
676
764
|
};
|
|
677
765
|
}
|
|
678
|
-
|
|
766
|
+
// Internal sender for SDK-generated error/diagnostic frames (invalid_args,
|
|
767
|
+
// handler-threw recovery). Bypasses assertKind — kind enforcement is a
|
|
768
|
+
// contract on the HANDLER's reply, not on SDK infrastructure frames — but
|
|
769
|
+
// still runs guardReply so double-reply protection stays intact.
|
|
770
|
+
const replyError = (body) => {
|
|
771
|
+
guardReply();
|
|
772
|
+
transport.sendReply(cmd.msgId, body);
|
|
773
|
+
};
|
|
774
|
+
return { ctx, replyError };
|
|
679
775
|
}
|
|
680
776
|
isDuplicate(msgId) {
|
|
681
777
|
return this.seen.has(msgId);
|