@wabot-dev/create 0.0.3 → 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 +45 -144
- package/lib/cli.mjs +1 -0
- package/lib/create-project.mjs +185 -75
- package/lib/i18n.mjs +167 -0
- package/lib/skills/framework-source.mjs +68 -0
- package/lib/skills/install.mjs +67 -3
- package/lib/skills/registry.mjs +29 -12
- package/package.json +5 -3
- package/skills/wabot-framework/SKILL.md +0 -32
- package/skills/wabot-framework/agents/openai.yaml +0 -5
- package/skills/wabot-framework/references/async-and-cron.md +0 -57
- package/skills/wabot-framework/references/controllers-and-auth.md +0 -104
- package/skills/wabot-framework/references/core-patterns.md +0 -83
- package/skills/wabot-framework/references/full-example.md +0 -278
- package/skills/wabot-framework/references/mindset-and-chat.md +0 -97
- package/skills/wabot-framework/references/ops.md +0 -58
- package/skills/wabot-framework/references/persistence.md +0 -85
- package/skills/wabot-framework/references/quickstart.md +0 -56
- package/skills/wabot-framework/references/validation-and-data.md +0 -88
package/lib/skills/install.mjs
CHANGED
|
@@ -2,7 +2,8 @@ import fs from 'fs/promises'
|
|
|
2
2
|
import os from 'os'
|
|
3
3
|
import path from 'path'
|
|
4
4
|
|
|
5
|
-
import { getSkill, listSkillNames } from './registry.mjs'
|
|
5
|
+
import { getSkill, listSkillNames, listSkills } from './registry.mjs'
|
|
6
|
+
import { resolveFrameworkSkillsDir } from './framework-source.mjs'
|
|
6
7
|
|
|
7
8
|
const supportedAgents = {
|
|
8
9
|
codex: ['.codex', 'skills'],
|
|
@@ -10,12 +11,75 @@ const supportedAgents = {
|
|
|
10
11
|
agents: ['.agents', 'skills'],
|
|
11
12
|
}
|
|
12
13
|
|
|
14
|
+
export function listSupportedAgents() {
|
|
15
|
+
return Object.keys(supportedAgents)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function installSkillsInProject(projectDir, agents, skillNames) {
|
|
19
|
+
if (!projectDir) {
|
|
20
|
+
throw new Error('installSkillsInProject: projectDir is required')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const resolvedAgents = (agents && agents.length > 0 ? agents : ['claude']).map((agent) => {
|
|
24
|
+
if (!supportedAgents[agent]) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`Unsupported agent "${agent}". Supported: ${Object.keys(supportedAgents).join(', ')}`,
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
return agent
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const skillsDir = resolveFrameworkSkillsDir(projectDir)
|
|
33
|
+
const skills =
|
|
34
|
+
skillNames && skillNames.length > 0
|
|
35
|
+
? skillNames.map((name) => requireSkill(name, skillsDir))
|
|
36
|
+
: listSkills(skillsDir)
|
|
37
|
+
|
|
38
|
+
const installations = []
|
|
39
|
+
|
|
40
|
+
for (const agent of resolvedAgents) {
|
|
41
|
+
const baseDir = path.join(projectDir, ...supportedAgents[agent])
|
|
42
|
+
await fs.mkdir(baseDir, { recursive: true })
|
|
43
|
+
|
|
44
|
+
for (const skill of skills) {
|
|
45
|
+
const installDir = path.join(baseDir, skill.name)
|
|
46
|
+
try {
|
|
47
|
+
await fs.cp(skill.sourceDir, installDir, {
|
|
48
|
+
recursive: true,
|
|
49
|
+
force: false,
|
|
50
|
+
errorOnExist: true,
|
|
51
|
+
})
|
|
52
|
+
installations.push({ agent, skill: skill.name, installDir, status: 'installed' })
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (error?.code === 'ERR_FS_CP_EEXIST' || error?.code === 'EEXIST') {
|
|
55
|
+
installations.push({ agent, skill: skill.name, installDir, status: 'skipped' })
|
|
56
|
+
continue
|
|
57
|
+
}
|
|
58
|
+
throw error
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { agents: resolvedAgents, skills: skills.map((skill) => skill.name), installations }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function requireSkill(name, skillsDir) {
|
|
67
|
+
const skill = getSkill(name, skillsDir)
|
|
68
|
+
if (!skill) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`Unknown skill "${name}". Available skills: ${listSkillNames(skillsDir).join(', ')}`,
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
return skill
|
|
74
|
+
}
|
|
75
|
+
|
|
13
76
|
export async function installSkill(skillName, options, cwd = process.cwd()) {
|
|
14
|
-
const
|
|
77
|
+
const skillsDir = resolveFrameworkSkillsDir(cwd)
|
|
78
|
+
const skill = getSkill(skillName, skillsDir)
|
|
15
79
|
|
|
16
80
|
if (!skill) {
|
|
17
81
|
throw new Error(
|
|
18
|
-
`Unknown skill "${skillName}". Available skills: ${listSkillNames().join(', ')}`,
|
|
82
|
+
`Unknown skill "${skillName}". Available skills: ${listSkillNames(skillsDir).join(', ')}`,
|
|
19
83
|
)
|
|
20
84
|
}
|
|
21
85
|
|
package/lib/skills/registry.mjs
CHANGED
|
@@ -1,17 +1,34 @@
|
|
|
1
|
-
import
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
import { resolveFrameworkSkillsDir } from './framework-source.mjs'
|
|
5
|
+
|
|
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.
|
|
9
|
+
|
|
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()
|
|
9
19
|
}
|
|
10
20
|
|
|
11
|
-
export function
|
|
12
|
-
return
|
|
21
|
+
export function listSkills(skillsDir = resolveFrameworkSkillsDir()) {
|
|
22
|
+
return listSkillNames(skillsDir).map((name) => ({
|
|
23
|
+
name,
|
|
24
|
+
sourceDir: path.join(skillsDir, name),
|
|
25
|
+
}))
|
|
13
26
|
}
|
|
14
27
|
|
|
15
|
-
export function
|
|
16
|
-
|
|
17
|
-
|
|
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 }
|
|
34
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wabot-dev/create",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "2.0.0-beta.0",
|
|
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,32 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: wabot-framework
|
|
3
|
-
description: Use when building, reviewing, or debugging apps with @wabot-dev/framework. This skill is self-contained and includes offline examples for project setup, dependency injection, mindsets, external function modules, validation, persistence, chat controllers, REST and socket controllers, async commands, cron, auth, logger, and locking.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Wabot Framework
|
|
7
|
-
|
|
8
|
-
Use this skill for Wabot application work. It is self-contained: read the bundled examples first and do not depend on web access for normal tasks.
|
|
9
|
-
|
|
10
|
-
## How To Use
|
|
11
|
-
|
|
12
|
-
Load the reference file that matches the request:
|
|
13
|
-
|
|
14
|
-
- `references/quickstart.md` for project bootstrap and first run
|
|
15
|
-
- `references/core-patterns.md` for dependency injection and service lifecycles
|
|
16
|
-
- `references/mindset-and-chat.md` for bot personality, tools, and chat routing
|
|
17
|
-
- `references/validation-and-data.md` for DTOs, validation, and `Mapper`
|
|
18
|
-
- `references/persistence.md` for entities, repositories, and PostgreSQL persistence
|
|
19
|
-
- `references/controllers-and-auth.md` for REST, socket, and JWT auth
|
|
20
|
-
- `references/async-and-cron.md` for commands and scheduled work
|
|
21
|
-
- `references/ops.md` for logger and locking
|
|
22
|
-
- `references/full-example.md` for an end-to-end sales bot app
|
|
23
|
-
|
|
24
|
-
## Rules
|
|
25
|
-
|
|
26
|
-
- Keep prose in Spanish when explaining to the team.
|
|
27
|
-
- Keep code identifiers in English.
|
|
28
|
-
- Use `@wabot-dev/framework` imports in examples.
|
|
29
|
-
- Do not invent APIs. If a detail is not covered by the bundled examples, say what is missing instead of guessing.
|
|
30
|
-
- Keep controllers thin and push business logic into services or modules.
|
|
31
|
-
- Use `@scoped()` only when request, chat, or socket isolation matters.
|
|
32
|
-
- Prefer the smallest example that demonstrates the pattern.
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
# Async And Cron
|
|
2
|
-
|
|
3
|
-
Use this when scheduling work in the background or recurring jobs.
|
|
4
|
-
|
|
5
|
-
## Command and handler
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
import { command, commandHandler, type ICommandHandler } from '@wabot-dev/framework'
|
|
9
|
-
import { isString } from '@wabot-dev/framework'
|
|
10
|
-
|
|
11
|
-
@command('save-test-tag')
|
|
12
|
-
export class SaveTestTag {
|
|
13
|
-
@isString()
|
|
14
|
-
value!: string
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
@commandHandler(SaveTestTag)
|
|
18
|
-
export class SaveTestTagHandler implements ICommandHandler<SaveTestTag> {
|
|
19
|
-
constructor(private tags: TagRepository) {}
|
|
20
|
-
|
|
21
|
-
async handle(input: SaveTestTag) {
|
|
22
|
-
await this.tags.save(input.value)
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
## Run or schedule a command
|
|
28
|
-
|
|
29
|
-
```typescript
|
|
30
|
-
import { Async } from '@wabot-dev/framework'
|
|
31
|
-
|
|
32
|
-
const job = await async.runCommand(SaveTestTag, { value: 'hello' })
|
|
33
|
-
await async.scheduleCommand(SaveTestTag, { value: 'later' }, { minutes: 10 })
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
## Cron handler
|
|
37
|
-
|
|
38
|
-
```typescript
|
|
39
|
-
import { cron } from '@wabot-dev/framework'
|
|
40
|
-
|
|
41
|
-
@cron({ name: 'sync-leads', cron: '0 * * * *' })
|
|
42
|
-
export class SyncLeadsCron {
|
|
43
|
-
constructor(private leadService: LeadService) {}
|
|
44
|
-
|
|
45
|
-
async handle() {
|
|
46
|
-
await this.leadService.sync()
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
## Good practice
|
|
52
|
-
|
|
53
|
-
- Use commands for one-off background work.
|
|
54
|
-
- Use cron for repeated schedules.
|
|
55
|
-
- Keep command DTOs validated and small.
|
|
56
|
-
- Keep cron handlers idempotent because they can run many times.
|
|
57
|
-
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
# Controllers And Auth
|
|
2
|
-
|
|
3
|
-
Use this when exposing HTTP endpoints, socket events, or protected routes.
|
|
4
|
-
|
|
5
|
-
## REST controller
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
import { IncomingMessage } from 'http'
|
|
9
|
-
import { onPost, restController } from '@wabot-dev/framework'
|
|
10
|
-
|
|
11
|
-
class PaymentService {
|
|
12
|
-
processWebhook(rawBody: string) {
|
|
13
|
-
return { ok: true, rawBody }
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
@restController('/webhooks/bold')
|
|
18
|
-
export class BoldWebhookController {
|
|
19
|
-
constructor(private paymentService: PaymentService) {}
|
|
20
|
-
|
|
21
|
-
@onPost({
|
|
22
|
-
disableJsonParser: true,
|
|
23
|
-
disableUrlEncodedParser: true,
|
|
24
|
-
})
|
|
25
|
-
async receiveWebhook(request: IncomingMessage) {
|
|
26
|
-
const rawBody = await this.readRawBody(request)
|
|
27
|
-
return this.paymentService.processWebhook(rawBody)
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
## Socket controller
|
|
33
|
-
|
|
34
|
-
```typescript
|
|
35
|
-
import { jwtHandshakeGuard, onSocketEvent, socketController } from '@wabot-dev/framework'
|
|
36
|
-
|
|
37
|
-
@jwtHandshakeGuard()
|
|
38
|
-
@socketController('/sales')
|
|
39
|
-
export class SalesSocketController {
|
|
40
|
-
@onSocketEvent('message')
|
|
41
|
-
async onMessage(payload: { text: string }) {
|
|
42
|
-
return { ok: true, received: payload.text }
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
## JWT and API key auth
|
|
48
|
-
|
|
49
|
-
Use JWT when the client already has a signed token. Use API keys when the client should present a shared secret.
|
|
50
|
-
|
|
51
|
-
```typescript
|
|
52
|
-
import {
|
|
53
|
-
apiKeyGuard,
|
|
54
|
-
apiKeyHandshakeGuard,
|
|
55
|
-
jwtGuard,
|
|
56
|
-
jwtHandshakeGuard,
|
|
57
|
-
onGet,
|
|
58
|
-
onPost,
|
|
59
|
-
restController,
|
|
60
|
-
socketController,
|
|
61
|
-
} from '@wabot-dev/framework'
|
|
62
|
-
|
|
63
|
-
class CreateLeadDto {
|
|
64
|
-
name!: string
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
class LeadService {
|
|
68
|
-
async create(body: CreateLeadDto) {
|
|
69
|
-
return { created: true, lead: body }
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
@restController('/api/leads')
|
|
74
|
-
export class LeadApiController {
|
|
75
|
-
constructor(private leadService: LeadService) {}
|
|
76
|
-
|
|
77
|
-
@apiKeyGuard()
|
|
78
|
-
@onGet()
|
|
79
|
-
async listLeads() {
|
|
80
|
-
return []
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
@jwtGuard()
|
|
84
|
-
@onPost()
|
|
85
|
-
async createLead(body: CreateLeadDto) {
|
|
86
|
-
return this.leadService.create(body)
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
@socketController('/lead-room')
|
|
91
|
-
@jwtHandshakeGuard()
|
|
92
|
-
export class LeadRoomSocketController {}
|
|
93
|
-
|
|
94
|
-
@apiKeyHandshakeGuard()
|
|
95
|
-
@socketController('/api-room')
|
|
96
|
-
export class ApiRoomSocketController {}
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
## Rule of thumb
|
|
100
|
-
|
|
101
|
-
- REST for request/response workflows, webhooks, and admin APIs.
|
|
102
|
-
- Socket for live events and long-lived sessions.
|
|
103
|
-
- JWT when the client already has a signed token.
|
|
104
|
-
- API key when the client should present a shared secret.
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
# Core Patterns
|
|
2
|
-
|
|
3
|
-
Use this when deciding how to register and inject services.
|
|
4
|
-
|
|
5
|
-
## Lifecycles
|
|
6
|
-
|
|
7
|
-
| Decorator | Use for | Example |
|
|
8
|
-
| --- | --- | --- |
|
|
9
|
-
| `@singleton()` | Shared clients, repositories, config, caches | one DB repository for the app |
|
|
10
|
-
| `@injectable()` | Stateless services and modules | calculators, adapters, helpers |
|
|
11
|
-
| `@scoped(Lifecycle.ContainerScoped)` | Request/chat/socket-local state | auth context, request context |
|
|
12
|
-
|
|
13
|
-
## Singleton example
|
|
14
|
-
|
|
15
|
-
```typescript
|
|
16
|
-
import { singleton } from '@wabot-dev/framework'
|
|
17
|
-
|
|
18
|
-
@singleton()
|
|
19
|
-
export class ProductRepository {
|
|
20
|
-
private products = new Map<string, string>()
|
|
21
|
-
|
|
22
|
-
save(id: string, name: string) {
|
|
23
|
-
this.products.set(id, name)
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
## Injectable example
|
|
29
|
-
|
|
30
|
-
```typescript
|
|
31
|
-
import { injectable } from '@wabot-dev/framework'
|
|
32
|
-
|
|
33
|
-
@injectable()
|
|
34
|
-
export class QuoteCalculator {
|
|
35
|
-
total(base: number, discount: number) {
|
|
36
|
-
return base - base * discount
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
## Scoped example
|
|
42
|
-
|
|
43
|
-
```typescript
|
|
44
|
-
import { Lifecycle, scoped } from '@wabot-dev/framework'
|
|
45
|
-
|
|
46
|
-
@scoped(Lifecycle.ContainerScoped)
|
|
47
|
-
export class RequestContext {
|
|
48
|
-
private userId?: string
|
|
49
|
-
|
|
50
|
-
setUserId(userId: string) {
|
|
51
|
-
this.userId = userId
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
requireUserId() {
|
|
55
|
-
if (!this.userId) throw new Error('Unauthorized')
|
|
56
|
-
return this.userId
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
## Token injection
|
|
62
|
-
|
|
63
|
-
```typescript
|
|
64
|
-
import { container, inject, injectable } from '@wabot-dev/framework'
|
|
65
|
-
|
|
66
|
-
container.register('SMTP_HOST', { useValue: 'smtp.example.com' })
|
|
67
|
-
container.register('SMTP_PORT', { useValue: 587 })
|
|
68
|
-
|
|
69
|
-
@injectable()
|
|
70
|
-
export class Mailer {
|
|
71
|
-
constructor(
|
|
72
|
-
@inject('SMTP_HOST') private smtpHost: string,
|
|
73
|
-
@inject('SMTP_PORT') private smtpPort: number,
|
|
74
|
-
) {}
|
|
75
|
-
}
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
## Rule of thumb
|
|
79
|
-
|
|
80
|
-
- Use singleton for shared infrastructure.
|
|
81
|
-
- Use injectable for pure business logic.
|
|
82
|
-
- Use scoped only when state must not leak across requests or chats.
|
|
83
|
-
|