@wabot-dev/create 0.0.2 → 0.0.3

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.
@@ -0,0 +1,179 @@
1
+ import fs from 'fs/promises'
2
+ import os from 'os'
3
+ import path from 'path'
4
+
5
+ import { getSkill, listSkillNames } from './registry.mjs'
6
+
7
+ const supportedAgents = {
8
+ codex: ['.codex', 'skills'],
9
+ claude: ['.claude', 'skills'],
10
+ agents: ['.agents', 'skills'],
11
+ }
12
+
13
+ export async function installSkill(skillName, options, cwd = process.cwd()) {
14
+ const skill = getSkill(skillName)
15
+
16
+ if (!skill) {
17
+ throw new Error(
18
+ `Unknown skill "${skillName}". Available skills: ${listSkillNames().join(', ')}`,
19
+ )
20
+ }
21
+
22
+ const plan = await buildInstallPlan(skill, options, cwd)
23
+
24
+ for (const destination of plan.destinations) {
25
+ await fs.mkdir(destination.baseDir, { recursive: true })
26
+ await fs.cp(skill.sourceDir, destination.installDir, {
27
+ recursive: true,
28
+ force: false,
29
+ errorOnExist: true,
30
+ })
31
+ }
32
+
33
+ return {
34
+ skill,
35
+ mode: plan.mode,
36
+ homeRoot: plan.homeRoot,
37
+ installations: plan.destinations.map((destination) => ({
38
+ agent: destination.agent,
39
+ installDir: destination.installDir,
40
+ })),
41
+ }
42
+ }
43
+
44
+ async function buildInstallPlan(skill, options, cwd) {
45
+ await assertPathExists(skill.sourceDir, `Packaged skill source not found at ${skill.sourceDir}`)
46
+
47
+ if (options.local && options.global) {
48
+ throw new Error('Choose only one install mode: --local or --global')
49
+ }
50
+
51
+ if (!options.global && (options.target || options.targets || options.home)) {
52
+ throw new Error('--home, --target, and --targets can only be used together with --global')
53
+ }
54
+
55
+ if (options.local) {
56
+ const installDir = path.join(cwd, '.agents', 'skills', skill.name)
57
+ await assertDestinationAvailable(installDir)
58
+
59
+ return {
60
+ mode: 'local',
61
+ homeRoot: null,
62
+ destinations: [
63
+ {
64
+ agent: 'agents',
65
+ baseDir: path.join(cwd, '.agents', 'skills'),
66
+ installDir,
67
+ },
68
+ ],
69
+ }
70
+ }
71
+
72
+ const homeRoot = resolveHomeRoot(options.home)
73
+ const targets = resolveTargets(options)
74
+ const destinations = targets.map((agent) => {
75
+ const baseDir = resolveAgentSkillsDir(homeRoot, agent)
76
+
77
+ return {
78
+ agent,
79
+ baseDir,
80
+ installDir: path.join(baseDir, skill.name),
81
+ }
82
+ })
83
+
84
+ await Promise.all(destinations.map((destination) => assertDestinationAvailable(destination.installDir)))
85
+
86
+ return {
87
+ mode: 'global',
88
+ homeRoot,
89
+ destinations,
90
+ }
91
+ }
92
+
93
+ function resolveTargets(options) {
94
+ const parsedTargets = []
95
+
96
+ if (options.target) {
97
+ parsedTargets.push(options.target)
98
+ }
99
+
100
+ if (options.targets) {
101
+ parsedTargets.push(
102
+ ...String(options.targets)
103
+ .split(',')
104
+ .map((target) => target.trim())
105
+ .filter(Boolean),
106
+ )
107
+ }
108
+
109
+ if (parsedTargets.length === 0) {
110
+ return ['codex']
111
+ }
112
+
113
+ const uniqueTargets = [...new Set(parsedTargets)]
114
+
115
+ if (uniqueTargets.length !== parsedTargets.length) {
116
+ throw new Error('Duplicate targets are not allowed in --target/--targets')
117
+ }
118
+
119
+ for (const target of uniqueTargets) {
120
+ if (!supportedAgents[target]) {
121
+ throw new Error(
122
+ `Unsupported target "${target}". Supported targets: ${Object.keys(supportedAgents).join(', ')}`,
123
+ )
124
+ }
125
+ }
126
+
127
+ return uniqueTargets
128
+ }
129
+
130
+ function resolveHomeRoot(explicitHome) {
131
+ if (explicitHome) {
132
+ return path.resolve(explicitHome)
133
+ }
134
+
135
+ const homeRoot = os.homedir()
136
+
137
+ if (!homeRoot) {
138
+ throw new Error('Unable to resolve a home directory. Pass --home <path> explicitly.')
139
+ }
140
+
141
+ return path.resolve(homeRoot)
142
+ }
143
+
144
+ function resolveAgentSkillsDir(homeRoot, agent) {
145
+ const segments = supportedAgents[agent]
146
+
147
+ if (!segments) {
148
+ throw new Error(
149
+ `Unsupported target "${agent}". Supported targets: ${Object.keys(supportedAgents).join(', ')}`,
150
+ )
151
+ }
152
+
153
+ return path.join(homeRoot, ...segments)
154
+ }
155
+
156
+ async function assertDestinationAvailable(installDir) {
157
+ try {
158
+ await fs.access(installDir)
159
+ throw new Error(`Skill already exists at ${installDir}`)
160
+ } catch (error) {
161
+ if (error?.code === 'ENOENT') {
162
+ return
163
+ }
164
+
165
+ throw error
166
+ }
167
+ }
168
+
169
+ async function assertPathExists(targetPath, errorMessage) {
170
+ try {
171
+ await fs.access(targetPath)
172
+ } catch (error) {
173
+ if (error?.code === 'ENOENT') {
174
+ throw new Error(errorMessage)
175
+ }
176
+
177
+ throw error
178
+ }
179
+ }
@@ -0,0 +1,17 @@
1
+ import { fileURLToPath } from 'url'
2
+
3
+ const skillDefinitions = {
4
+ 'wabot-framework': {
5
+ name: 'wabot-framework',
6
+ sourceDir: fileURLToPath(new URL('../../skills/wabot-framework', import.meta.url)),
7
+ description: 'Wabot Framework packaged skill',
8
+ },
9
+ }
10
+
11
+ export function getSkill(skillName) {
12
+ return skillDefinitions[skillName] ?? null
13
+ }
14
+
15
+ export function listSkillNames() {
16
+ return Object.keys(skillDefinitions).sort()
17
+ }
package/package.json CHANGED
@@ -1,19 +1,18 @@
1
1
  {
2
2
  "name": "@wabot-dev/create",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "Project creator for Wabot Framework",
5
5
  "license": "ISC",
6
6
  "author": "",
7
7
  "type": "module",
8
8
  "main": "index.js",
9
- "scripts": {
10
- "test": "echo \"Error: no test specified\" && exit 1"
11
- },
12
9
  "bin": {
13
10
  "create-wabot": "./bin/create-wabot.mjs"
14
11
  },
15
12
  "files": [
16
- "bin"
13
+ "bin",
14
+ "lib",
15
+ "skills"
17
16
  ],
18
17
  "dependencies": {
19
18
  "chalk": "^5.4.1",
@@ -0,0 +1,32 @@
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.
@@ -0,0 +1,5 @@
1
+ interface:
2
+ display_name: "Wabot Framework"
3
+ short_description: "Build apps with @wabot-dev/framework"
4
+ default_prompt: "Use this skill to implement, review, or debug Wabot applications using the bundled offline examples and reference files."
5
+
@@ -0,0 +1,57 @@
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
+
@@ -0,0 +1,104 @@
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.
@@ -0,0 +1,83 @@
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
+