@localhostdevs/sdk 0.2.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 +111 -0
- package/dist/bot.d.ts +26 -0
- package/dist/bot.d.ts.map +1 -0
- package/dist/bot.js +129 -0
- package/dist/bot.js.map +1 -0
- package/dist/context.d.ts +11 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +23 -0
- package/dist/context.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/subjects.d.ts +17 -0
- package/dist/subjects.d.ts.map +1 -0
- package/dist/subjects.js +17 -0
- package/dist/subjects.js.map +1 -0
- package/dist/types.d.ts +59 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +54 -0
package/README.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
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
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @localhostdevs/sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quickstart
|
|
12
|
+
|
|
13
|
+
After creating a bot in the [dashboard](https://localhostdevs.com/dashboard/bots) and copying your API key, write this to `app.js`:
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import { Bot } from '@localhostdevs/sdk';
|
|
17
|
+
|
|
18
|
+
const bot = new Bot({
|
|
19
|
+
id: 'price-bot', // your bot's handle (matches the @handle on the marketplace)
|
|
20
|
+
natsUrl: 'nats://broker.localhostdevs.com:4222',
|
|
21
|
+
// natsToken: process.env.LHD_NATS_TOKEN, // optional; auto-read from env if unset
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
bot.cmd('ping', async (ctx) => {
|
|
25
|
+
await ctx.reply({ text: 'pong' });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
bot.cmd('echo', async (ctx) => {
|
|
29
|
+
await ctx.reply({ text: `echo: ${JSON.stringify(ctx.args)}` });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
await bot.connect();
|
|
33
|
+
console.log('bot online — waiting for commands');
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Then:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
LHD_NATS_TOKEN=<broker token from the dashboard> node app.js
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The bot stays online for as long as the process runs. Heartbeats fire every 5 seconds so the marketplace shows it as online.
|
|
43
|
+
|
|
44
|
+
## API
|
|
45
|
+
|
|
46
|
+
### `new Bot(opts)`
|
|
47
|
+
|
|
48
|
+
| Option | Type | Default | Notes |
|
|
49
|
+
| -------------------- | ------------------- | ---------------------------- | ---------------------------------------------------------------------------------------- |
|
|
50
|
+
| `id` | `string` (required) | — | Your bot's handle. Lowercase letters / digits / dashes. |
|
|
51
|
+
| `natsUrl` | `string` | `nats://localhost:4222` | The NATS broker URL. |
|
|
52
|
+
| `natsToken` | `string` | `process.env.LHD_NATS_TOKEN` | Token presented at NATS connect. Required by the production broker; unset for local dev. |
|
|
53
|
+
| `heartbeatMs` | `number` | `5000` | Heartbeat interval in milliseconds. |
|
|
54
|
+
| `idempotencyLruSize` | `number` | `1024` | How many recent message IDs to remember for deduplication. |
|
|
55
|
+
|
|
56
|
+
### `bot.cmd(name, handler)`
|
|
57
|
+
|
|
58
|
+
Register a handler for a command. Must be called before `connect()`. Handler receives a `CommandContext`:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
bot.cmd('greet', async (ctx) => {
|
|
62
|
+
// ctx.args: parsed args from the consumer
|
|
63
|
+
// ctx.command: 'greet'
|
|
64
|
+
// ctx.msgId: unique id for this invocation
|
|
65
|
+
|
|
66
|
+
await ctx.reply({ text: `Hello, ${ctx.args.name ?? 'world'}!` });
|
|
67
|
+
|
|
68
|
+
// ctx.reply also supports:
|
|
69
|
+
// data: any — structured payload alongside text
|
|
70
|
+
// dispatch: { to, command, args } — chain to another bot (max depth 5)
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### `await bot.connect()`
|
|
75
|
+
|
|
76
|
+
Open the NATS connection, subscribe to your command subject, and start emitting heartbeats. Resolves after the subscription is registered with the broker — safe to publish from outside immediately after.
|
|
77
|
+
|
|
78
|
+
### `await bot.disconnect()`
|
|
79
|
+
|
|
80
|
+
Stop heartbeats, drain the subscription, close the connection.
|
|
81
|
+
|
|
82
|
+
### `bot.subjectFor(commandName)`
|
|
83
|
+
|
|
84
|
+
Helper that returns the NATS subject this command would land on. Useful in tests and scripts.
|
|
85
|
+
|
|
86
|
+
## Idempotency
|
|
87
|
+
|
|
88
|
+
Every command from the platform carries a `msgId`. The SDK deduplicates redelivered messages via a fixed-size LRU keyed by `msgId`. If a consumer accidentally re-sends the same command, your handler runs once.
|
|
89
|
+
|
|
90
|
+
## Heartbeats
|
|
91
|
+
|
|
92
|
+
The SDK publishes a heartbeat every `heartbeatMs` (default 5s) so the marketplace can show your bot as **online**. If heartbeats stop arriving for >15s the bot appears **stale**; >30s it goes **offline**.
|
|
93
|
+
|
|
94
|
+
## Bot-to-bot composition
|
|
95
|
+
|
|
96
|
+
A reply can carry a `dispatch` directive to chain a command to another bot:
|
|
97
|
+
|
|
98
|
+
```js
|
|
99
|
+
bot.cmd('summary', async (ctx) => {
|
|
100
|
+
await ctx.reply({
|
|
101
|
+
text: 'fetching price first…',
|
|
102
|
+
dispatch: { to: 'price-bot', command: 'price', args: { ticker: 'AAPL' } },
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Chains run up to depth 5 with loop detection. The original consumer pays for every step.
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
|
|
111
|
+
MIT
|
package/dist/bot.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { nanoid } from 'nanoid';
|
|
2
|
+
import type { BotOptions, CommandHandler } from './types.js';
|
|
3
|
+
export declare class Bot {
|
|
4
|
+
private readonly opts;
|
|
5
|
+
private nc;
|
|
6
|
+
private subscription;
|
|
7
|
+
private heartbeatTimer;
|
|
8
|
+
private readonly handlers;
|
|
9
|
+
private readonly seen;
|
|
10
|
+
private readonly seenQueue;
|
|
11
|
+
constructor(opts: BotOptions);
|
|
12
|
+
/** Register a handler for a command. Must be called before connect(). */
|
|
13
|
+
cmd(name: string, handler: CommandHandler): this;
|
|
14
|
+
/** Helper for tests + scripts: the subject a command should be published to. */
|
|
15
|
+
subjectFor(command: string): string;
|
|
16
|
+
/** Open NATS connection, subscribe to command subjects, start heartbeat. */
|
|
17
|
+
connect(): Promise<void>;
|
|
18
|
+
/** Tear down: stop heartbeat, drain subscription, close NATS. */
|
|
19
|
+
disconnect(): Promise<void>;
|
|
20
|
+
private publishHeartbeat;
|
|
21
|
+
private consume;
|
|
22
|
+
private isDuplicate;
|
|
23
|
+
private recordSeen;
|
|
24
|
+
}
|
|
25
|
+
export { nanoid };
|
|
26
|
+
//# sourceMappingURL=bot.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bot.d.ts","sourceRoot":"","sources":["../src/bot.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAGhC,OAAO,KAAK,EAAE,UAAU,EAAmB,cAAc,EAAqB,MAAM,YAAY,CAAC;AASjG,qBAAa,GAAG;IACd,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAqB;IAC1C,OAAO,CAAC,EAAE,CAA+B;IACzC,OAAO,CAAC,YAAY,CAA6B;IACjD,OAAO,CAAC,cAAc,CAA+C;IACrE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqC;IAC9D,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA0B;IAC/C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgB;gBAE9B,IAAI,EAAE,UAAU;IAa5B,yEAAyE;IACzE,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI;IAWhD,gFAAgF;IAChF,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM;IAInC,4EAA4E;IACtE,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAsB9B,iEAAiE;IAC3D,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAejC,OAAO,CAAC,gBAAgB;YAMV,OAAO;IA+BrB,OAAO,CAAC,WAAW;IAInB,OAAO,CAAC,UAAU;CAQnB;AAGD,OAAO,EAAE,MAAM,EAAE,CAAC"}
|
package/dist/bot.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { connect, JSONCodec } from 'nats';
|
|
2
|
+
import { nanoid } from 'nanoid';
|
|
3
|
+
import { createCommandContext } from './context.js';
|
|
4
|
+
import { subjects } from './subjects.js';
|
|
5
|
+
const codec = JSONCodec();
|
|
6
|
+
const DEFAULT_NATS_URL = 'nats://localhost:4222';
|
|
7
|
+
const DEFAULT_HEARTBEAT_MS = 5000;
|
|
8
|
+
const DEFAULT_IDEMPOTENCY_LRU_SIZE = 1024;
|
|
9
|
+
export class Bot {
|
|
10
|
+
opts;
|
|
11
|
+
nc = null;
|
|
12
|
+
subscription = null;
|
|
13
|
+
heartbeatTimer = null;
|
|
14
|
+
handlers = new Map();
|
|
15
|
+
seen = new Set();
|
|
16
|
+
seenQueue = []; // FIFO for LRU eviction
|
|
17
|
+
constructor(opts) {
|
|
18
|
+
if (!opts.id || !/^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(opts.id)) {
|
|
19
|
+
throw new Error(`Bot id must be lowercase alphanumeric + dashes (got ${opts.id})`);
|
|
20
|
+
}
|
|
21
|
+
this.opts = {
|
|
22
|
+
id: opts.id,
|
|
23
|
+
natsUrl: opts.natsUrl ?? DEFAULT_NATS_URL,
|
|
24
|
+
natsToken: opts.natsToken ?? process.env.LHD_NATS_TOKEN ?? null,
|
|
25
|
+
heartbeatMs: opts.heartbeatMs ?? DEFAULT_HEARTBEAT_MS,
|
|
26
|
+
idempotencyLruSize: opts.idempotencyLruSize ?? DEFAULT_IDEMPOTENCY_LRU_SIZE,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** Register a handler for a command. Must be called before connect(). */
|
|
30
|
+
cmd(name, handler) {
|
|
31
|
+
if (this.nc) {
|
|
32
|
+
throw new Error('cmd() must be called before connect()');
|
|
33
|
+
}
|
|
34
|
+
if (this.handlers.has(name)) {
|
|
35
|
+
throw new Error(`duplicate handler for command ${name}`);
|
|
36
|
+
}
|
|
37
|
+
this.handlers.set(name, handler);
|
|
38
|
+
return this;
|
|
39
|
+
}
|
|
40
|
+
/** Helper for tests + scripts: the subject a command should be published to. */
|
|
41
|
+
subjectFor(command) {
|
|
42
|
+
return subjects.cmd(this.opts.id, command);
|
|
43
|
+
}
|
|
44
|
+
/** Open NATS connection, subscribe to command subjects, start heartbeat. */
|
|
45
|
+
async connect() {
|
|
46
|
+
if (this.nc) {
|
|
47
|
+
throw new Error('already connected');
|
|
48
|
+
}
|
|
49
|
+
const connectOpts = { servers: this.opts.natsUrl };
|
|
50
|
+
if (this.opts.natsToken) {
|
|
51
|
+
connectOpts.token = this.opts.natsToken;
|
|
52
|
+
}
|
|
53
|
+
this.nc = await connect(connectOpts);
|
|
54
|
+
this.subscription = this.nc.subscribe(subjects.cmdAll(this.opts.id));
|
|
55
|
+
// Force the SUB protocol command to be ack'd by the server before connect()
|
|
56
|
+
// resolves. Without this, callers (and tests) that publish immediately after
|
|
57
|
+
// connect() can lose the first message — NATS core drops messages with no
|
|
58
|
+
// matching subscribers.
|
|
59
|
+
await this.nc.flush();
|
|
60
|
+
void this.consume();
|
|
61
|
+
this.heartbeatTimer = setInterval(() => this.publishHeartbeat(), this.opts.heartbeatMs);
|
|
62
|
+
this.publishHeartbeat(); // emit immediately on connect
|
|
63
|
+
}
|
|
64
|
+
/** Tear down: stop heartbeat, drain subscription, close NATS. */
|
|
65
|
+
async disconnect() {
|
|
66
|
+
if (this.heartbeatTimer) {
|
|
67
|
+
clearInterval(this.heartbeatTimer);
|
|
68
|
+
this.heartbeatTimer = null;
|
|
69
|
+
}
|
|
70
|
+
if (this.subscription) {
|
|
71
|
+
this.subscription.unsubscribe();
|
|
72
|
+
this.subscription = null;
|
|
73
|
+
}
|
|
74
|
+
if (this.nc) {
|
|
75
|
+
await this.nc.drain();
|
|
76
|
+
this.nc = null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
publishHeartbeat() {
|
|
80
|
+
if (!this.nc)
|
|
81
|
+
return;
|
|
82
|
+
const beat = { botId: this.opts.id, ts: Date.now() };
|
|
83
|
+
this.nc.publish(subjects.heartbeat(this.opts.id), codec.encode(beat));
|
|
84
|
+
}
|
|
85
|
+
async consume() {
|
|
86
|
+
if (!this.subscription || !this.nc)
|
|
87
|
+
return;
|
|
88
|
+
for await (const msg of this.subscription) {
|
|
89
|
+
try {
|
|
90
|
+
const envelope = codec.decode(msg.data);
|
|
91
|
+
if (!envelope ||
|
|
92
|
+
typeof envelope.command !== 'string' ||
|
|
93
|
+
typeof envelope.msgId !== 'string') {
|
|
94
|
+
continue; // malformed; drop silently
|
|
95
|
+
}
|
|
96
|
+
if (this.isDuplicate(envelope.msgId)) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
this.recordSeen(envelope.msgId);
|
|
100
|
+
const handler = this.handlers.get(envelope.command);
|
|
101
|
+
if (!handler) {
|
|
102
|
+
// Unknown command — could publish an error reply; v1 keeps it simple and drops.
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const ctx = createCommandContext(this.nc, envelope);
|
|
106
|
+
await handler(ctx);
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
// Don't let one bad message kill the loop. Log to stderr for now.
|
|
110
|
+
console.error('[localhostdevs sdk] command dispatch error:', err);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
isDuplicate(msgId) {
|
|
115
|
+
return this.seen.has(msgId);
|
|
116
|
+
}
|
|
117
|
+
recordSeen(msgId) {
|
|
118
|
+
this.seen.add(msgId);
|
|
119
|
+
this.seenQueue.push(msgId);
|
|
120
|
+
while (this.seenQueue.length > this.opts.idempotencyLruSize) {
|
|
121
|
+
const evicted = this.seenQueue.shift();
|
|
122
|
+
if (evicted)
|
|
123
|
+
this.seen.delete(evicted);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Re-export nanoid so demo scripts can use the same id generator the SDK expects.
|
|
128
|
+
export { nanoid };
|
|
129
|
+
//# sourceMappingURL=bot.js.map
|
package/dist/bot.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bot.js","sourceRoot":"","sources":["../src/bot.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { NatsConnection } from 'nats';
|
|
2
|
+
import type { CommandContext } from './types.js';
|
|
3
|
+
import { subjects } from './subjects.js';
|
|
4
|
+
export declare function createCommandContext(nc: NatsConnection, envelope: {
|
|
5
|
+
command: string;
|
|
6
|
+
args: Record<string, unknown>;
|
|
7
|
+
msgId: string;
|
|
8
|
+
replyTo: string;
|
|
9
|
+
}): CommandContext;
|
|
10
|
+
export { subjects };
|
|
11
|
+
//# sourceMappingURL=context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,MAAM,CAAC;AAE3C,OAAO,KAAK,EAAE,cAAc,EAA4B,MAAM,YAAY,CAAC;AAC3E,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAIzC,wBAAgB,oBAAoB,CAClC,EAAE,EAAE,cAAc,EAClB,QAAQ,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAC3F,cAAc,CAiBhB;AAGD,OAAO,EAAE,QAAQ,EAAE,CAAC"}
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { JSONCodec } from 'nats';
|
|
2
|
+
import { subjects } from './subjects.js';
|
|
3
|
+
const codec = JSONCodec();
|
|
4
|
+
export function createCommandContext(nc, envelope) {
|
|
5
|
+
let replied = false;
|
|
6
|
+
return {
|
|
7
|
+
command: envelope.command,
|
|
8
|
+
args: envelope.args,
|
|
9
|
+
msgId: envelope.msgId,
|
|
10
|
+
async reply(body) {
|
|
11
|
+
if (replied) {
|
|
12
|
+
throw new Error(`reply() called more than once for msgId=${envelope.msgId}`);
|
|
13
|
+
}
|
|
14
|
+
replied = true;
|
|
15
|
+
const payload = { msgId: envelope.msgId, body };
|
|
16
|
+
nc.publish(envelope.replyTo, codec.encode(payload));
|
|
17
|
+
// No need to await — NATS publish is fire-and-forget.
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
// Re-export for tests.
|
|
22
|
+
export { subjects };
|
|
23
|
+
//# sourceMappingURL=context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,MAAM,CAAC;AAEjC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAEzC,MAAM,KAAK,GAAG,SAAS,EAAE,CAAC;AAE1B,MAAM,UAAU,oBAAoB,CAClC,EAAkB,EAClB,QAA4F;IAE5F,IAAI,OAAO,GAAG,KAAK,CAAC;IAEpB,OAAO;QACL,OAAO,EAAE,QAAQ,CAAC,OAAO;QACzB,IAAI,EAAE,QAAQ,CAAC,IAAI;QACnB,KAAK,EAAE,QAAQ,CAAC,KAAK;QACrB,KAAK,CAAC,KAAK,CAAC,IAAe;YACzB,IAAI,OAAO,EAAE,CAAC;gBACZ,MAAM,IAAI,KAAK,CAAC,2CAA2C,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC;YAC/E,CAAC;YACD,OAAO,GAAG,IAAI,CAAC;YACf,MAAM,OAAO,GAAkB,EAAE,KAAK,EAAE,QAAQ,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC;YAC/D,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;YACpD,sDAAsD;QACxD,CAAC;KACF,CAAC;AACJ,CAAC;AAED,uBAAuB;AACvB,OAAO,EAAE,QAAQ,EAAE,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const SDK_PACKAGE_VERSION = "0.2.0";
|
|
2
|
+
export { Bot } from './bot.js';
|
|
3
|
+
export { subjects } from './subjects.js';
|
|
4
|
+
export type { BotOptions, CommandContext, CommandHandler, CommandEnvelope, ReplyBody, ReplyEnvelope, HeartbeatEnvelope, } from './types.js';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,mBAAmB,UAAU,CAAC;AAC3C,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAC/B,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,YAAY,EACV,UAAU,EACV,cAAc,EACd,cAAc,EACd,eAAe,EACf,SAAS,EACT,aAAa,EACb,iBAAiB,GAClB,MAAM,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,mBAAmB,GAAG,OAAO,CAAC;AAC3C,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAC/B,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NATS subject helpers.
|
|
3
|
+
*
|
|
4
|
+
* bot.<id>.cmd.<command> — command published by gateway/CLI, consumed by bot
|
|
5
|
+
* bot.<id>.heartbeat — 5-second presence pings from the bot
|
|
6
|
+
* reply.<msgId> — per-message reply subject; publisher subscribes to this
|
|
7
|
+
*
|
|
8
|
+
* The reply subject deliberately doesn't include the bot id; replies are correlated
|
|
9
|
+
* via msgId, and the publisher only listens to its own short-lived reply subject.
|
|
10
|
+
*/
|
|
11
|
+
export declare const subjects: {
|
|
12
|
+
cmd: (botId: string, command: string) => string;
|
|
13
|
+
cmdAll: (botId: string) => string;
|
|
14
|
+
heartbeat: (botId: string) => string;
|
|
15
|
+
reply: (msgId: string) => string;
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=subjects.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subjects.d.ts","sourceRoot":"","sources":["../src/subjects.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,eAAO,MAAM,QAAQ;iBACN,MAAM,WAAW,MAAM,KAAG,MAAM;oBAC7B,MAAM,KAAG,MAAM;uBACZ,MAAM,KAAG,MAAM;mBACnB,MAAM,KAAG,MAAM;CAC/B,CAAC"}
|
package/dist/subjects.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NATS subject helpers.
|
|
3
|
+
*
|
|
4
|
+
* bot.<id>.cmd.<command> — command published by gateway/CLI, consumed by bot
|
|
5
|
+
* bot.<id>.heartbeat — 5-second presence pings from the bot
|
|
6
|
+
* reply.<msgId> — per-message reply subject; publisher subscribes to this
|
|
7
|
+
*
|
|
8
|
+
* The reply subject deliberately doesn't include the bot id; replies are correlated
|
|
9
|
+
* via msgId, and the publisher only listens to its own short-lived reply subject.
|
|
10
|
+
*/
|
|
11
|
+
export const subjects = {
|
|
12
|
+
cmd: (botId, command) => `bot.${botId}.cmd.${command}`,
|
|
13
|
+
cmdAll: (botId) => `bot.${botId}.cmd.>`,
|
|
14
|
+
heartbeat: (botId) => `bot.${botId}.heartbeat`,
|
|
15
|
+
reply: (msgId) => `reply.${msgId}`,
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=subjects.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subjects.js","sourceRoot":"","sources":["../src/subjects.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,MAAM,CAAC,MAAM,QAAQ,GAAG;IACtB,GAAG,EAAE,CAAC,KAAa,EAAE,OAAe,EAAU,EAAE,CAAC,OAAO,KAAK,QAAQ,OAAO,EAAE;IAC9E,MAAM,EAAE,CAAC,KAAa,EAAU,EAAE,CAAC,OAAO,KAAK,QAAQ;IACvD,SAAS,EAAE,CAAC,KAAa,EAAU,EAAE,CAAC,OAAO,KAAK,YAAY;IAC9D,KAAK,EAAE,CAAC,KAAa,EAAU,EAAE,CAAC,SAAS,KAAK,EAAE;CACnD,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export type CommandHandler = (ctx: CommandContext) => Promise<void> | void;
|
|
2
|
+
export interface CommandContext {
|
|
3
|
+
/** Bot-scoped command name (e.g., 'price', 'analyze'). */
|
|
4
|
+
readonly command: string;
|
|
5
|
+
/** Decoded argument map from the publisher. */
|
|
6
|
+
readonly args: Record<string, unknown>;
|
|
7
|
+
/** Unique id assigned by the publisher; used for idempotency dedup + reply correlation. */
|
|
8
|
+
readonly msgId: string;
|
|
9
|
+
/** Sends a reply to the publisher. May be called at most once per command. */
|
|
10
|
+
reply(body: ReplyBody): Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
export interface ReplyBody {
|
|
13
|
+
/** Plain text reply. Optional. */
|
|
14
|
+
text?: string;
|
|
15
|
+
/** Optional structured payload (free-form for v1; refined in later plans). */
|
|
16
|
+
data?: unknown;
|
|
17
|
+
/**
|
|
18
|
+
* Bot-to-bot composition: when set, the platform invokes another bot
|
|
19
|
+
* with this command using the same originating consumer as the principal.
|
|
20
|
+
* Only single-dispatch is supported in v1.
|
|
21
|
+
*/
|
|
22
|
+
dispatch?: {
|
|
23
|
+
to: string;
|
|
24
|
+
command: string;
|
|
25
|
+
args?: Record<string, unknown>;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export interface BotOptions {
|
|
29
|
+
/** Stable identifier for this bot (e.g., 'price-bot'). Used in NATS subject names. */
|
|
30
|
+
id: string;
|
|
31
|
+
/** NATS connection URL. Defaults to `nats://localhost:4222`. */
|
|
32
|
+
natsUrl?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Token presented at NATS connect time when the broker requires auth.
|
|
35
|
+
* For localhostdevs production, the platform supplies this via the
|
|
36
|
+
* `LHD_NATS_TOKEN` env var. Falls back to `process.env.LHD_NATS_TOKEN`.
|
|
37
|
+
*/
|
|
38
|
+
natsToken?: string;
|
|
39
|
+
/** Heartbeat interval in milliseconds. Defaults to 5000. */
|
|
40
|
+
heartbeatMs?: number;
|
|
41
|
+
/** Idempotency LRU size. Defaults to 1024. */
|
|
42
|
+
idempotencyLruSize?: number;
|
|
43
|
+
}
|
|
44
|
+
export interface CommandEnvelope {
|
|
45
|
+
msgId: string;
|
|
46
|
+
command: string;
|
|
47
|
+
args: Record<string, unknown>;
|
|
48
|
+
/** NATS subject the bot should reply on. */
|
|
49
|
+
replyTo: string;
|
|
50
|
+
}
|
|
51
|
+
export interface ReplyEnvelope {
|
|
52
|
+
msgId: string;
|
|
53
|
+
body: ReplyBody;
|
|
54
|
+
}
|
|
55
|
+
export interface HeartbeatEnvelope {
|
|
56
|
+
botId: string;
|
|
57
|
+
ts: number;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,cAAc,GAAG,CAAC,GAAG,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAE3E,MAAM,WAAW,cAAc;IAC7B,0DAA0D;IAC1D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,+CAA+C;IAC/C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACvC,2FAA2F;IAC3F,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,8EAA8E;IAC9E,KAAK,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC;AAED,MAAM,WAAW,SAAS;IACxB,kCAAkC;IAClC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8EAA8E;IAC9E,IAAI,CAAC,EAAE,OAAO,CAAC;IACf;;;;OAIG;IACH,QAAQ,CAAC,EAAE;QACT,EAAE,EAAE,MAAM,CAAC;QACX,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KAChC,CAAC;CACH;AAED,MAAM,WAAW,UAAU;IACzB,sFAAsF;IACtF,EAAE,EAAE,MAAM,CAAC;IACX,gEAAgE;IAChE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4DAA4D;IAC5D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,8CAA8C;IAC9C,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,4CAA4C;IAC5C,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,SAAS,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,MAAM,CAAC;CACZ"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@localhostdevs/sdk",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "JavaScript SDK for building bots on localhostdevs — run your app on your laptop, publish it as a command-driven bot.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"localhostdevs",
|
|
7
|
+
"bot",
|
|
8
|
+
"nats",
|
|
9
|
+
"chatops",
|
|
10
|
+
"cli",
|
|
11
|
+
"sdk"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/jugod0/localhostdevelopers",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/jugod0/localhostdevelopers.git",
|
|
17
|
+
"directory": "packages/sdk"
|
|
18
|
+
},
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"type": "module",
|
|
21
|
+
"main": "./dist/index.js",
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"import": "./dist/index.js",
|
|
27
|
+
"default": "./dist/index.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"README.md"
|
|
33
|
+
],
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=20"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsc -p tsconfig.build.json",
|
|
39
|
+
"typecheck": "tsc --noEmit",
|
|
40
|
+
"test": "vitest run",
|
|
41
|
+
"demo:client": "tsx scripts/demo-client.ts",
|
|
42
|
+
"prepublishOnly": "npm run build"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"nanoid": "^5.1.11",
|
|
46
|
+
"nats": "^2.29.3"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"tsx": "^4.21.0"
|
|
50
|
+
},
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"access": "public"
|
|
53
|
+
}
|
|
54
|
+
}
|