@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.
- package/README.md +27 -0
- package/bin/skills.mjs +151 -0
- package/bin/wabot-skills.mjs +120 -0
- package/dist/build/build.js +1031 -8
- package/dist/src/addon/chat-bot/in-memory/InMemoryChatMemory.js +1 -3
- package/dist/src/addon/chat-bot/xai/XAIChatAdapter.js +180 -0
- package/dist/src/addon/chat-controller/cmd/cmdChannelSocketPath.js +1 -5
- package/dist/src/addon/chat-controller/hubspot/@hubspot.js +28 -0
- package/dist/src/addon/chat-controller/hubspot/HubSpotChannel.js +81 -0
- package/dist/src/addon/chat-controller/hubspot/HubSpotChannelConfig.js +20 -0
- package/dist/src/addon/chat-controller/hubspot/HubSpotReceiver.js +42 -0
- package/dist/src/addon/chat-controller/hubspot/HubSpotSender.js +118 -0
- package/dist/src/addon/chat-controller/hubspot/HubSpotWebhookController.js +122 -0
- package/dist/src/addon/chat-controller/hubspot/downloadHubSpotAttachments.js +45 -0
- package/dist/src/addon/chat-controller/hubspot/hubspotChannelName.js +3 -0
- package/dist/src/addon/chat-controller/hubspot/verifyHubSpotSignatureV3.js +28 -0
- package/dist/src/addon/chat-controller/{telegram/markdownToTelegramHtml.js → markdown/markdownToChatHtml.js} +5 -8
- package/dist/src/addon/chat-controller/slack/@slack.js +22 -0
- package/dist/src/addon/chat-controller/slack/SlackChannel.js +187 -0
- package/dist/src/addon/chat-controller/slack/SlackChannelConfig.js +12 -0
- package/dist/src/addon/chat-controller/slack/markdownToSlackMrkdwn.js +38 -0
- package/dist/src/addon/chat-controller/slack/slackChannelName.js +3 -0
- package/dist/src/addon/chat-controller/telegram/TelegramChannel.js +2 -2
- package/dist/src/addon/ui/preact/PreactRenderer.js +86 -0
- package/dist/src/addon/ui/preact/outlet.js +22 -0
- package/dist/src/addon/ui/preact/preactClientRuntime.js +67 -0
- package/dist/src/core/repository/CrudRepository.js +7 -7
- package/dist/src/feature/async/computeDedupKey.js +1 -1
- package/dist/src/feature/chat-controller/runChatControllers.js +4 -1
- package/dist/src/feature/pg/@pgExtension.js +2 -4
- package/dist/src/feature/project-runner/ProjectRunner.js +62 -10
- package/dist/src/feature/project-runner/scanner.js +1 -1
- package/dist/src/feature/repository/@memExtension.js +1 -2
- package/dist/src/feature/rest-controller/runRestControllers.js +11 -6
- package/dist/src/feature/ui-controller/actions.js +35 -0
- package/dist/src/feature/ui-controller/bundler/UiBundler.js +191 -0
- package/dist/src/feature/ui-controller/bundler/devMiddleware.js +41 -0
- package/dist/src/feature/ui-controller/bundler/index.js +4 -0
- package/dist/src/feature/ui-controller/bundler/manifest.js +34 -0
- package/dist/src/feature/ui-controller/bundler/navRuntime.js +236 -0
- package/dist/src/feature/ui-controller/bundler/pageAssets.js +30 -0
- package/dist/src/feature/ui-controller/document/escape.js +17 -0
- package/dist/src/feature/ui-controller/document/helpers.js +13 -0
- package/dist/src/feature/ui-controller/document/renderDocument.js +43 -0
- package/dist/src/feature/ui-controller/island/IslandRegistry.js +68 -0
- package/dist/src/feature/ui-controller/island/island.js +40 -0
- package/dist/src/feature/ui-controller/island/serialize.js +35 -0
- package/dist/src/feature/ui-controller/metadata/@action.js +18 -0
- package/dist/src/feature/ui-controller/metadata/@uiController.js +19 -0
- package/dist/src/feature/ui-controller/metadata/@uiMiddleware.js +20 -0
- package/dist/src/feature/ui-controller/metadata/@view.js +18 -0
- package/dist/src/feature/ui-controller/metadata/UiControllerMetadataStore.js +107 -0
- package/dist/src/feature/ui-controller/renderer/UiRendererRegistry.js +42 -0
- package/dist/src/feature/ui-controller/runUiControllers.js +285 -0
- package/dist/src/index.d.ts +640 -3
- package/dist/src/index.js +32 -3
- package/dist/src/testing/LlmJudge.js +93 -0
- package/dist/src/testing/MockChatAdapter.js +68 -0
- package/dist/src/testing/TestChatMemory.js +73 -0
- package/dist/src/testing/asyncHarness.js +66 -0
- package/dist/src/testing/auth.js +114 -0
- package/dist/src/testing/chatBotHarness.js +88 -0
- package/dist/src/testing/chatControllerHarness.js +94 -0
- package/dist/src/testing/conformance/chatAdapterConformanceCases.js +656 -0
- package/dist/src/testing/fixtures.js +53 -0
- package/dist/src/testing/helpers.js +42 -0
- package/dist/src/testing/index.d.ts +818 -0
- package/dist/src/testing/index.js +14 -0
- package/dist/src/testing/repositories.js +34 -0
- package/dist/src/testing/restHarness.js +127 -0
- package/dist/src/testing/testImageBase64.js +5 -0
- package/dist/src/testing/uiHarness.js +102 -0
- package/dist/src/testing/validation.js +66 -0
- package/dist/src/ui/client.js +6 -0
- package/dist/src/ui/index.d.ts +427 -0
- package/dist/src/ui/index.js +29 -0
- package/dist/src/ui/jsx-dev-runtime.d.ts +1 -0
- package/dist/src/ui/jsx-dev-runtime.js +1 -0
- package/dist/src/ui/jsx-runtime.d.ts +1 -0
- package/dist/src/ui/jsx-runtime.js +1 -0
- package/package.json +48 -11
- package/skills/wabot-async/SKILL.md +143 -0
- package/skills/wabot-auth/SKILL.md +153 -0
- package/skills/wabot-chat/SKILL.md +140 -0
- package/skills/wabot-di-config/SKILL.md +117 -0
- package/skills/wabot-framework/SKILL.md +81 -0
- package/skills/wabot-framework/references/quickstart.md +85 -0
- package/skills/wabot-mindset/SKILL.md +159 -0
- package/skills/wabot-ops/SKILL.md +151 -0
- package/skills/wabot-persistence/SKILL.md +159 -0
- package/skills/wabot-rest-socket/SKILL.md +167 -0
- package/skills/wabot-testing/SKILL.md +214 -0
- package/skills/wabot-ui/SKILL.md +201 -0
- 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.
|