@wabot-dev/create 0.0.4 → 1.0.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.
- package/README.md +45 -144
- package/lib/create-project.mjs +83 -11
- package/lib/i18n.mjs +18 -6
- package/lib/skills/framework-source.mjs +68 -0
- package/lib/skills/install.mjs +15 -8
- package/lib/skills/registry.mjs +26 -56
- package/package.json +5 -3
- package/skills/wabot-async/SKILL.md +0 -139
- package/skills/wabot-auth/SKILL.md +0 -149
- package/skills/wabot-chat/SKILL.md +0 -136
- package/skills/wabot-di-config/SKILL.md +0 -117
- package/skills/wabot-framework/SKILL.md +0 -79
- package/skills/wabot-framework/references/quickstart.md +0 -85
- package/skills/wabot-mindset/SKILL.md +0 -155
- package/skills/wabot-ops/SKILL.md +0 -151
- package/skills/wabot-persistence/SKILL.md +0 -155
- package/skills/wabot-rest-socket/SKILL.md +0 -163
- package/skills/wabot-validation/SKILL.md +0 -104
package/lib/skills/registry.mjs
CHANGED
|
@@ -1,64 +1,34 @@
|
|
|
1
|
-
import
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
return {
|
|
5
|
-
name,
|
|
6
|
-
description,
|
|
7
|
-
sourceDir: fileURLToPath(new URL(`../../skills/${name}`, import.meta.url)),
|
|
8
|
-
}
|
|
9
|
-
}
|
|
4
|
+
import { resolveFrameworkSkillsDir } from './framework-source.mjs'
|
|
10
5
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
'Umbrella skill — project boot, layout, scripts, and where to dive for each subsystem',
|
|
15
|
-
),
|
|
16
|
-
'wabot-di-config': packagedSkill(
|
|
17
|
-
'wabot-di-config',
|
|
18
|
-
'DI lifecycles, Env, and typed config references',
|
|
19
|
-
),
|
|
20
|
-
'wabot-validation': packagedSkill(
|
|
21
|
-
'wabot-validation',
|
|
22
|
-
'Validators, @description, validateAndTransform, Mapper',
|
|
23
|
-
),
|
|
24
|
-
'wabot-persistence': packagedSkill(
|
|
25
|
-
'wabot-persistence',
|
|
26
|
-
'Entities, repositories, query DSL, mem/pg extensions',
|
|
27
|
-
),
|
|
28
|
-
'wabot-mindset': packagedSkill(
|
|
29
|
-
'wabot-mindset',
|
|
30
|
-
'Mindsets, modules, models, ChatOperator',
|
|
31
|
-
),
|
|
32
|
-
'wabot-chat': packagedSkill(
|
|
33
|
-
'wabot-chat',
|
|
34
|
-
'Chat controllers, channels (cmd/socket/telegram/whatsapp/wasender), chat-bot adapters',
|
|
35
|
-
),
|
|
36
|
-
'wabot-rest-socket': packagedSkill(
|
|
37
|
-
'wabot-rest-socket',
|
|
38
|
-
'REST and Socket.IO controllers, middlewares, handshake guards',
|
|
39
|
-
),
|
|
40
|
-
'wabot-async': packagedSkill(
|
|
41
|
-
'wabot-async',
|
|
42
|
-
'Commands, cron handlers, @transaction, the Async service',
|
|
43
|
-
),
|
|
44
|
-
'wabot-auth': packagedSkill(
|
|
45
|
-
'wabot-auth',
|
|
46
|
-
'Auth<D> scope, JWT and API key guards, token issuing',
|
|
47
|
-
),
|
|
48
|
-
'wabot-ops': packagedSkill(
|
|
49
|
-
'wabot-ops',
|
|
50
|
-
'Logger, Locker, CustomError, Password, Random',
|
|
51
|
-
),
|
|
52
|
-
}
|
|
6
|
+
// Skills are owned and versioned by @wabot-dev/framework. The registry discovers
|
|
7
|
+
// them from the framework's packaged `skills/` directory rather than bundling its
|
|
8
|
+
// own copy, so adding a skill upstream requires no change here.
|
|
53
9
|
|
|
54
|
-
export function
|
|
55
|
-
return
|
|
10
|
+
export function listSkillNames(skillsDir = resolveFrameworkSkillsDir()) {
|
|
11
|
+
return fs
|
|
12
|
+
.readdirSync(skillsDir, { withFileTypes: true })
|
|
13
|
+
.filter(
|
|
14
|
+
(entry) =>
|
|
15
|
+
entry.isDirectory() && fs.existsSync(path.join(skillsDir, entry.name, 'SKILL.md')),
|
|
16
|
+
)
|
|
17
|
+
.map((entry) => entry.name)
|
|
18
|
+
.sort()
|
|
56
19
|
}
|
|
57
20
|
|
|
58
|
-
export function
|
|
59
|
-
return
|
|
21
|
+
export function listSkills(skillsDir = resolveFrameworkSkillsDir()) {
|
|
22
|
+
return listSkillNames(skillsDir).map((name) => ({
|
|
23
|
+
name,
|
|
24
|
+
sourceDir: path.join(skillsDir, name),
|
|
25
|
+
}))
|
|
60
26
|
}
|
|
61
27
|
|
|
62
|
-
export function
|
|
63
|
-
|
|
28
|
+
export function getSkill(name, skillsDir = resolveFrameworkSkillsDir()) {
|
|
29
|
+
const sourceDir = path.join(skillsDir, name)
|
|
30
|
+
if (!fs.existsSync(path.join(sourceDir, 'SKILL.md'))) {
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
return { name, sourceDir }
|
|
64
34
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wabot-dev/create",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Project creator for Wabot Framework",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"author": "",
|
|
@@ -11,9 +11,11 @@
|
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
13
|
"bin",
|
|
14
|
-
"lib"
|
|
15
|
-
"skills"
|
|
14
|
+
"lib"
|
|
16
15
|
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test:smoke": "node scripts/smoke-skills-bulk-install.mjs && node scripts/smoke-skill-installer.mjs"
|
|
18
|
+
},
|
|
17
19
|
"dependencies": {
|
|
18
20
|
"chalk": "^5.4.1",
|
|
19
21
|
"commander": "^14.0.0",
|
|
@@ -1,139 +0,0 @@
|
|
|
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: `'pg'` when `DATABASE_URL` is Postgres).
|
|
96
|
-
|
|
97
|
-
```typescript
|
|
98
|
-
import { transaction } from '@wabot-dev/framework'
|
|
99
|
-
|
|
100
|
-
@injectable()
|
|
101
|
-
export class Checkout {
|
|
102
|
-
@transaction(['pg'])
|
|
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
|
-
- If you decorate with `@transaction(['pg'])` and the app is running without Postgres, the call still executes but no PG transaction wraps it (the in-memory adapter is a no-op).
|
|
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(['pg'])` rather than chaining manual `withPgTransaction` calls.
|
|
@@ -1,149 +0,0 @@
|
|
|
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 runner picks `PgJwtRefreshTokenRepository` under PG; you can register your 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 the `X-Api-Key` header (REST) or `socket.handshake.auth.apiKey` (socket), validates via `ApiKeyRepository`, and assigns the lookup result to `Auth`.
|
|
132
|
-
|
|
133
|
-
`ApiKeyRepository` defaults to `PgApiKeyRepository` under Postgres. A `RemoteApiKeyRepository` is also exported for delegating 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([...])`.
|
|
@@ -1,136 +0,0 @@
|
|
|
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:channel` 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` | Long-poll Telegram bot. Pass the raw 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.
|
|
@@ -1,117 +0,0 @@
|
|
|
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.
|