@wabot-dev/framework 0.9.27 → 2.0.0-beta.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.
Files changed (94) hide show
  1. package/README.md +27 -0
  2. package/bin/skills.mjs +151 -0
  3. package/bin/wabot-skills.mjs +120 -0
  4. package/dist/build/build.js +1031 -8
  5. package/dist/src/addon/chat-bot/in-memory/InMemoryChatMemory.js +1 -3
  6. package/dist/src/addon/chat-bot/xai/XAIChatAdapter.js +180 -0
  7. package/dist/src/addon/chat-controller/cmd/cmdChannelSocketPath.js +1 -5
  8. package/dist/src/addon/chat-controller/hubspot/@hubspot.js +28 -0
  9. package/dist/src/addon/chat-controller/hubspot/HubSpotChannel.js +81 -0
  10. package/dist/src/addon/chat-controller/hubspot/HubSpotChannelConfig.js +20 -0
  11. package/dist/src/addon/chat-controller/hubspot/HubSpotReceiver.js +42 -0
  12. package/dist/src/addon/chat-controller/hubspot/HubSpotSender.js +118 -0
  13. package/dist/src/addon/chat-controller/hubspot/HubSpotWebhookController.js +122 -0
  14. package/dist/src/addon/chat-controller/hubspot/downloadHubSpotAttachments.js +45 -0
  15. package/dist/src/addon/chat-controller/hubspot/hubspotChannelName.js +3 -0
  16. package/dist/src/addon/chat-controller/hubspot/verifyHubSpotSignatureV3.js +28 -0
  17. package/dist/src/addon/chat-controller/{telegram/markdownToTelegramHtml.js → markdown/markdownToChatHtml.js} +5 -8
  18. package/dist/src/addon/chat-controller/slack/@slack.js +22 -0
  19. package/dist/src/addon/chat-controller/slack/SlackChannel.js +187 -0
  20. package/dist/src/addon/chat-controller/slack/SlackChannelConfig.js +12 -0
  21. package/dist/src/addon/chat-controller/slack/markdownToSlackMrkdwn.js +38 -0
  22. package/dist/src/addon/chat-controller/slack/slackChannelName.js +3 -0
  23. package/dist/src/addon/chat-controller/telegram/TelegramChannel.js +2 -2
  24. package/dist/src/addon/ui/preact/PreactRenderer.js +86 -0
  25. package/dist/src/addon/ui/preact/outlet.js +22 -0
  26. package/dist/src/addon/ui/preact/preactClientRuntime.js +67 -0
  27. package/dist/src/core/repository/CrudRepository.js +7 -7
  28. package/dist/src/feature/async/computeDedupKey.js +1 -1
  29. package/dist/src/feature/chat-controller/runChatControllers.js +4 -1
  30. package/dist/src/feature/pg/@pgExtension.js +2 -4
  31. package/dist/src/feature/project-runner/ProjectRunner.js +62 -10
  32. package/dist/src/feature/project-runner/scanner.js +1 -1
  33. package/dist/src/feature/repository/@memExtension.js +1 -2
  34. package/dist/src/feature/rest-controller/runRestControllers.js +11 -6
  35. package/dist/src/feature/ui-controller/actions.js +35 -0
  36. package/dist/src/feature/ui-controller/bundler/UiBundler.js +191 -0
  37. package/dist/src/feature/ui-controller/bundler/devMiddleware.js +41 -0
  38. package/dist/src/feature/ui-controller/bundler/index.js +4 -0
  39. package/dist/src/feature/ui-controller/bundler/manifest.js +34 -0
  40. package/dist/src/feature/ui-controller/bundler/navRuntime.js +236 -0
  41. package/dist/src/feature/ui-controller/bundler/pageAssets.js +30 -0
  42. package/dist/src/feature/ui-controller/document/escape.js +17 -0
  43. package/dist/src/feature/ui-controller/document/helpers.js +13 -0
  44. package/dist/src/feature/ui-controller/document/renderDocument.js +43 -0
  45. package/dist/src/feature/ui-controller/island/IslandRegistry.js +68 -0
  46. package/dist/src/feature/ui-controller/island/island.js +40 -0
  47. package/dist/src/feature/ui-controller/island/serialize.js +35 -0
  48. package/dist/src/feature/ui-controller/metadata/@action.js +18 -0
  49. package/dist/src/feature/ui-controller/metadata/@uiController.js +19 -0
  50. package/dist/src/feature/ui-controller/metadata/@uiMiddleware.js +20 -0
  51. package/dist/src/feature/ui-controller/metadata/@view.js +18 -0
  52. package/dist/src/feature/ui-controller/metadata/UiControllerMetadataStore.js +107 -0
  53. package/dist/src/feature/ui-controller/renderer/UiRendererRegistry.js +42 -0
  54. package/dist/src/feature/ui-controller/runUiControllers.js +285 -0
  55. package/dist/src/index.d.ts +640 -3
  56. package/dist/src/index.js +32 -3
  57. package/dist/src/testing/LlmJudge.js +93 -0
  58. package/dist/src/testing/MockChatAdapter.js +68 -0
  59. package/dist/src/testing/TestChatMemory.js +73 -0
  60. package/dist/src/testing/asyncHarness.js +66 -0
  61. package/dist/src/testing/auth.js +114 -0
  62. package/dist/src/testing/chatBotHarness.js +88 -0
  63. package/dist/src/testing/chatControllerHarness.js +94 -0
  64. package/dist/src/testing/conformance/chatAdapterConformanceCases.js +656 -0
  65. package/dist/src/testing/fixtures.js +53 -0
  66. package/dist/src/testing/helpers.js +42 -0
  67. package/dist/src/testing/index.d.ts +818 -0
  68. package/dist/src/testing/index.js +14 -0
  69. package/dist/src/testing/repositories.js +34 -0
  70. package/dist/src/testing/restHarness.js +127 -0
  71. package/dist/src/testing/testImageBase64.js +5 -0
  72. package/dist/src/testing/uiHarness.js +102 -0
  73. package/dist/src/testing/validation.js +66 -0
  74. package/dist/src/ui/client.js +6 -0
  75. package/dist/src/ui/index.d.ts +427 -0
  76. package/dist/src/ui/index.js +29 -0
  77. package/dist/src/ui/jsx-dev-runtime.d.ts +1 -0
  78. package/dist/src/ui/jsx-dev-runtime.js +1 -0
  79. package/dist/src/ui/jsx-runtime.d.ts +1 -0
  80. package/dist/src/ui/jsx-runtime.js +1 -0
  81. package/package.json +48 -11
  82. package/skills/wabot-async/SKILL.md +143 -0
  83. package/skills/wabot-auth/SKILL.md +153 -0
  84. package/skills/wabot-chat/SKILL.md +140 -0
  85. package/skills/wabot-di-config/SKILL.md +117 -0
  86. package/skills/wabot-framework/SKILL.md +81 -0
  87. package/skills/wabot-framework/references/quickstart.md +85 -0
  88. package/skills/wabot-mindset/SKILL.md +159 -0
  89. package/skills/wabot-ops/SKILL.md +151 -0
  90. package/skills/wabot-persistence/SKILL.md +159 -0
  91. package/skills/wabot-rest-socket/SKILL.md +167 -0
  92. package/skills/wabot-testing/SKILL.md +214 -0
  93. package/skills/wabot-ui/SKILL.md +201 -0
  94. package/skills/wabot-validation/SKILL.md +108 -0
@@ -0,0 +1,143 @@
1
+ ---
2
+ name: wabot-async
3
+ description: Use when running background work in Wabot — fire-and-forget commands, scheduled deferred commands, cron handlers, or wrapping side-effectful methods in a DB transaction. Covers @command, @commandHandler, @cronHandler, @transaction, the Async service (runCommand, scheduleCommand, IScheduleAt), ICommandHandler / ICronHandler, and how the in-memory vs PG job/cron stores are wired by the project runner.
4
+ ---
5
+
6
+ # Async commands, cron, and transactions
7
+
8
+ Wabot's async layer handles three kinds of work:
9
+
10
+ - **Commands** — typed messages with a registered handler. Run immediately (`runCommand`) or scheduled (`scheduleCommand`).
11
+ - **Crons** — handlers that fire on a cron expression.
12
+ - **Transactions** — wrap a method so all repository writes happen inside one DB transaction per registered adapter.
13
+
14
+ The job store, cron job store, and transaction adapter are auto-selected by the project runner based on `DATABASE_URL` (in-memory ↔ PostgreSQL). No manual registration.
15
+
16
+ ## Commands
17
+
18
+ A command is a plain class decorated with `@command(name)`. Fields use the standard validators from `wabot-validation`.
19
+
20
+ ```typescript
21
+ import { command, isNumber, isString, min } from '@wabot-dev/framework'
22
+
23
+ @command('orders.charge')
24
+ export class ChargeOrder {
25
+ @isString() orderId!: string
26
+ @isNumber() @min(1) amountCents!: number
27
+ }
28
+ ```
29
+
30
+ The handler implements `ICommandHandler<C>` and is bound with `@commandHandler(Command)`:
31
+
32
+ ```typescript
33
+ import { commandHandler, ICommandHandler, injectable } from '@wabot-dev/framework'
34
+ import { ChargeOrder } from './ChargeOrder'
35
+
36
+ @commandHandler(ChargeOrder)
37
+ export class ChargeOrderHandler implements ICommandHandler<ChargeOrder> {
38
+ constructor(private payments: PaymentService) {}
39
+
40
+ async handle(cmd: ChargeOrder) {
41
+ await this.payments.charge(cmd.orderId, cmd.amountCents)
42
+ }
43
+ }
44
+ ```
45
+
46
+ Trigger it with the `Async` singleton:
47
+
48
+ ```typescript
49
+ import { Async, container } from '@wabot-dev/framework'
50
+
51
+ const async = container.resolve(Async)
52
+
53
+ // Run as soon as a worker is free
54
+ await async.runCommand(ChargeOrder, { orderId: 'o_1', amountCents: 2500 })
55
+
56
+ // Schedule for later: absolute Date or relative delay
57
+ await async.scheduleCommand(ChargeOrder, { orderId: 'o_1', amountCents: 2500 }, { minutes: 30 })
58
+ await async.scheduleCommand(ChargeOrder, { orderId: 'o_1', amountCents: 2500 }, new Date('2026-12-31T08:00:00Z'))
59
+ ```
60
+
61
+ `IScheduleAt = Date | { seconds: number } | { minutes: number } | { hours: number } | { days: number }`.
62
+
63
+ `Async.runCommand` returns a `Job` immediately (it has an `id` you can persist). The payload is validated against the command class before the job is stored — bad shapes throw `CustomError`.
64
+
65
+ `@commandHandler` applies `@injectable()`. There is at most one handler per command name; registering a second throws.
66
+
67
+ ## Cron handlers
68
+
69
+ ```typescript
70
+ import { cronHandler, ICronHandler } from '@wabot-dev/framework'
71
+
72
+ @cronHandler({ name: 'daily-cleanup', cron: '0 3 * * *' })
73
+ export class DailyCleanup implements ICronHandler {
74
+ constructor(private games: GameRepository) {}
75
+
76
+ async handle() {
77
+ // ...
78
+ }
79
+
80
+ // Optional. Called when handle() throws.
81
+ async handleError(err: any) {
82
+ // ...
83
+ }
84
+ }
85
+ ```
86
+
87
+ - `cron` accepts standard 5-field or 6-field (with seconds) cron expressions.
88
+ - `disabled: true` turns a handler off without removing it (useful for guarded rollouts).
89
+ - `@cronHandler` applies `@singleton()` automatically — do not stack lifecycle decorators.
90
+
91
+ The runner registers these into `CronScheduler`, which persists per-cron state in `CronJobRepository` so missed runs aren't replayed on restart.
92
+
93
+ ## Transactions
94
+
95
+ `@transaction(dbNames?)` wraps an async method so every adapter listed runs the call inside a transaction. Without arguments, all registered adapters participate (today there is one: the runner registers `PgTransactionAdapter` under the name `'default'` when `DATABASE_URL` is Postgres).
96
+
97
+ ```typescript
98
+ import { transaction } from '@wabot-dev/framework'
99
+
100
+ @injectable()
101
+ export class Checkout {
102
+ @transaction()
103
+ async checkout(orderId: string) {
104
+ await this.orderRepository.update(...)
105
+ await this.paymentRepository.create(...)
106
+ }
107
+ }
108
+ ```
109
+
110
+ Behavior:
111
+ - Repository writes performed inside the method go to the same client/connection.
112
+ - Throwing rolls back; returning commits.
113
+ - Nested `@transaction`-decorated calls reuse the outer transaction (no nested begin).
114
+ - Without Postgres no adapter is registered, so `@transaction()` just executes the method (no wrap). Naming a specific adapter (e.g. `@transaction(['default'])`) throws at call time if it isn't registered — never write `@transaction(['pg'])`.
115
+
116
+ ## Manual run / stop
117
+
118
+ In production the runner discovers and starts everything. For testing or custom hosts:
119
+
120
+ ```typescript
121
+ import { runCommandHandlers, runCronHandlers, stopCommandHandlers, stopCronHandlers } from '@wabot-dev/framework'
122
+
123
+ runCommandHandlers([ChargeOrderHandler])
124
+ runCronHandlers([DailyCleanup])
125
+
126
+ // later
127
+ stopCommandHandlers([ChargeOrderHandler])
128
+ stopCronHandlers([DailyCleanup])
129
+ ```
130
+
131
+ `testAsync` is exported for integration tests against the real job/cron schedulers — see `wabot-ts/src/feature/async/testAsync.ts` for an example.
132
+
133
+ ## Rules
134
+
135
+ - Command classes are *data* classes. Don't put behavior on them — that belongs in the handler.
136
+ - Don't reuse a command name for two handlers. The framework throws on boot.
137
+ - Don't `await async.runCommand(...)` inside a request handler expecting it to finish synchronously — `runCommand` schedules the job and returns immediately. Use `async.scheduleCommand(..., { seconds: 0 })` if you want the same fire-and-forget behavior explicitly.
138
+ - Cron expressions in seconds (6 fields) are supported but be careful: the in-memory adapter polls fast, the PG adapter polls at a lower rate.
139
+ - Wrap multi-repository writes in `@transaction()` rather than chaining manual `withPgTransaction` calls.
140
+
141
+ ## Testing
142
+
143
+ `createAsyncHarness({ register?, authInfo? })` from `@wabot-dev/framework/testing` runs `@command` handlers (`harness.execute(Command, data)` — real validation, returns the transformed command) and `@cronHandler` jobs (`harness.runCron(Handler)`) inline, with no PostgreSQL or polling workers. `isValidCronSequence` and `waitUntil` help with schedule assertions. See the `wabot-testing` skill.
@@ -0,0 +1,153 @@
1
+ ---
2
+ name: wabot-auth
3
+ description: Use when adding authentication to a Wabot REST endpoint or Socket.IO namespace, issuing JWT access/refresh tokens, or protecting endpoints with API keys. Covers the chat/request-scoped Auth<D> service, @jwtGuard / @jwtHandshakeGuard, JwtConfig and the JWT_* env vars, Jwt.createToken / findRefreshTokenAuthInfo, JwtRefreshToken, and @apiKeyGuard / @apiKeyHandshakeGuard with PgApiKeyRepository / RemoteApiKeyRepository.
4
+ ---
5
+
6
+ # Authentication
7
+
8
+ Wabot's auth is request-scoped. A guard middleware verifies the credential, then writes the resolved auth info into the per-request/socket `Auth<D>` instance. Downstream code reads it with `auth.require()`.
9
+
10
+ ## `Auth<D>`
11
+
12
+ ```typescript
13
+ import { Auth, injectable } from '@wabot-dev/framework'
14
+
15
+ interface SessionInfo {
16
+ userId: string
17
+ role: 'admin' | 'user'
18
+ }
19
+
20
+ @injectable()
21
+ export class OrdersService {
22
+ constructor(private auth: Auth<SessionInfo>) {}
23
+
24
+ list() {
25
+ const session = this.auth.require() // throws CustomError({ httpCode: 401 }) if not assigned
26
+ return this.repo.findByUserId(session.userId)
27
+ }
28
+ }
29
+ ```
30
+
31
+ `Auth` is `@scoped(Lifecycle.ContainerScoped)` and exists once per request, chat, or socket connection. Methods:
32
+
33
+ - `assign(info)` — sets the auth info (used by guards). Throws if already assigned.
34
+ - `override(info)` — replaces even when assigned (use cautiously).
35
+ - `require()` — returns the info or throws 401.
36
+ - `isAssigned()` / `wasOverrided()` / `clear()`.
37
+
38
+ ## JWT — REST
39
+
40
+ ```typescript
41
+ import { jwtGuard, onGet, onPost, restController } from '@wabot-dev/framework'
42
+
43
+ @restController('/account')
44
+ export class AccountController {
45
+ @onGet('/me')
46
+ @jwtGuard()
47
+ async me(_, auth: Auth<SessionInfo>) {
48
+ return auth.require()
49
+ }
50
+ }
51
+ ```
52
+
53
+ `@jwtGuard()` reads `Authorization: Bearer <token>`, verifies it with `JwtConfig` (env-driven, see below), and calls `auth.assign(payload)`.
54
+
55
+ ## JWT — Socket
56
+
57
+ ```typescript
58
+ import { jwtHandshakeGuard, socketController } from '@wabot-dev/framework'
59
+
60
+ @socketController('private')
61
+ @jwtHandshakeGuard()
62
+ export class PrivateSocketController { /* ... */ }
63
+ ```
64
+
65
+ The token is read from `socket.handshake.auth.token` first, falling back to the `Authorization` header.
66
+
67
+ ## Issuing tokens
68
+
69
+ ```typescript
70
+ import { Auth, Jwt, JwtAccessAndRefreshTokenDto, injectable, onPost, restController } from '@wabot-dev/framework'
71
+
72
+ @restController('/auth')
73
+ export class AuthController {
74
+ constructor(private jwt: Jwt, private auth: Auth<SessionInfo>) {}
75
+
76
+ @onPost('/login')
77
+ async login(body: LoginRequest): Promise<JwtAccessAndRefreshTokenDto> {
78
+ const session = await this.userService.verify(body)
79
+ this.auth.assign(session)
80
+ return this.jwt.createToken({ device: body.device ?? 'unknown' })
81
+ }
82
+
83
+ @onPost('/refresh')
84
+ async refresh(body: RefreshRequest): Promise<JwtAccessAndRefreshTokenDto> {
85
+ const session = await this.jwt.findRefreshTokenAuthInfo(body.refreshToken)
86
+ this.auth.override(session)
87
+ return this.jwt.createToken()
88
+ }
89
+ }
90
+ ```
91
+
92
+ `Jwt.createToken(metadata?)`:
93
+ - Reads the current `auth.require()` info.
94
+ - Persists a `JwtRefreshToken` via `JwtRefreshTokenRepository`. The base class throws `Method not implemented` — the project must register an implementation: `container.registerType(JwtRefreshTokenRepository, PgJwtRefreshTokenRepository)` (table `wabot.jwt_refresh_token`), or its own.
95
+ - Returns `{ access: { token, expiration }, refresh: { token, expiration } }`.
96
+
97
+ `Jwt.findRefreshTokenAuthInfo(secret)` validates the refresh secret (hash-compare + expiration + revocation) and returns the original auth payload. Throws 401 if any check fails.
98
+
99
+ ### Env vars
100
+
101
+ | Variable | Default | Description |
102
+ | --- | --- | --- |
103
+ | `JWT_SECRET` | (required) | Symmetric secret or asymmetric key used for sign/verify |
104
+ | `JWT_ALGORITHM` | `HS256` | Any `jsonwebtoken.Algorithm` |
105
+ | `JWT_ACCESS_EXPIRATION_SECONDS` | `600` (10 min) | Access token lifetime |
106
+ | `JWT_REFRESH_EXPIRATION_SECONDS` | `31536000` (1 yr) | Refresh token lifetime |
107
+
108
+ `JwtConfig` is `@singleton()` and reads these at boot.
109
+
110
+ ### Refresh-token metadata
111
+
112
+ `JwtRefreshToken<D>` exposes `metadata` (the arbitrary `Record<string, string>` you passed to `createToken`), `expirationTime`, `revoke()`, and `isValidToken(secret)`. The repository exposes `findByMetadata(metadata)` so you can revoke all tokens issued to a device.
113
+
114
+ ## API key
115
+
116
+ ```typescript
117
+ import { apiKeyGuard, apiKeyHandshakeGuard, onGet, restController, socketController } from '@wabot-dev/framework'
118
+
119
+ @restController('/internal')
120
+ export class InternalController {
121
+ @onGet('/health')
122
+ @apiKeyGuard()
123
+ async health() { return { ok: true } }
124
+ }
125
+
126
+ @socketController('admin')
127
+ @apiKeyHandshakeGuard()
128
+ export class AdminSocketController { /* ... */ }
129
+ ```
130
+
131
+ The guard reads `Authorization: Api-Key <secret>` (REST) or `socket.handshake.auth.token` / the `Authorization` handshake header (socket), validates via `ApiKeyRepository`, and assigns the lookup result to `Auth`.
132
+
133
+ `ApiKeyRepository` is a base class with no storage — register an implementation yourself: `container.registerType(ApiKeyRepository, PgApiKeyRepository)` (table `wabot.api_key`), or `RemoteApiKeyRepository` to delegate validation to an external service:
134
+
135
+ ```typescript
136
+ import { container, ApiKeyRepository, RemoteApiKeyRepository } from '@wabot-dev/framework'
137
+
138
+ container.registerType(ApiKeyRepository, RemoteApiKeyRepository)
139
+ ```
140
+
141
+ Set `WABOT_API_KEY` and `WABOT_LLM_URL` to point `RemoteApiKeyRepository` at your auth service (it expects the same shape as the Wabot platform).
142
+
143
+ ## Rules
144
+
145
+ - Always type `Auth<D>` with your session shape. `auth.require()` returns the typed value.
146
+ - A guard runs once per request/connection. Don't reassign manually — use `override(...)` only on token refresh.
147
+ - `@jwtGuard()` and `@apiKeyGuard()` work on REST endpoints; their `*HandshakeGuard()` variants work on socket controllers. Don't mix them up.
148
+ - Never log `auth.require()` results — they may contain PII or token claims. Use a redacted view.
149
+ - If you need a custom guard, implement `IMiddleware` (REST) or `IHandshakeMiddleware` (socket) and wire it with `@middleware(...)` / `@handshakeMiddlewares([...])`.
150
+
151
+ ## Testing
152
+
153
+ From `@wabot-dev/framework/testing`: `RestHarness` with `jwt: true` registers a test `JwtConfig` so the real `@jwtGuard` works without env secrets (`harness.as(authInfo)` signs per-request tokens; `harness.jwt.signInvalid()` for 401 tests). `TestApiKeyRepository` is an in-RAM `ApiKeyRepository` for `@apiKeyGuard` (`register: [[ApiKeyRepository, repo]]`, `repo.addKey(authInfo)`). Harnesses also accept `authInfo` to populate the container-scoped `Auth` directly. See the `wabot-testing` skill.
@@ -0,0 +1,140 @@
1
+ ---
2
+ name: wabot-chat
3
+ description: Use when wiring chat input/output for a Wabot bot — receiving messages from one or more channels (cmd, socket, telegram, whatsapp, wasender), injecting a ChatBot bound to a mindset, and replying. Covers @chatController, @chatBot, @cmd / @socket / @telegram / @whatsApp / @wasender, IReceivedMessage, the ChatBot.sendMessage callback, and the @chatAdapter / runChatAdapters provider model.
4
+ ---
5
+
6
+ # Chat controllers, channels, and adapters
7
+
8
+ A chat controller is the entry point for messages from a transport (terminal, socket, Telegram, WhatsApp). It owns a `ChatBot` bound to one mindset and forwards the message into the bot's processing loop.
9
+
10
+ ## Controller shape
11
+
12
+ ```typescript
13
+ import {
14
+ chatBot,
15
+ ChatBot,
16
+ chatController,
17
+ cmd,
18
+ socket,
19
+ telegram,
20
+ whatsApp,
21
+ type IReceivedMessage,
22
+ } from '@wabot-dev/framework'
23
+ import { PixelMindset } from './mindset/PixelMindset'
24
+
25
+ @chatController()
26
+ export class PixelChatController {
27
+ constructor(@chatBot(PixelMindset) private pixel: ChatBot) {}
28
+
29
+ @cmd()
30
+ @socket({ namespace: 'pixel' })
31
+ @telegram({ botToken: process.env.TELEGRAM_BOT_TOKEN! })
32
+ @whatsApp({ number: '+1555…', accessToken: '…', businessNumberId: '…' })
33
+ async onMessage(ctx: IReceivedMessage) {
34
+ await this.pixel.sendMessage(ctx.message, async (reply) => {
35
+ await ctx.reply(reply)
36
+ })
37
+ }
38
+ }
39
+ ```
40
+
41
+ Channel decorators stack: each one registers the same method on a different transport. The runner instantiates a channel per registration.
42
+
43
+ `@chatController()` takes no arguments today — `IchatControllerConfig` is currently empty. It applies `@injectable()`.
44
+
45
+ ## `ChatBot` injection
46
+
47
+ `@chatBot(MindsetCtor)` injects a `ChatBot` instance scoped to that mindset. You can have several in one controller (e.g. a "support" bot and a "sales" bot) — each gets a unique injection token under the hood.
48
+
49
+ `ChatBot.sendMessage(message, callback)`:
50
+ - Persists the incoming message as a `humanMessage` chat item.
51
+ - Calls the mindset → adapter loop, executing any tool calls.
52
+ - Invokes `callback(replyMessage)` once per `botMessage` item produced.
53
+
54
+ Inside the loop, the bot picks `models.visionLlm` if any image is attached, otherwise `models.llm`. It will only use providers registered through `@chatAdapter` (see below).
55
+
56
+ ## `IReceivedMessage` and `IChatMessage`
57
+
58
+ ```typescript
59
+ interface IReceivedMessage {
60
+ message: IChatMessage
61
+ reply: (message: IChatMessage) => Promise<Record<string, string> | void>
62
+ }
63
+
64
+ interface IChatMessage {
65
+ senderId?: string
66
+ senderName?: string
67
+ text?: string
68
+ images?: IChatMessageImage[]
69
+ documents?: IChatMessageDocument[]
70
+ object?: object
71
+ metadata?: Record<string, string>
72
+ }
73
+ ```
74
+
75
+ The `reply` callback comes from the channel — for `@cmd` it writes to stdout of the connected `cmd:channel` client; for `@socket` it emits a Socket.IO event; for `@telegram`/`@whatsApp`/`@wasender` it calls the provider's send API. You always pass an `IChatMessage` (with `text`, `images`, or `documents`); never raw strings.
76
+
77
+ ## Channels — what each decorator needs
78
+
79
+ | Decorator | Config | Notes |
80
+ | --- | --- | --- |
81
+ | `@cmd()` | none | Local Unix socket terminal client. Run `npm run cmd` to chat. State stored under `.wabot/cmd-channel/<route>/`. |
82
+ | `@socket({ namespace })` | `namespace: string \| ConfigReference<string>` | Mounts a Socket.IO namespace. Tag-fn refs (`str\`socket.namespace\``) supported. |
83
+ | `@telegram({ botToken })` | `botToken: string \| ConfigReference<string>` | Long-poll Telegram bot. Raw token or `str\`telegram.bot_token\``. |
84
+ | `@whatsApp(numberOrConfig)` | string OR `{ number, accessToken?, businessNumberId? }` (each may be a `ConfigReference`) | WhatsApp Cloud API. |
85
+ | `@wasender(config?)` | `{ apiKey?, webhookSecret?, phoneNumber?, webhookPath? }` (each may be a `ConfigReference`) | WaSender integration. |
86
+
87
+ Channel configs that accept `ConfigReference` resolve env vars automatically — use `str\`wasender.apiKey\`` from `@wabot-dev/framework` to point to `WASENDER_APIKEY`.
88
+
89
+ ## Chat resolution
90
+
91
+ When a message arrives, `ChatResolver` looks up the `Chat` by `IChatConnection` (`{ chatType: 'PRIVATE' | 'GROUP', channelName, id }`). If none exists, a new `Chat` is created under a lock. You never write this resolution code — it's already wired.
92
+
93
+ Inside a chat handler, these tokens are available in the child container:
94
+ - `Chat` — the resolved chat entity
95
+ - `ChatMemory` — append/read chat items for this chat
96
+ - `ChatOperator` — convenience wrapper (see `wabot-mindset`)
97
+ - `Auth` — populated if the channel attached auth (e.g. via `injectInstances`)
98
+
99
+ ## Chat adapters (providers)
100
+
101
+ Each LLM provider has a `@chatAdapter({ provider: 'name' })` class implementing `IChatAdapter.nextItems`. The framework ships adapters for `openai`, `anthropic`, `google`, `openrouter`, `deepseek`, and `wabot`.
102
+
103
+ The project runner registers adapters automatically based on env keys (`OPENAI_API_KEY` → OpenAI, etc.). To override, pass `chatAdapters: [...]` to `run({ chatAdapters: [MyAdapter, OpenaiChatAdapter] })`.
104
+
105
+ To call them manually (outside the runner):
106
+
107
+ ```typescript
108
+ import { runChatAdapters, OpenaiChatAdapter } from '@wabot-dev/framework'
109
+ runChatAdapters([OpenaiChatAdapter])
110
+ ```
111
+
112
+ `runChatAdapters` binds a `UnionChatAdapter` to the `ChatAdapter` token — it dispatches by `IMindsetModelRef.provider`.
113
+
114
+ To build your own adapter, implement `IChatAdapter`:
115
+
116
+ ```typescript
117
+ import { chatAdapter, IChatAdapter, IChatAdapterNextItemsReq, IChatAdapterNextItemsRes, singleton } from '@wabot-dev/framework'
118
+
119
+ @chatAdapter({ provider: 'my-llm' })
120
+ @singleton()
121
+ export class MyLlmAdapter implements IChatAdapter {
122
+ async nextItems(req: IChatAdapterNextItemsReq): Promise<IChatAdapterNextItemsRes> {
123
+ // map req.systemPrompt + req.prevItems + req.tools to your API, return { nextItems, usage }
124
+ return { nextItems: [], usage: { inputTokens: 0, outputTokens: 0, provider: 'my-llm', model: req.models[0].model } }
125
+ }
126
+ }
127
+ ```
128
+
129
+ ## Rules
130
+
131
+ - Keep controllers thin — fetch from `ctx.message`, call `bot.sendMessage`, return. Business logic belongs in modules/services.
132
+ - Don't pass raw strings to `ctx.reply` — wrap them in `{ text: '...' }`.
133
+ - The reply callback is async; await it so the channel actually flushes.
134
+ - `@chatBot(Mindset)` is a parameter decorator — it can only appear in a constructor.
135
+ - If you add a new chat adapter, its file must be imported during the runner's scan (so the `@chatAdapter` side-effect runs); place it inside `directories`.
136
+ - Avoid building a mindset/controller pair when one controller can route between several mindsets with multiple `@chatBot(...)` constructor params.
137
+
138
+ ## Testing
139
+
140
+ `createChatControllerHarness({ controller })` from `@wabot-dev/framework/testing` drives a `@chatController` end-to-end without a real channel (same per-message container path as production, `@chatBot` included), with a scripted `MockChatAdapter` playing the LLM. Custom `IChatAdapter` implementations should pass `chatAdapterConformanceCases`. See the `wabot-testing` skill.
@@ -0,0 +1,117 @@
1
+ ---
2
+ name: wabot-di-config
3
+ description: Use when wiring services, choosing service lifecycles, reading env vars, or expressing typed config references for Wabot apps. Covers @injectable / @singleton / @scoped / @inject, the container, the Env service, and the config tag functions (str, num, bool, obj, strArr, numArr, boolArr) that drive resolveConfigReferences in channel and adapter decorators.
4
+ ---
5
+
6
+ # DI, Env, and config references
7
+
8
+ Wabot uses `tsyringe` under the hood and re-exports the pieces you need from `@wabot-dev/framework`:
9
+
10
+ ```typescript
11
+ import { container, injectable, singleton, scoped, Lifecycle, inject } from '@wabot-dev/framework'
12
+ ```
13
+
14
+ ## Lifecycles
15
+
16
+ | Decorator | When to use | Example |
17
+ | --- | --- | --- |
18
+ | `@singleton()` | App-wide infra: clients, repositories, config holders, caches. One instance for the process. | DB clients, adapters |
19
+ | `@injectable()` | Stateless / per-resolution services. Each `resolve` returns a new instance. | Calculators, mappers |
20
+ | `@scoped(Lifecycle.ContainerScoped)` | Per-request / per-chat / per-socket state. Lives in the child container created by the runner. | `Auth`, request context |
21
+
22
+ The framework already creates child containers for every chat message, REST request, and socket connection. `ContainerScoped` services do not leak across them.
23
+
24
+ ```typescript
25
+ import { injectable, singleton, scoped, Lifecycle } from '@wabot-dev/framework'
26
+
27
+ @singleton()
28
+ export class ProductCache {
29
+ private byId = new Map<string, string>()
30
+ put(id: string, name: string) { this.byId.set(id, name) }
31
+ }
32
+
33
+ @injectable()
34
+ export class QuoteCalculator {
35
+ total(base: number, discount: number) { return base - base * discount }
36
+ }
37
+
38
+ @scoped(Lifecycle.ContainerScoped)
39
+ export class RequestContext {
40
+ private userId?: string
41
+ setUserId(id: string) { this.userId = id }
42
+ requireUserId() {
43
+ if (!this.userId) throw new Error('Unauthorized')
44
+ return this.userId
45
+ }
46
+ }
47
+ ```
48
+
49
+ Decorators that mark something as a controller/handler also apply `@injectable()` for you (`@chatController`, `@restController`, `@socketController`, `@commandHandler`, `@cronHandler`, `@mindset`, `@mindsetModule`). Do not stack lifecycle decorators on top of those — they will conflict.
50
+
51
+ ## Resolving and registering
52
+
53
+ ```typescript
54
+ import { container, inject, injectable } from '@wabot-dev/framework'
55
+
56
+ container.register('SMTP_HOST', { useValue: 'smtp.example.com' })
57
+ container.register('SMTP_PORT', { useValue: 587 })
58
+
59
+ @injectable()
60
+ export class Mailer {
61
+ constructor(
62
+ @inject('SMTP_HOST') private host: string,
63
+ @inject('SMTP_PORT') private port: number,
64
+ ) {}
65
+ }
66
+ ```
67
+
68
+ You rarely need `container.register*` in app code — for chat/REST/socket components the runner does it. Use registration only for:
69
+
70
+ - Replacing a default infra binding (e.g. swap `Locker` for a custom implementation).
71
+ - Providing typed config values to classes you cannot decorate.
72
+ - Bootstrapping in tests.
73
+
74
+ `container.registerInstance(Token, value)` and `container.registerType(From, To)` are the two most common helpers.
75
+
76
+ ## `Env`
77
+
78
+ ```typescript
79
+ import { container, Env } from '@wabot-dev/framework'
80
+
81
+ const env = container.resolve(Env)
82
+
83
+ const dbUrl = env.requireString('DATABASE_URL')
84
+ const port = env.requireNumber('PORT', { default: 3000 })
85
+ const isDev = env.isDevelopment() // controlled by WABOT_ENV: 'development' | 'staging' | 'testing' | 'production'
86
+ ```
87
+
88
+ `Env.requireString` / `requireNumber` throw if the variable is missing and no `default` is provided.
89
+
90
+ ## Config tag functions
91
+
92
+ Some addon decorators (e.g. `@socket`, `@whatsApp`, `@wasender`) accept a config object whose fields may be `ConfigReference` values produced by tag functions. The reference is resolved by `resolveConfigReferences` (called inside the decorator) against `process.env`, with the path translated to UPPER_SNAKE_CASE.
93
+
94
+ ```typescript
95
+ import { socket } from '@wabot-dev/framework' // tag fns also exported from the package root
96
+ import { str, num, bool, obj, strArr, numArr, boolArr } from '@wabot-dev/framework'
97
+
98
+ @socket({
99
+ // Resolved from env: SOCKET_NAMESPACE; defaults to "chat" if env is missing.
100
+ namespace: str`socket.namespace:chat`,
101
+ maxConnections: num`socket.max:100`,
102
+ })
103
+ class MyController { /* ... */ }
104
+ ```
105
+
106
+ Syntax: `<fn>\`path.with.dots:default\``. The default after the colon is optional. Only literal strings are allowed; interpolation throws.
107
+
108
+ Available factories: `str`, `num`, `bool`, `obj` (parses JSON), `strArr`, `numArr`, `boolArr` (comma-separated).
109
+
110
+ If you call `resolveConfigReferences(cfg)` yourself it returns a typed object where each `ConfigReference<T>` field is replaced by `T`.
111
+
112
+ ## Rules
113
+
114
+ - Default to `@injectable()` for new code. Promote to `@singleton()` only when state must be shared.
115
+ - `@scoped(Lifecycle.ContainerScoped)` only when the value must not leak across requests, chats, or socket connections.
116
+ - Don't call `container.resolve(...)` inside a constructor — declare dependencies as constructor parameters and let DI inject them.
117
+ - Use `Env.requireString/requireNumber` for plain env reads; use config tag functions only inside decorator configs that the framework already resolves.
@@ -0,0 +1,81 @@
1
+ ---
2
+ name: wabot-framework
3
+ description: Use when working with @wabot-dev/framework — building, reviewing, or debugging chat bots, REST APIs, sockets, async jobs, or any project scaffolded with @wabot-dev/create. Covers project boot, the IProjectRunnerConfig flow, file layout, the WABOT_BUNDLED dev/prod split, and where to dive for each subsystem.
4
+ ---
5
+
6
+ # Wabot Framework — Umbrella skill
7
+
8
+ Wabot is a TypeScript framework for chat bots, REST and Socket.IO APIs, and async/cron work, all wired through a `tsyringe`-based DI container. Code is self-discovered from `src/` at boot time and each subsystem is opt-in via decorators.
9
+
10
+ ## Project boot — `src/_run_.ts`
11
+
12
+ ```typescript
13
+ import { run, IProjectRunnerConfig } from '@wabot-dev/framework'
14
+
15
+ export const config: IProjectRunnerConfig = {}
16
+
17
+ export default config
18
+
19
+ if (process.env.WABOT_BUNDLED !== '1') {
20
+ run(config)
21
+ }
22
+ ```
23
+
24
+ - `run(config)` is the entry point. It scans `directories` (default `['src']`), imports every `.ts`/`.js` it finds (so decorators register), then starts every discovered subsystem.
25
+ - `WABOT_BUNDLED=1` is set in production by `npm start` after `npm run build` produces `dist/entry.js` (bundled by `@wabot-dev/framework/dist/build/build.js`). In that mode you must not call `run()` at import time — the bundled entry calls it.
26
+ - If `DATABASE_URL` is set to `postgres://…` the runner registers Pg adapters (chat memory/repo, jobs, cron jobs, locker, transaction). Otherwise it registers the in-memory equivalents. There is no manual `container.registerType(ChatRepository, …)` to write.
27
+
28
+ `IProjectRunnerConfig` fields: `directories?`, `exclude?`, `connectionString?`, `chatAdapters?` (override auto-detect by env keys: `OPENAI_API_KEY` → OpenAI, `OPENROUTER_API_KEY` → OpenRouter, `ANTHROPIC_API_KEY` → Anthropic, `GOOGLE_API_KEY` → Google), `preloaded?` (used by the bundled output — skip filesystem scan).
29
+
30
+ ## Scripts (from `@wabot-dev/template`)
31
+
32
+ | Script | What it does |
33
+ | --- | --- |
34
+ | `npm run dev` | `node --import=@yucacodes/ts --env-file=./.env ./src/_run_.ts` |
35
+ | `npm run dev:watch` | Same with `--watch --watch-preserve-output` |
36
+ | `npm run cmd` | Runs `runCmdClient()` — connects to the local cmd channel for terminal chats |
37
+ | `npm run build` | Bundles to `dist/entry.js` via the framework's `build.js` |
38
+ | `npm start` | Runs the bundle with `WABOT_BUNDLED=1` |
39
+ | `npm run tsc` | `tsc --noEmit` |
40
+
41
+ ## Typical layout
42
+
43
+ ```text
44
+ src/
45
+ _run_.ts # boot: run(config)
46
+ _cmd_.ts # optional terminal client
47
+ <bot>/
48
+ <Bot>ChatController.ts # @chatController + channel decorators
49
+ mindset/<Bot>Mindset.ts # @mindset implementing IMindset
50
+ modules/<Foo>Module.ts # @mindsetModule with tool functions
51
+ models/<x>/<X>.ts # Entity subclass
52
+ models/<x>/<X>Repository.ts # @repository + @memExtension/@pgExtension
53
+ ```
54
+
55
+ The runner discovers `@chatController`, `@restController`, `@socketController`, `@uiController`, `@commandHandler`, `@cronHandler`, and `@chatAdapter` by scanning `directories`. No central registration list.
56
+
57
+ ## Subsystem skills
58
+
59
+ Load the skill that matches the task:
60
+
61
+ | Task | Skill |
62
+ | --- | --- |
63
+ | Wire services, lifecycles, env, config | `wabot-di-config` |
64
+ | Validate DTOs, transform input, describe tool args | `wabot-validation` |
65
+ | Define entities, repositories, queries | `wabot-persistence` |
66
+ | Define bot behavior, modules, models | `wabot-mindset` |
67
+ | Receive chat messages, configure channels (cmd, socket, telegram, whatsapp) | `wabot-chat` |
68
+ | Expose REST endpoints or Socket.IO namespaces | `wabot-rest-socket` |
69
+ | Server-render web pages, forms, and interactive islands | `wabot-ui` |
70
+ | Background commands, cron schedules, DB transactions | `wabot-async` |
71
+ | Protect endpoints/sockets with JWT or API key | `wabot-auth` |
72
+ | Logger, locking, errors, password hashing, randomness | `wabot-ops` |
73
+ | Write tests or evals (harnesses, mocks, LlmJudge) | `wabot-testing` |
74
+
75
+ ## Hard rules
76
+
77
+ - All decorators and helpers ship from a single package: `@wabot-dev/framework`. Never import from internal paths.
78
+ - Code identifiers are in English; user-facing text (mindset prompts, docs) may be in Spanish or the bot's chosen language.
79
+ - Decorators run as **side effects** on import. The project runner relies on this. Do not lazy-load files that contain decorators behind conditional branches.
80
+ - Never invent decorators or types not present in the framework source. If a need is not covered, write a plain class with `@injectable()`/`@singleton()` and inject it.
81
+ - Do not rename or restructure the discovery scripts (`_run_.ts`, `_cmd_.ts`) without also updating `package.json` and the build output.