@omnixal/openclaw-nats-plugin 0.1.9 → 0.1.11

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/cli/bun-setup.ts CHANGED
@@ -58,11 +58,13 @@ export async function bunSetup(): Promise<void> {
58
58
  writeEnvVariables(envVars);
59
59
 
60
60
  // Write .env into sidecar dir so loadDotEnv picks it up
61
+ // Explicit localhost values override any container-level env (e.g. OPENCLAW_WS_URL=ws://openclaw:...)
61
62
  const sidecarEnv = [
62
63
  `PORT=3104`,
63
64
  `DB_PATH=${join(DATA_DIR, 'nats-sidecar.db')}`,
64
65
  `NATS_SERVERS=nats://127.0.0.1:4222`,
65
66
  `NATS_PLUGIN_API_KEY=${apiKey}`,
67
+ `OPENCLAW_WS_URL=ws://127.0.0.1:18789`,
66
68
  ].join('\n');
67
69
  writeFileSync(join(SIDECAR_DIR, '.env'), sidecarEnv, 'utf-8');
68
70
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnixal/openclaw-nats-plugin",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "NATS JetStream event-driven plugin for OpenClaw",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,17 +1,12 @@
1
- import { Service, BaseService, type OnModuleInit, type OnModuleDestroy } from '@onebun/core';
2
- import type { Message, Subscription } from '@onebun/core';
3
- import { NatsAdapterService } from '../nats-streams/nats-adapter.service';
1
+ import { Controller, BaseController, Subscribe, OnQueueReady, type Message } from '@onebun/core';
4
2
  import { PipelineService } from '../pre-handlers/pipeline.service';
5
3
  import { GatewayClientService } from '../gateway/gateway-client.service';
6
4
  import { PendingService } from '../pending/pending.service';
7
5
  import type { NatsEventEnvelope } from '../publisher/envelope';
8
6
 
9
- @Service()
10
- export class ConsumerService extends BaseService implements OnModuleInit, OnModuleDestroy {
11
- private subscription: Subscription | null = null;
12
-
7
+ @Controller('/consumer')
8
+ export class ConsumerController extends BaseController {
13
9
  constructor(
14
- private natsAdapter: NatsAdapterService,
15
10
  private pipeline: PipelineService,
16
11
  private gatewayClient: GatewayClientService,
17
12
  private pendingService: PendingService,
@@ -19,26 +14,17 @@ export class ConsumerService extends BaseService implements OnModuleInit, OnModu
19
14
  super();
20
15
  }
21
16
 
22
- async onModuleInit(): Promise<void> {
23
- if (!this.natsAdapter.isConnected()) {
24
- this.logger.warn('NATS not connected, consumer will not start');
25
- return;
26
- }
27
-
28
- try {
29
- const consumerName = this.config.get('consumer.name');
30
- this.subscription = await this.natsAdapter.subscribe(
31
- 'agent.inbound.>',
32
- (message: Message<unknown>) => this.handleInbound(message),
33
- { ackMode: 'manual', group: consumerName },
34
- );
35
- this.logger.info(`Consuming from agent_inbound as ${consumerName}`);
36
- } catch (err: any) {
37
- this.logger.warn(`Failed to start consumer: ${err?.message}`);
38
- }
17
+ @OnQueueReady()
18
+ onReady() {
19
+ const consumerName = this.config.get('consumer.name');
20
+ this.logger.info(`Queue connected, consuming as ${consumerName}`);
39
21
  }
40
22
 
41
- private async handleInbound(message: Message<unknown>): Promise<void> {
23
+ @Subscribe('agent.inbound.>', {
24
+ ackMode: 'manual',
25
+ group: 'openclaw-main',
26
+ })
27
+ async handleInbound(message: Message<unknown>): Promise<void> {
42
28
  try {
43
29
  const envelope = this.extractEnvelope(message);
44
30
 
@@ -103,11 +89,4 @@ export class ConsumerService extends BaseService implements OnModuleInit, OnModu
103
89
  private formatMessage(envelope: NatsEventEnvelope): string {
104
90
  return `[NATS:${envelope.subject}] ${JSON.stringify(envelope.payload)}`;
105
91
  }
106
-
107
- async onModuleDestroy(): Promise<void> {
108
- if (this.subscription) {
109
- await this.subscription.unsubscribe();
110
- this.subscription = null;
111
- }
112
- }
113
92
  }
@@ -1,12 +1,11 @@
1
1
  import { Module } from '@onebun/core';
2
- import { ConsumerService } from './consumer.service';
3
- import { NatsStreamsModule } from '../nats-streams/nats-streams.module';
2
+ import { ConsumerController } from './consumer.controller';
4
3
  import { PreHandlersModule } from '../pre-handlers/pre-handlers.module';
5
4
  import { GatewayClientModule } from '../gateway/gateway-client.module';
6
5
  import { PendingModule } from '../pending/pending.module';
7
6
 
8
7
  @Module({
9
- imports: [NatsStreamsModule, PreHandlersModule, GatewayClientModule, PendingModule],
10
- providers: [ConsumerService],
8
+ imports: [PreHandlersModule, GatewayClientModule, PendingModule],
9
+ controllers: [ConsumerController],
11
10
  })
12
11
  export class ConsumerModule {}
@@ -1,12 +1,11 @@
1
1
  import { Module } from '@onebun/core';
2
2
  import { HealthController } from './health.controller';
3
3
  import { HealthService } from './health.service';
4
- import { NatsStreamsModule } from '../nats-streams/nats-streams.module';
5
4
  import { GatewayClientModule } from '../gateway/gateway-client.module';
6
5
  import { PendingModule } from '../pending/pending.module';
7
6
 
8
7
  @Module({
9
- imports: [NatsStreamsModule, GatewayClientModule, PendingModule],
8
+ imports: [GatewayClientModule, PendingModule],
10
9
  controllers: [HealthController],
11
10
  providers: [HealthService],
12
11
  })
@@ -1,5 +1,4 @@
1
- import { Service, BaseService } from '@onebun/core';
2
- import { NatsAdapterService } from '../nats-streams/nats-adapter.service';
1
+ import { Service, BaseService, QueueService } from '@onebun/core';
3
2
  import { GatewayClientService } from '../gateway/gateway-client.service';
4
3
  import { PendingService } from '../pending/pending.service';
5
4
 
@@ -20,19 +19,27 @@ export class HealthService extends BaseService {
20
19
  private readonly startedAt = Date.now();
21
20
 
22
21
  constructor(
23
- private nats: NatsAdapterService,
22
+ private queueService: QueueService,
24
23
  private gateway: GatewayClientService,
25
24
  private pending: PendingService,
26
25
  ) {
27
26
  super();
28
27
  }
29
28
 
29
+ private isQueueConnected(): boolean {
30
+ try {
31
+ return this.queueService.getAdapter().isConnected();
32
+ } catch {
33
+ return false;
34
+ }
35
+ }
36
+
30
37
  async getStatus(): Promise<HealthStatus> {
31
38
  const pendingCount = await this.pending.countPending();
32
39
 
33
40
  return {
34
41
  nats: {
35
- connected: this.nats.isConnected(),
42
+ connected: this.isQueueConnected(),
36
43
  url: this.config.get('nats.servers'),
37
44
  },
38
45
  gateway: {
@@ -1,13 +1,36 @@
1
1
  import { OneBunApplication } from '@onebun/core';
2
+ import { JetStreamQueueAdapter, type JetStreamAdapterOptions } from '@onebun/nats';
2
3
  import { AppModule } from './app.module';
3
4
  import { envSchema } from './config';
4
5
 
6
+ const natsServers = Bun.env.NATS_SERVERS ?? 'nats://localhost:4222';
7
+ const ackWaitMs = Number(Bun.env.NATS_ACK_WAIT_MS ?? 30000);
8
+ const maxDeliver = Number(Bun.env.NATS_MAX_DELIVER ?? 3);
9
+
5
10
  const app = new OneBunApplication(AppModule, {
6
11
  development: Bun.env.NODE_ENV !== 'production',
7
12
  envSchema,
8
13
  envOptions: {
9
14
  loadDotEnv: true,
10
15
  },
16
+ queue: {
17
+ adapter: JetStreamQueueAdapter as any,
18
+ options: {
19
+ servers: natsServers,
20
+ stream: 'agent_inbound',
21
+ createStream: true,
22
+ streamConfig: {
23
+ subjects: ['agent.inbound.>'],
24
+ retention: 'workqueue',
25
+ storage: 'file',
26
+ replicas: 1,
27
+ },
28
+ consumerConfig: {
29
+ ackWait: ackWaitMs * 1_000_000, // ms → ns
30
+ maxDeliver,
31
+ },
32
+ } as JetStreamAdapterOptions,
33
+ },
11
34
  });
12
35
 
13
36
  app.start()
@@ -1,8 +1,7 @@
1
1
  import { Module } from '@onebun/core';
2
- import { NatsAdapterService } from './nats-adapter.service';
2
+ import { StreamSetupController } from './stream-setup.controller';
3
3
 
4
4
  @Module({
5
- providers: [NatsAdapterService],
6
- exports: [NatsAdapterService],
5
+ controllers: [StreamSetupController],
7
6
  })
8
7
  export class NatsStreamsModule {}
@@ -0,0 +1,64 @@
1
+ import { Controller, BaseController, OnQueueReady, QueueService } from '@onebun/core';
2
+
3
+ /**
4
+ * Ensures additional JetStream streams (agent_events, agent_dlq) exist
5
+ * once the queue adapter is connected.
6
+ */
7
+ @Controller('/nats-setup')
8
+ export class StreamSetupController extends BaseController {
9
+ constructor(private queueService: QueueService) {
10
+ super();
11
+ }
12
+
13
+ @OnQueueReady()
14
+ async onReady(): Promise<void> {
15
+ await this.ensureExtraStreams();
16
+ }
17
+
18
+ private async ensureExtraStreams(): Promise<void> {
19
+ try {
20
+ const adapter = this.queueService.getAdapter();
21
+ const nc = (adapter as any)?.client?.getConnection?.();
22
+ if (!nc) {
23
+ this.logger.warn('Cannot access NATS connection for stream creation');
24
+ return;
25
+ }
26
+
27
+ const jsModule = await import('@nats-io/jetstream');
28
+ const jsm = await jsModule.jetstreamManager(nc);
29
+
30
+ const SEVEN_DAYS_NS = 7 * 24 * 60 * 60 * 1e9;
31
+
32
+ await this.ensureStream(jsm, {
33
+ name: 'agent_events',
34
+ subjects: ['agent.events.>'],
35
+ retention: 'limits',
36
+ max_age: SEVEN_DAYS_NS,
37
+ storage: 'file',
38
+ num_replicas: 1,
39
+ });
40
+
41
+ await this.ensureStream(jsm, {
42
+ name: 'agent_dlq',
43
+ subjects: ['agent.dlq.>'],
44
+ retention: 'limits',
45
+ max_age: SEVEN_DAYS_NS,
46
+ storage: 'file',
47
+ num_replicas: 1,
48
+ });
49
+
50
+ this.logger.info('Additional JetStream streams ensured (agent_events, agent_dlq)');
51
+ } catch (err: any) {
52
+ this.logger.warn(`Failed to ensure extra streams: ${err?.message}`);
53
+ }
54
+ }
55
+
56
+ private async ensureStream(jsm: any, config: Record<string, any>): Promise<void> {
57
+ try {
58
+ await jsm.streams.info(config.name);
59
+ await jsm.streams.update(config.name, config);
60
+ } catch {
61
+ await jsm.streams.add(config);
62
+ }
63
+ }
64
+ }
@@ -1,10 +1,8 @@
1
1
  import { Module } from '@onebun/core';
2
2
  import { PublisherService } from './publisher.service';
3
3
  import { PublisherController } from './publisher.controller';
4
- import { NatsStreamsModule } from '../nats-streams/nats-streams.module';
5
4
 
6
5
  @Module({
7
- imports: [NatsStreamsModule],
8
6
  controllers: [PublisherController],
9
7
  providers: [PublisherService],
10
8
  exports: [PublisherService],
@@ -1,20 +1,15 @@
1
- import { Service, BaseService } from '@onebun/core';
2
- import { NatsAdapterService } from '../nats-streams/nats-adapter.service';
1
+ import { Service, BaseService, QueueService } from '@onebun/core';
3
2
  import { createEnvelope, type EnvelopeMeta } from './envelope';
4
3
 
5
4
  @Service()
6
5
  export class PublisherService extends BaseService {
7
- constructor(private natsAdapter: NatsAdapterService) {
6
+ constructor(private queueService: QueueService) {
8
7
  super();
9
8
  }
10
9
 
11
10
  async publish(subject: string, payload: unknown, meta?: EnvelopeMeta): Promise<void> {
12
- if (!this.natsAdapter.isConnected()) {
13
- this.logger.warn(`NATS not connected, dropping publish to ${subject}`);
14
- return;
15
- }
16
11
  const envelope = createEnvelope(subject, payload, meta);
17
- await this.natsAdapter.publish(subject, envelope);
12
+ await this.queueService.publish(subject, envelope);
18
13
  this.logger.debug(`Published to ${subject}`, { id: envelope.id });
19
14
  }
20
15
  }
@@ -1,133 +0,0 @@
1
- import { Service, BaseService, type OnModuleInit, type OnModuleDestroy } from '@onebun/core';
2
- import type { Subscription, MessageHandler, SubscribeOptions, PublishOptions } from '@onebun/core';
3
- import { JetStreamQueueAdapter, type JetStreamAdapterOptions } from '@onebun/nats';
4
-
5
- /**
6
- * Manages the JetStreamQueueAdapter lifecycle with graceful degradation.
7
- *
8
- * When NATS is unavailable, the service runs in degraded mode:
9
- * publish and subscribe operations are silently dropped with warnings.
10
- *
11
- * Also ensures additional streams (agent_events, agent_dlq) exist.
12
- */
13
- @Service()
14
- export class NatsAdapterService extends BaseService implements OnModuleInit, OnModuleDestroy {
15
- private adapter: JetStreamQueueAdapter | null = null;
16
- private _connected = false;
17
-
18
- async onModuleInit(): Promise<void> {
19
- const servers = this.config.get('nats.servers');
20
- const reconnectTimeWait = this.config.get('nats.reconnectDelayMs');
21
- const maxReconnectAttempts = this.config.get('nats.maxReconnectAttempts');
22
- const ackWaitMs = this.config.get('consumer.ackWaitMs');
23
- const maxDeliver = this.config.get('consumer.maxDeliver');
24
-
25
- const options: JetStreamAdapterOptions = {
26
- servers,
27
- maxReconnectAttempts,
28
- reconnectTimeWait,
29
- stream: 'agent_inbound',
30
- createStream: true,
31
- streamConfig: {
32
- subjects: ['agent.inbound.>'],
33
- retention: 'workqueue',
34
- storage: 'file',
35
- replicas: 1,
36
- },
37
- consumerConfig: {
38
- ackWait: ackWaitMs * 1_000_000, // ms → ns
39
- maxDeliver,
40
- },
41
- };
42
-
43
- this.adapter = new JetStreamQueueAdapter(options);
44
-
45
- try {
46
- await this.adapter.connect();
47
- this._connected = true;
48
- await this.ensureExtraStreams();
49
- this.logger.info('NATS JetStream connected');
50
- } catch (err: any) {
51
- this.logger.warn(`NATS connection failed, running in degraded mode: ${err?.message}`);
52
- this.adapter = null;
53
- this._connected = false;
54
- }
55
- }
56
-
57
- isConnected(): boolean {
58
- return this._connected && this.adapter !== null;
59
- }
60
-
61
- async publish<T>(pattern: string, data: T, options?: PublishOptions): Promise<string | null> {
62
- if (!this.adapter || !this.isConnected()) {
63
- this.logger.warn(`NATS not connected, dropping publish to ${pattern}`);
64
- return null;
65
- }
66
- return await this.adapter.publish(pattern, data, options);
67
- }
68
-
69
- async subscribe<T>(
70
- pattern: string,
71
- handler: MessageHandler<T>,
72
- options?: SubscribeOptions,
73
- ): Promise<Subscription | null> {
74
- if (!this.adapter || !this.isConnected()) {
75
- this.logger.warn(`NATS not connected, cannot subscribe to ${pattern}`);
76
- return null;
77
- }
78
- return await this.adapter.subscribe(pattern, handler, options);
79
- }
80
-
81
- async onModuleDestroy(): Promise<void> {
82
- if (this.adapter) {
83
- await this.adapter.disconnect();
84
- this.logger.info('NATS JetStream disconnected');
85
- }
86
- }
87
-
88
- private async ensureExtraStreams(): Promise<void> {
89
- try {
90
- const nc = (this.adapter as any)?.client?.getConnection?.();
91
- if (!nc) {
92
- this.logger.warn('Cannot access NATS connection for stream creation');
93
- return;
94
- }
95
-
96
- const jsModule = await import('@nats-io/jetstream');
97
- const jsm = await jsModule.jetstreamManager(nc);
98
-
99
- const SEVEN_DAYS_NS = 7 * 24 * 60 * 60 * 1e9;
100
-
101
- await this.ensureStream(jsm, {
102
- name: 'agent_events',
103
- subjects: ['agent.events.>'],
104
- retention: 'limits',
105
- max_age: SEVEN_DAYS_NS,
106
- storage: 'file',
107
- num_replicas: 1,
108
- });
109
-
110
- await this.ensureStream(jsm, {
111
- name: 'agent_dlq',
112
- subjects: ['agent.dlq.>'],
113
- retention: 'limits',
114
- max_age: SEVEN_DAYS_NS,
115
- storage: 'file',
116
- num_replicas: 1,
117
- });
118
-
119
- this.logger.info('Additional JetStream streams ensured (agent_events, agent_dlq)');
120
- } catch (err: any) {
121
- this.logger.warn(`Failed to ensure extra streams: ${err?.message}`);
122
- }
123
- }
124
-
125
- private async ensureStream(jsm: any, config: Record<string, any>): Promise<void> {
126
- try {
127
- await jsm.streams.info(config.name);
128
- await jsm.streams.update(config.name, config);
129
- } catch {
130
- await jsm.streams.add(config);
131
- }
132
- }
133
- }