@omnixal/openclaw-nats-plugin 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/package.json +3 -2
  2. package/plugins/nats-context-engine/http-handler.ts +1 -1
  3. package/plugins/nats-context-engine/index.ts +59 -0
  4. package/sidecar/bun.lock +8 -6
  5. package/sidecar/package.json +4 -4
  6. package/sidecar/src/app.module.ts +9 -2
  7. package/sidecar/src/consumer/consumer.controller.ts +4 -0
  8. package/sidecar/src/consumer/consumer.module.ts +2 -1
  9. package/sidecar/src/db/migrations/0004_familiar_zaladane.sql +17 -0
  10. package/sidecar/src/db/migrations/meta/0004_snapshot.json +306 -0
  11. package/sidecar/src/db/migrations/meta/_journal.json +7 -0
  12. package/sidecar/src/db/schema.ts +20 -0
  13. package/sidecar/src/health/health.service.ts +1 -1
  14. package/sidecar/src/index.ts +6 -0
  15. package/sidecar/src/metrics/metrics.controller.ts +16 -0
  16. package/sidecar/src/metrics/metrics.module.ts +10 -0
  17. package/sidecar/src/metrics/metrics.service.ts +64 -0
  18. package/sidecar/src/publisher/publisher.module.ts +2 -0
  19. package/sidecar/src/publisher/publisher.service.ts +6 -1
  20. package/sidecar/src/router/router.controller.ts +20 -12
  21. package/sidecar/src/router/router.repository.ts +39 -7
  22. package/sidecar/src/router/router.service.ts +10 -2
  23. package/sidecar/src/scheduler/scheduler.controller.ts +68 -0
  24. package/sidecar/src/scheduler/scheduler.module.ts +13 -0
  25. package/sidecar/src/scheduler/scheduler.repository.ts +64 -0
  26. package/sidecar/src/scheduler/scheduler.service.ts +138 -0
  27. package/sidecar/src/validation/schemas.ts +18 -0
  28. package/skills/nats-events/SKILL.md +41 -28
  29. package/dashboard/dist/assets/index--UFIkwvP.js +0 -2
  30. package/dashboard/dist/assets/index-CafgidIc.css +0 -2
  31. package/dashboard/dist/index.html +0 -13
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnixal/openclaw-nats-plugin",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "NATS JetStream event-driven plugin for OpenClaw",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -10,7 +10,8 @@
10
10
  },
11
11
  "scripts": {
12
12
  "build:dashboard": "cd dashboard && bun run build",
13
- "prepublishOnly": "bun run build:dashboard"
13
+ "prepublishOnly": "bun run build:dashboard",
14
+ "nats": "docker run -d --name nats -p 4222:4222 nats:2.10-alpine -js"
14
15
  },
15
16
  "files": [
16
17
  "index.ts",
@@ -55,7 +55,7 @@ async function proxyToSidecar(
55
55
  }
56
56
 
57
57
  let body: string | undefined;
58
- if (req.method === 'POST' || req.method === 'PUT') {
58
+ if (req.method === 'POST' || req.method === 'PUT' || req.method === 'PATCH') {
59
59
  body = await readBody(req);
60
60
  }
61
61
 
@@ -172,6 +172,65 @@ export default function (api: any) {
172
172
  },
173
173
  });
174
174
 
175
+ // ── Cron Scheduler Tools ────────────────────────────────────────────
176
+
177
+ api.registerTool({
178
+ name: 'nats_cron_add',
179
+ description: 'Create or update a scheduled cron job that publishes a NATS event on a schedule. No LLM wake — fires directly.',
180
+ parameters: {
181
+ type: 'object',
182
+ properties: {
183
+ name: { type: 'string', description: 'Unique job name (e.g., daily-report, hourly-check)' },
184
+ cron: { type: 'string', description: 'Cron expression (e.g., "0 9 * * *" for daily at 9am)' },
185
+ subject: { type: 'string', description: 'NATS subject to publish (must start with agent.events.)' },
186
+ payload: { type: 'object', description: 'Event payload data' },
187
+ timezone: { type: 'string', description: 'Timezone (default: UTC). e.g., Europe/Moscow' },
188
+ },
189
+ required: ['name', 'cron', 'subject'],
190
+ },
191
+ async execute(_id: string, params: any) {
192
+ const result = await sidecarFetch('/api/cron', {
193
+ method: 'POST',
194
+ body: JSON.stringify({
195
+ name: params.name,
196
+ cron: params.cron,
197
+ subject: params.subject,
198
+ payload: params.payload ?? {},
199
+ timezone: params.timezone,
200
+ }),
201
+ });
202
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
203
+ },
204
+ });
205
+
206
+ api.registerTool({
207
+ name: 'nats_cron_remove',
208
+ description: 'Remove a scheduled cron job by name.',
209
+ parameters: {
210
+ type: 'object',
211
+ properties: {
212
+ name: { type: 'string', description: 'Job name to remove' },
213
+ },
214
+ required: ['name'],
215
+ },
216
+ async execute(_id: string, params: any) {
217
+ const result = await sidecarFetch(`/api/cron/${encodeURIComponent(params.name)}`, {
218
+ method: 'DELETE',
219
+ });
220
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
221
+ },
222
+ });
223
+
224
+ api.registerTool({
225
+ name: 'nats_cron_list',
226
+ description: 'List all scheduled cron jobs with their next run time and status.',
227
+ parameters: { type: 'object', properties: {} },
228
+ async execute() {
229
+ const result = await sidecarFetch('/api/cron');
230
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
231
+ },
232
+ });
233
+
175
234
  // ── Dashboard UI ─────────────────────────────────────────────────
176
235
 
177
236
  api.registerHttpRoute({
package/sidecar/bun.lock CHANGED
@@ -5,11 +5,11 @@
5
5
  "": {
6
6
  "name": "@ai-entrepreneur/nats-sidecar",
7
7
  "dependencies": {
8
- "@onebun/core": "^0.2.12",
8
+ "@onebun/core": "^0.2.14",
9
9
  "@onebun/drizzle": "^0.2.4",
10
- "@onebun/envs": "^0.2.1",
10
+ "@onebun/envs": "^0.2.2",
11
11
  "@onebun/logger": "^0.2.1",
12
- "@onebun/nats": "^0.2.5",
12
+ "@onebun/nats": "^0.2.6",
13
13
  "arktype": "^2.2.0",
14
14
  "ulid": "^2.3.0",
15
15
  },
@@ -104,17 +104,17 @@
104
104
 
105
105
  "@nats-io/transport-node": ["@nats-io/transport-node@3.3.1", "", { "dependencies": { "@nats-io/nats-core": "3.3.1", "@nats-io/nkeys": "2.0.3", "@nats-io/nuid": "2.0.3" } }, "sha512-GBvY0VcvyQEILgy5bjpqU1GpDYmSF06bW59I7cewZuNGS9u3AoV/gf+a+3ep45T/Z+UC661atq/b7x+QV12w+Q=="],
106
106
 
107
- "@onebun/core": ["@onebun/core@0.2.12", "", { "dependencies": { "@onebun/envs": "^0.2.1", "@onebun/logger": "^0.2.1", "@onebun/metrics": "^0.2.2", "@onebun/requests": "^0.2.1", "@onebun/trace": "^0.2.1", "arktype": "^2.0.0", "effect": "^3.13.10" }, "peerDependencies": { "testcontainers": ">=10.0.0" }, "optionalPeers": ["testcontainers"] }, "sha512-Vy3A0pmp/eZjwgEgdZApMKHPJ697LvQETmqEWyUqUr01zcfy0HYI9KcRBRIHlGoajAYghwH+D9w4N3Ip6pGCcA=="],
107
+ "@onebun/core": ["@onebun/core@0.2.14", "", { "dependencies": { "@onebun/envs": "^0.2.2", "@onebun/logger": "^0.2.1", "@onebun/metrics": "^0.2.2", "@onebun/requests": "^0.2.1", "@onebun/trace": "^0.2.1", "arktype": "^2.0.0", "effect": "^3.13.10" }, "peerDependencies": { "testcontainers": ">=10.0.0" }, "optionalPeers": ["testcontainers"] }, "sha512-uClu4Oez18y6BldubdE6R/I02Brrk5eXCoxxPatgRJ9qYRM+jKSTNmeZVoqqo7ai/rMYM9lJ1WuF9d7p6/RtDA=="],
108
108
 
109
109
  "@onebun/drizzle": ["@onebun/drizzle@0.2.4", "", { "dependencies": { "@onebun/envs": "^0.2.1", "@onebun/logger": "^0.2.1", "arktype": "^2.0.0", "drizzle-arktype": "^0.1.3", "drizzle-kit": "^0.31.6", "drizzle-orm": "^0.44.7", "effect": "^3.13.10" }, "peerDependencies": { "@onebun/core": ">=0.2.0" }, "bin": { "onebun-drizzle": "bin/drizzle-kit.ts" } }, "sha512-LbkW2hU9pTKZU/rlrHNdwhI4jYoMl+v22c3G2zc0L0aW77nW7ZCfp5YqOJYufWJbfOTSWEnNOVZQXMueYhBxsA=="],
110
110
 
111
- "@onebun/envs": ["@onebun/envs@0.2.1", "", { "dependencies": { "effect": "^3.13.10" } }, "sha512-kiXJcA4ct194+aNJK8zkrVuaAgPPVpTkcW8tJU9XN9KOh8003lENOOuUZUcieMCxdMWUgo08lp9UgiwawLan+Q=="],
111
+ "@onebun/envs": ["@onebun/envs@0.2.2", "", { "dependencies": { "effect": "^3.13.10" } }, "sha512-WIjc1LpGnecYArSWsZhheyUSYJlo+iz9SA7ZfIXQnt1vkLd7ILCmVCtODBvqG9Mh86CMmromf1lCyRkjNZyLoA=="],
112
112
 
113
113
  "@onebun/logger": ["@onebun/logger@0.2.1", "", { "dependencies": { "effect": "^3.13.10" } }, "sha512-u/zirsUSGBfbjVv274qqIHG5jzPBWY3vl8HzM6hjzsMCCpExgstkQiP1eP9rF1isIzhetwmyfBpYYc9eYsbrrw=="],
114
114
 
115
115
  "@onebun/metrics": ["@onebun/metrics@0.2.2", "", { "dependencies": { "@onebun/requests": "^0.2.1", "effect": "^3.13.10", "prom-client": "^15.1.3" } }, "sha512-8oN74MZeaWyyPHi5H3pZyY0V3cM8ORupHe2fR0gGYsZqHyM4S9UJStV29rNQlvhyesHzL7p5x3Ux6n/SRYBszw=="],
116
116
 
117
- "@onebun/nats": ["@onebun/nats@0.2.5", "", { "dependencies": { "@nats-io/jetstream": "^3.0.0-31", "@nats-io/transport-node": "^3.0.0-31", "effect": "^3.13.10" }, "peerDependencies": { "@onebun/core": ">=0.2.0" } }, "sha512-J+p9UPJGqBLeeBY6n50E6at1lyA+xmg+wzqw1HwN516t+XHzctmnrB8S5PmX/3sQI18fktFcJgAyWJfQA1piDA=="],
117
+ "@onebun/nats": ["@onebun/nats@0.2.6", "", { "dependencies": { "@nats-io/jetstream": "^3.0.0-31", "@nats-io/transport-node": "^3.0.0-31", "effect": "^3.13.10" }, "peerDependencies": { "@onebun/core": ">=0.2.0" } }, "sha512-b7b0PUu0eGDLPbstOVjaCW0GCKsBzQb8qyYQmjKPu6RCsm02qvUU8HWDytoy01mYnQxWpI93qVogGiQKd6Ps4A=="],
118
118
 
119
119
  "@onebun/requests": ["@onebun/requests@0.2.1", "", { "dependencies": { "effect": "^3.13.10" } }, "sha512-Wit+o3zRiuZOM7O0nAJ0rpFVLgkypJ1UR5uuHi0IZuiCvGmxv+Vus2+QqHoCL141L7SPO1Xlywt8dVqqu4NP7w=="],
120
120
 
@@ -446,6 +446,8 @@
446
446
 
447
447
  "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
448
448
 
449
+ "@onebun/drizzle/@onebun/envs": ["@onebun/envs@0.2.1", "", { "dependencies": { "effect": "^3.13.10" } }, "sha512-kiXJcA4ct194+aNJK8zkrVuaAgPPVpTkcW8tJU9XN9KOh8003lENOOuUZUcieMCxdMWUgo08lp9UgiwawLan+Q=="],
450
+
449
451
  "bcrypt-pbkdf/tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="],
450
452
 
451
453
  "bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
@@ -11,14 +11,14 @@
11
11
  "typecheck": "bunx tsc --noEmit",
12
12
  "db:generate": "bunx onebun-drizzle generate",
13
13
  "db:push": "bunx onebun-drizzle push",
14
- "db:studio": "bunx onebun-drizzle studio"
14
+ "db:studio": "bunx onebun-drizzle studio",
15
15
  },
16
16
  "dependencies": {
17
- "@onebun/core": "^0.2.12",
17
+ "@onebun/core": "^0.2.14",
18
18
  "@onebun/drizzle": "^0.2.4",
19
- "@onebun/envs": "^0.2.1",
19
+ "@onebun/envs": "^0.2.2",
20
20
  "@onebun/logger": "^0.2.1",
21
- "@onebun/nats": "^0.2.5",
21
+ "@onebun/nats": "^0.2.6",
22
22
  "arktype": "^2.2.0",
23
23
  "ulid": "^2.3.0"
24
24
  },
@@ -1,5 +1,6 @@
1
- import { Module } from '@onebun/core';
1
+ import { getConfig, Module } from '@onebun/core';
2
2
  import { DrizzleModule, DatabaseType } from '@onebun/drizzle';
3
+ import { envSchema, type AppConfig } from './config';
3
4
  import { DedupModule } from './dedup/dedup.module';
4
5
  import { PublisherModule } from './publisher/publisher.module';
5
6
  import { PreHandlersModule } from './pre-handlers/pre-handlers.module';
@@ -8,6 +9,10 @@ import { ConsumerModule } from './consumer/consumer.module';
8
9
  import { PendingModule } from './pending/pending.module';
9
10
  import { HealthModule } from './health/health.module';
10
11
  import { RouterModule } from './router/router.module';
12
+ import { SchedulerModule } from './scheduler/scheduler.module';
13
+ import { MetricsModule } from './metrics/metrics.module';
14
+
15
+ const config = getConfig<AppConfig>(envSchema);
11
16
 
12
17
  @Module({
13
18
  imports: [
@@ -15,7 +20,7 @@ import { RouterModule } from './router/router.module';
15
20
  connection: {
16
21
  type: DatabaseType.SQLITE,
17
22
  options: {
18
- url: process.env.DB_PATH ?? './data/nats-sidecar.db',
23
+ url: config.get('database.url'),
19
24
  },
20
25
  },
21
26
  migrationsFolder: './src/db/migrations',
@@ -28,6 +33,8 @@ import { RouterModule } from './router/router.module';
28
33
  PendingModule,
29
34
  HealthModule,
30
35
  RouterModule,
36
+ SchedulerModule,
37
+ MetricsModule,
31
38
  ],
32
39
  })
33
40
  export class AppModule {}
@@ -3,6 +3,7 @@ import { PipelineService } from '../pre-handlers/pipeline.service';
3
3
  import { GatewayClientService } from '../gateway/gateway-client.service';
4
4
  import { PendingService } from '../pending/pending.service';
5
5
  import { RouterService } from '../router/router.service';
6
+ import { MetricsService } from '../metrics/metrics.service';
6
7
  import type { NatsEventEnvelope } from '../publisher/envelope';
7
8
 
8
9
  @Controller('/consumer')
@@ -12,6 +13,7 @@ export class ConsumerController extends BaseController {
12
13
  private gatewayClient: GatewayClientService,
13
14
  private pendingService: PendingService,
14
15
  private routerService: RouterService,
16
+ private metrics: MetricsService,
15
17
  ) {
16
18
  super();
17
19
  }
@@ -57,6 +59,8 @@ export class ConsumerController extends BaseController {
57
59
  priority: (ctx.enrichments['priority'] as number) ?? envelope.meta?.priority ?? 5,
58
60
  },
59
61
  });
62
+ await this.routerService.recordDelivery(route.id, envelope.subject);
63
+ this.metrics.recordConsume(envelope.subject);
60
64
  }
61
65
  await message.ack();
62
66
  } else {
@@ -3,9 +3,10 @@ import { ConsumerController } from './consumer.controller';
3
3
  import { PreHandlersModule } from '../pre-handlers/pre-handlers.module';
4
4
  import { PendingModule } from '../pending/pending.module';
5
5
  import { RouterModule } from '../router/router.module';
6
+ import { MetricsModule } from '../metrics/metrics.module';
6
7
 
7
8
  @Module({
8
- imports: [PreHandlersModule, PendingModule, RouterModule],
9
+ imports: [PreHandlersModule, PendingModule, RouterModule, MetricsModule],
9
10
  controllers: [ConsumerController],
10
11
  })
11
12
  export class ConsumerModule {}
@@ -0,0 +1,17 @@
1
+ CREATE TABLE `cron_jobs` (
2
+ `id` text PRIMARY KEY NOT NULL,
3
+ `name` text NOT NULL,
4
+ `expr` text NOT NULL,
5
+ `subject` text NOT NULL,
6
+ `payload` text,
7
+ `timezone` text DEFAULT 'UTC' NOT NULL,
8
+ `enabled` integer DEFAULT true NOT NULL,
9
+ `last_run_at` integer,
10
+ `created_at` integer NOT NULL
11
+ );
12
+ --> statement-breakpoint
13
+ CREATE UNIQUE INDEX `cron_jobs_name_unique` ON `cron_jobs` (`name`);--> statement-breakpoint
14
+ CREATE INDEX `cron_jobs_name_idx` ON `cron_jobs` (`name`);--> statement-breakpoint
15
+ ALTER TABLE `event_routes` ADD `last_delivered_at` integer;--> statement-breakpoint
16
+ ALTER TABLE `event_routes` ADD `last_event_subject` text;--> statement-breakpoint
17
+ ALTER TABLE `event_routes` ADD `delivery_count` integer DEFAULT 0 NOT NULL;
@@ -0,0 +1,306 @@
1
+ {
2
+ "version": "6",
3
+ "dialect": "sqlite",
4
+ "id": "e31f4610-cdfc-459e-b947-8363975599a0",
5
+ "prevId": "cb92737c-c257-4b58-bbf0-798ab1494961",
6
+ "tables": {
7
+ "cron_jobs": {
8
+ "name": "cron_jobs",
9
+ "columns": {
10
+ "id": {
11
+ "name": "id",
12
+ "type": "text",
13
+ "primaryKey": true,
14
+ "notNull": true,
15
+ "autoincrement": false
16
+ },
17
+ "name": {
18
+ "name": "name",
19
+ "type": "text",
20
+ "primaryKey": false,
21
+ "notNull": true,
22
+ "autoincrement": false
23
+ },
24
+ "expr": {
25
+ "name": "expr",
26
+ "type": "text",
27
+ "primaryKey": false,
28
+ "notNull": true,
29
+ "autoincrement": false
30
+ },
31
+ "subject": {
32
+ "name": "subject",
33
+ "type": "text",
34
+ "primaryKey": false,
35
+ "notNull": true,
36
+ "autoincrement": false
37
+ },
38
+ "payload": {
39
+ "name": "payload",
40
+ "type": "text",
41
+ "primaryKey": false,
42
+ "notNull": false,
43
+ "autoincrement": false
44
+ },
45
+ "timezone": {
46
+ "name": "timezone",
47
+ "type": "text",
48
+ "primaryKey": false,
49
+ "notNull": true,
50
+ "autoincrement": false,
51
+ "default": "'UTC'"
52
+ },
53
+ "enabled": {
54
+ "name": "enabled",
55
+ "type": "integer",
56
+ "primaryKey": false,
57
+ "notNull": true,
58
+ "autoincrement": false,
59
+ "default": true
60
+ },
61
+ "last_run_at": {
62
+ "name": "last_run_at",
63
+ "type": "integer",
64
+ "primaryKey": false,
65
+ "notNull": false,
66
+ "autoincrement": false
67
+ },
68
+ "created_at": {
69
+ "name": "created_at",
70
+ "type": "integer",
71
+ "primaryKey": false,
72
+ "notNull": true,
73
+ "autoincrement": false
74
+ }
75
+ },
76
+ "indexes": {
77
+ "cron_jobs_name_unique": {
78
+ "name": "cron_jobs_name_unique",
79
+ "columns": [
80
+ "name"
81
+ ],
82
+ "isUnique": true
83
+ },
84
+ "cron_jobs_name_idx": {
85
+ "name": "cron_jobs_name_idx",
86
+ "columns": [
87
+ "name"
88
+ ],
89
+ "isUnique": false
90
+ }
91
+ },
92
+ "foreignKeys": {},
93
+ "compositePrimaryKeys": {},
94
+ "uniqueConstraints": {},
95
+ "checkConstraints": {}
96
+ },
97
+ "dedup_events": {
98
+ "name": "dedup_events",
99
+ "columns": {
100
+ "event_id": {
101
+ "name": "event_id",
102
+ "type": "text",
103
+ "primaryKey": true,
104
+ "notNull": true,
105
+ "autoincrement": false
106
+ },
107
+ "subject": {
108
+ "name": "subject",
109
+ "type": "text",
110
+ "primaryKey": false,
111
+ "notNull": true,
112
+ "autoincrement": false
113
+ },
114
+ "seen_at": {
115
+ "name": "seen_at",
116
+ "type": "integer",
117
+ "primaryKey": false,
118
+ "notNull": true,
119
+ "autoincrement": false
120
+ }
121
+ },
122
+ "indexes": {
123
+ "dedup_events_seen_at_idx": {
124
+ "name": "dedup_events_seen_at_idx",
125
+ "columns": [
126
+ "seen_at"
127
+ ],
128
+ "isUnique": false
129
+ }
130
+ },
131
+ "foreignKeys": {},
132
+ "compositePrimaryKeys": {},
133
+ "uniqueConstraints": {},
134
+ "checkConstraints": {}
135
+ },
136
+ "event_routes": {
137
+ "name": "event_routes",
138
+ "columns": {
139
+ "id": {
140
+ "name": "id",
141
+ "type": "text",
142
+ "primaryKey": true,
143
+ "notNull": true,
144
+ "autoincrement": false
145
+ },
146
+ "pattern": {
147
+ "name": "pattern",
148
+ "type": "text",
149
+ "primaryKey": false,
150
+ "notNull": true,
151
+ "autoincrement": false
152
+ },
153
+ "target": {
154
+ "name": "target",
155
+ "type": "text",
156
+ "primaryKey": false,
157
+ "notNull": true,
158
+ "autoincrement": false,
159
+ "default": "'main'"
160
+ },
161
+ "enabled": {
162
+ "name": "enabled",
163
+ "type": "integer",
164
+ "primaryKey": false,
165
+ "notNull": true,
166
+ "autoincrement": false,
167
+ "default": true
168
+ },
169
+ "priority": {
170
+ "name": "priority",
171
+ "type": "integer",
172
+ "primaryKey": false,
173
+ "notNull": true,
174
+ "autoincrement": false,
175
+ "default": 5
176
+ },
177
+ "created_at": {
178
+ "name": "created_at",
179
+ "type": "integer",
180
+ "primaryKey": false,
181
+ "notNull": true,
182
+ "autoincrement": false
183
+ },
184
+ "last_delivered_at": {
185
+ "name": "last_delivered_at",
186
+ "type": "integer",
187
+ "primaryKey": false,
188
+ "notNull": false,
189
+ "autoincrement": false
190
+ },
191
+ "last_event_subject": {
192
+ "name": "last_event_subject",
193
+ "type": "text",
194
+ "primaryKey": false,
195
+ "notNull": false,
196
+ "autoincrement": false
197
+ },
198
+ "delivery_count": {
199
+ "name": "delivery_count",
200
+ "type": "integer",
201
+ "primaryKey": false,
202
+ "notNull": true,
203
+ "autoincrement": false,
204
+ "default": 0
205
+ }
206
+ },
207
+ "indexes": {
208
+ "event_routes_pattern_unique": {
209
+ "name": "event_routes_pattern_unique",
210
+ "columns": [
211
+ "pattern"
212
+ ],
213
+ "isUnique": true
214
+ },
215
+ "event_routes_pattern_idx": {
216
+ "name": "event_routes_pattern_idx",
217
+ "columns": [
218
+ "pattern"
219
+ ],
220
+ "isUnique": false
221
+ },
222
+ "event_routes_target_idx": {
223
+ "name": "event_routes_target_idx",
224
+ "columns": [
225
+ "target"
226
+ ],
227
+ "isUnique": false
228
+ }
229
+ },
230
+ "foreignKeys": {},
231
+ "compositePrimaryKeys": {},
232
+ "uniqueConstraints": {},
233
+ "checkConstraints": {}
234
+ },
235
+ "pending_events": {
236
+ "name": "pending_events",
237
+ "columns": {
238
+ "id": {
239
+ "name": "id",
240
+ "type": "text",
241
+ "primaryKey": true,
242
+ "notNull": true,
243
+ "autoincrement": false
244
+ },
245
+ "session_key": {
246
+ "name": "session_key",
247
+ "type": "text",
248
+ "primaryKey": false,
249
+ "notNull": true,
250
+ "autoincrement": false
251
+ },
252
+ "subject": {
253
+ "name": "subject",
254
+ "type": "text",
255
+ "primaryKey": false,
256
+ "notNull": true,
257
+ "autoincrement": false
258
+ },
259
+ "payload": {
260
+ "name": "payload",
261
+ "type": "text",
262
+ "primaryKey": false,
263
+ "notNull": false,
264
+ "autoincrement": false
265
+ },
266
+ "priority": {
267
+ "name": "priority",
268
+ "type": "integer",
269
+ "primaryKey": false,
270
+ "notNull": true,
271
+ "autoincrement": false,
272
+ "default": 5
273
+ },
274
+ "created_at": {
275
+ "name": "created_at",
276
+ "type": "integer",
277
+ "primaryKey": false,
278
+ "notNull": true,
279
+ "autoincrement": false
280
+ },
281
+ "delivered_at": {
282
+ "name": "delivered_at",
283
+ "type": "integer",
284
+ "primaryKey": false,
285
+ "notNull": false,
286
+ "autoincrement": false
287
+ }
288
+ },
289
+ "indexes": {},
290
+ "foreignKeys": {},
291
+ "compositePrimaryKeys": {},
292
+ "uniqueConstraints": {},
293
+ "checkConstraints": {}
294
+ }
295
+ },
296
+ "views": {},
297
+ "enums": {},
298
+ "_meta": {
299
+ "schemas": {},
300
+ "tables": {},
301
+ "columns": {}
302
+ },
303
+ "internal": {
304
+ "indexes": {}
305
+ }
306
+ }
@@ -29,6 +29,13 @@
29
29
  "when": 1773938595817,
30
30
  "tag": "0003_wet_deathbird",
31
31
  "breakpoints": true
32
+ },
33
+ {
34
+ "idx": 4,
35
+ "version": "6",
36
+ "when": 1773965190659,
37
+ "tag": "0004_familiar_zaladane",
38
+ "breakpoints": true
32
39
  }
33
40
  ]
34
41
  }
@@ -28,6 +28,9 @@ export const eventRoutes = sqliteTable('event_routes', {
28
28
  enabled: integer('enabled', { mode: 'boolean' }).notNull().default(true),
29
29
  priority: integer('priority').notNull().default(5),
30
30
  createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull(),
31
+ lastDeliveredAt: integer('last_delivered_at', { mode: 'timestamp_ms' }),
32
+ lastEventSubject: text('last_event_subject'),
33
+ deliveryCount: integer('delivery_count').notNull().default(0),
31
34
  }, (table) => [
32
35
  index('event_routes_pattern_idx').on(table.pattern),
33
36
  index('event_routes_target_idx').on(table.target),
@@ -35,3 +38,20 @@ export const eventRoutes = sqliteTable('event_routes', {
35
38
 
36
39
  export type DbEventRoute = typeof eventRoutes.$inferSelect;
37
40
  export type NewEventRoute = typeof eventRoutes.$inferInsert;
41
+
42
+ export const cronJobs = sqliteTable('cron_jobs', {
43
+ id: text('id').primaryKey(),
44
+ name: text('name').notNull().unique(),
45
+ expr: text('expr').notNull(),
46
+ subject: text('subject').notNull(),
47
+ payload: text('payload', { mode: 'json' }).$type<unknown>(),
48
+ timezone: text('timezone').notNull().default('UTC'),
49
+ enabled: integer('enabled', { mode: 'boolean' }).notNull().default(true),
50
+ lastRunAt: integer('last_run_at', { mode: 'timestamp_ms' }),
51
+ createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull(),
52
+ }, (table) => [
53
+ index('cron_jobs_name_idx').on(table.name),
54
+ ]);
55
+
56
+ export type DbCronJob = typeof cronJobs.$inferSelect;
57
+ export type NewCronJob = typeof cronJobs.$inferInsert;
@@ -49,7 +49,7 @@ export class HealthService extends BaseService {
49
49
  pendingCount,
50
50
  uptimeSeconds: Math.floor((Date.now() - this.startedAt) / 1000),
51
51
  config: {
52
- streams: ['agent_events', 'agent_dlq'],
52
+ streams: ['agent_events', 'agent_dlq', 'scheduler_internal'],
53
53
  consumerName: this.config.get('consumer.name'),
54
54
  dedupTtlSeconds: this.config.get('dedup.ttlSeconds'),
55
55
  },
@@ -35,6 +35,12 @@ const app = new OneBunApplication(AppModule, {
35
35
  retention: 'limits',
36
36
  maxAge: SEVEN_DAYS_NS,
37
37
  },
38
+ {
39
+ name: 'scheduler_internal',
40
+ subjects: ['scheduler.>'],
41
+ retention: 'limits',
42
+ maxAge: 24 * 60 * 60 * 1e9, // 1 day
43
+ },
38
44
  ],
39
45
  consumerConfig: {
40
46
  ackWait: ackWaitMs * 1_000_000, // ms → ns
@@ -0,0 +1,16 @@
1
+ import { Controller, BaseController, Get, UseMiddleware } from '@onebun/core';
2
+ import { ApiKeyMiddleware } from '../auth/api-key.middleware';
3
+ import { MetricsService } from './metrics.service';
4
+
5
+ @Controller('/api/metrics')
6
+ @UseMiddleware(ApiKeyMiddleware)
7
+ export class MetricsController extends BaseController {
8
+ constructor(private metrics: MetricsService) {
9
+ super();
10
+ }
11
+
12
+ @Get('/')
13
+ getAll() {
14
+ return this.success(this.metrics.getAll());
15
+ }
16
+ }
@@ -0,0 +1,10 @@
1
+ import { Module } from '@onebun/core';
2
+ import { MetricsService } from './metrics.service';
3
+ import { MetricsController } from './metrics.controller';
4
+
5
+ @Module({
6
+ controllers: [MetricsController],
7
+ providers: [MetricsService],
8
+ exports: [MetricsService],
9
+ })
10
+ export class MetricsModule {}