@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 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
@@ -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"}
@@ -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"}
@@ -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,4 @@
1
+ export const SDK_PACKAGE_VERSION = '0.2.0';
2
+ export { Bot } from './bot.js';
3
+ export { subjects } from './subjects.js';
4
+ //# sourceMappingURL=index.js.map
@@ -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"}
@@ -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"}
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -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
+ }