@pikku/skills 0.12.4 → 0.12.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/skills.gen.js +1 -1
  2. package/package.json +1 -1
  3. package/skills/pikku-addon/SKILL.md +56 -29
  4. package/skills/pikku-ai-agent/SKILL.md +197 -105
  5. package/skills/pikku-ai-vercel/SKILL.md +57 -18
  6. package/skills/pikku-ai-voice/SKILL.md +126 -52
  7. package/skills/pikku-audit/SKILL.md +35 -13
  8. package/skills/pikku-aws/SKILL.md +66 -16
  9. package/skills/pikku-backblaze/SKILL.md +44 -11
  10. package/skills/pikku-better-auth/SKILL.md +45 -10
  11. package/skills/pikku-cli/SKILL.md +67 -18
  12. package/skills/pikku-cli/references/complete-example.md +2 -0
  13. package/skills/pikku-concepts/SKILL.md +75 -10
  14. package/skills/pikku-config/SKILL.md +56 -14
  15. package/skills/pikku-cron/SKILL.md +13 -6
  16. package/skills/pikku-deploy-azure/SKILL.md +83 -28
  17. package/skills/pikku-deploy-cloudflare/SKILL.md +79 -37
  18. package/skills/pikku-deploy-express/SKILL.md +40 -4
  19. package/skills/pikku-deploy-fastify/SKILL.md +22 -1
  20. package/skills/pikku-deploy-lambda/SKILL.md +99 -19
  21. package/skills/pikku-deploy-nextjs/SKILL.md +49 -5
  22. package/skills/pikku-deploy-uws/SKILL.md +54 -1
  23. package/skills/pikku-deps/SKILL.md +29 -8
  24. package/skills/pikku-emails/SKILL.md +36 -5
  25. package/skills/pikku-fabric/SKILL.md +27 -2
  26. package/skills/pikku-fabric-debug/SKILL.md +5 -1
  27. package/skills/pikku-feature/SKILL.md +6 -1
  28. package/skills/pikku-gateway-slack/SKILL.md +72 -11
  29. package/skills/pikku-http/SKILL.md +18 -5
  30. package/skills/pikku-http/references/http-options.md +10 -5
  31. package/skills/pikku-i18n/SKILL.md +18 -7
  32. package/skills/pikku-info/SKILL.md +18 -8
  33. package/skills/pikku-jose/SKILL.md +35 -6
  34. package/skills/pikku-kysely/SKILL.md +78 -15
  35. package/skills/pikku-machine-auth/SKILL.md +36 -1
  36. package/skills/pikku-mcp/SKILL.md +159 -149
  37. package/skills/pikku-middleware/SKILL.md +17 -5
  38. package/skills/pikku-mongodb/SKILL.md +10 -2
  39. package/skills/pikku-n8n-import/SKILL.md +14 -6
  40. package/skills/pikku-permissions/SKILL.md +102 -22
  41. package/skills/pikku-pino/SKILL.md +12 -4
  42. package/skills/pikku-product-second-opinion/SKILL.md +3 -3
  43. package/skills/pikku-queue/SKILL.md +45 -16
  44. package/skills/pikku-react/SKILL.md +41 -14
  45. package/skills/pikku-react-query/SKILL.md +14 -10
  46. package/skills/pikku-realtime/SKILL.md +44 -22
  47. package/skills/pikku-redis/SKILL.md +12 -3
  48. package/skills/pikku-rpc/SKILL.md +23 -12
  49. package/skills/pikku-rtl/SKILL.md +21 -17
  50. package/skills/pikku-scenario/SKILL.md +108 -75
  51. package/skills/pikku-schedule/SKILL.md +39 -6
  52. package/skills/pikku-schema-ajv/SKILL.md +24 -2
  53. package/skills/pikku-schema-cfworker/SKILL.md +22 -2
  54. package/skills/pikku-security/SKILL.md +54 -9
  55. package/skills/pikku-services/SKILL.md +49 -9
  56. package/skills/pikku-services/references/audit-wire-service.md +2 -1
  57. package/skills/pikku-template-clone/SKILL.md +10 -5
  58. package/skills/pikku-trigger/SKILL.md +50 -6
  59. package/skills/pikku-versioning/SKILL.md +46 -17
  60. package/skills/pikku-websocket/SKILL.md +72 -44
  61. package/skills/pikku-workflow/SKILL.md +35 -1
  62. package/skills/pikku-workflow/references/workflow-reference.md +13 -8
  63. package/skills/pikku-workflows-client/SKILL.md +13 -6
  64. package/skills/pikku-ws/SKILL.md +44 -8
@@ -6,7 +6,8 @@ description: >-
6
6
  generated WebSocket client. TRIGGER when: code uses wireChannel, user asks about WebSocket,
7
7
  real-time, live updates, chat, pub/sub, or the generated WebSocket client. DO NOT TRIGGER when:
8
8
  user asks about HTTP/REST (use pikku-http), SSE (use pikku-http with sse: true), or WebSocket
9
- deployment specifics (use pikku-deploy-uws).
9
+ deployment specifics (use pikku-deploy-uws), or typed pub/sub events (use pikku-realtime).
10
+ installGroups: [core]
10
11
  ---
11
12
 
12
13
  # Pikku WebSocket Wiring
@@ -41,19 +42,32 @@ import { wireChannel } from '@pikku/core/channel'
41
42
 
42
43
  wireChannel({
43
44
  name: string, // Channel name (e.g. 'todos')
44
- onConnect: async () => {}, // Called when client connects
45
- onDisconnect: async () => {}, // Called when client disconnects
46
- onMessageWiring: { // Action → function mapping
47
- [actionName: string]: {
48
- func: PikkuFunc,
49
- auth?: boolean, // Override channel-level auth
50
- permissions?: Record<string, PikkuPermission | PikkuPermission[]>,
45
+ route: string, // REQUIRED — the URL path (e.g. '/todos')
46
+ auth?: boolean, // Channel-level auth default
47
+ onConnect?: PikkuFunc, // Called when client connects
48
+ onDisconnect?: PikkuFunc, // Called when client disconnects
49
+ onMessage?: PikkuFunc, // Catch-all for unrouted messages
50
+ onMessageWiring?: { // TWO levels — see below
51
+ [messageField: string]: {
52
+ [fieldValue: string]: {
53
+ func: PikkuFunc,
54
+ auth?: boolean, // Override channel-level auth
55
+ middleware?: PikkuMiddleware[],
56
+ }
51
57
  }
52
58
  },
59
+ middleware?: PikkuMiddleware[],
53
60
  channelMiddleware?: PikkuChannelMiddleware[],
61
+ binary?: boolean | null,
62
+ onBinaryMessage?: (services, data, channel) => ...,
63
+ tags?: string[], // Targets tag middleware
54
64
  })
55
65
  ```
56
66
 
67
+ Note there is **no `permissions` key on a message wiring** — wire-level
68
+ permissions were removed in #972. Authorization lives on the function's own
69
+ `permissions` field (see `pikku-permissions`).
70
+
57
71
  ### `pikkuChannelMiddleware(fn)`
58
72
 
59
73
  ```typescript
@@ -78,37 +92,46 @@ addChannelMiddleware('todos', [addTimestamp, filterSensitive])
78
92
  ```typescript
79
93
  wireChannel({
80
94
  name: 'todos',
81
- onConnect: async () => {},
82
- onDisconnect: async () => {},
95
+ route: '/todos',
83
96
  onMessageWiring: {
84
- create: { func: createTodo },
85
- list: { func: listTodos, auth: false },
97
+ action: { // ← the field to route on
98
+ create: { func: createTodo }, // ← its possible values
99
+ list: { func: listTodos, auth: false },
100
+ },
86
101
  },
87
102
  })
88
103
  ```
89
104
 
90
105
  ### Action Routing with Auth
91
106
 
92
- Clients send `{ action: 'create', data: {...} }`. Pikku routes to the matching function.
107
+ `onMessageWiring` nests two levels because the routing key is configurable. The
108
+ **outer** key names the field in the incoming message to dispatch on; the
109
+ **inner** keys are the values that field can take. With the conventional outer
110
+ key `action`, a client sending `{ action: 'create', data: {...} }` reaches
111
+ `createTodo` — but a CLI channel might route on `command` instead, which is why
112
+ the field is not hardcoded.
93
113
 
94
114
  ```typescript
95
- const authenticate = pikkuFunc({
115
+ const authenticate = pikkuSessionlessFunc({
96
116
  title: 'Authenticate',
97
- func: async ({ setSession }, { token }) => {
117
+ // setSession lives on the WIRE (third param), not on services
118
+ func: async (services, { token }, { setSession }) => {
98
119
  const session = await verifyJWT(token)
99
- setSession(session)
120
+ await setSession(session)
100
121
  return { success: true }
101
122
  },
102
123
  })
103
124
 
104
125
  wireChannel({
105
126
  name: 'todos',
106
- onConnect: async () => {},
107
- onDisconnect: async () => {},
127
+ route: '/todos',
128
+ auth: true,
108
129
  onMessageWiring: {
109
- auth: { func: authenticate, auth: false }, // No session required
110
- subscribe: { func: subscribeTodos }, // Session required
111
- create: { func: createTodo },
130
+ action: {
131
+ authenticate: { func: authenticate, auth: false }, // No session required
132
+ subscribe: { func: subscribeTodos }, // Session required
133
+ create: { func: createTodo },
134
+ },
112
135
  },
113
136
  })
114
137
  ```
@@ -120,25 +143,28 @@ Use EventHub for real-time broadcasting across connections:
120
143
  ```typescript
121
144
  wireChannel({
122
145
  name: 'todos',
123
- onConnect: async ({ eventHub, channel }) => {
146
+ route: '/todos',
147
+ // eventHub is a service (1st param); channel lives on the wire (3rd)
148
+ onConnect: async ({ eventHub }, _data, { channel }) => {
124
149
  eventHub.subscribe('todos:updated', (data) => {
125
150
  channel.send(data)
126
151
  })
127
152
  },
128
- onDisconnect: async () => {},
129
153
  onMessageWiring: {
130
- create: {
131
- func: pikkuFunc({
132
- title: 'Create Todo',
133
- func: async ({ db, eventHub }, { text }) => {
134
- const todo = await db.createTodo({ text })
135
- eventHub.publish('todos:updated', {
136
- event: 'created',
137
- todo,
138
- })
139
- return { todo }
140
- },
141
- }),
154
+ action: {
155
+ create: {
156
+ func: pikkuFunc({
157
+ title: 'Create Todo',
158
+ func: async ({ db, eventHub }, { text }) => {
159
+ const todo = await db.createTodo({ text })
160
+ eventHub.publish('todos:updated', {
161
+ event: 'created',
162
+ todo,
163
+ })
164
+ return { todo }
165
+ },
166
+ }),
167
+ },
142
168
  },
143
169
  },
144
170
  })
@@ -167,9 +193,8 @@ addChannelMiddleware('todos', [addTimestamp, filterSensitive])
167
193
  // Or inline on wiring
168
194
  wireChannel({
169
195
  name: 'todos',
196
+ route: '/todos',
170
197
  channelMiddleware: [addTimestamp],
171
- onConnect: async () => {},
172
- onDisconnect: async () => {},
173
198
  onMessageWiring: { ... },
174
199
  })
175
200
  ```
@@ -179,7 +204,7 @@ wireChannel({
179
204
  After `npx pikku all`:
180
205
 
181
206
  ```typescript
182
- import { PikkuWebSocket } from '.pikku/pikku-websocket.gen.js'
207
+ import { PikkuWebSocket } from '#pikku/pikku-websocket.gen.js'
183
208
 
184
209
  const pikku = new PikkuWebSocket(ws)
185
210
  const todosRoute = pikku.getRoute('todos')
@@ -197,7 +222,7 @@ todosRoute.subscribe('todos:updated', (data) => {
197
222
 
198
223
  ```typescript
199
224
  // functions/chat.functions.ts
200
- export const authenticate = pikkuFunc({
225
+ export const authenticate = pikkuSessionlessFunc({
201
226
  title: 'Authenticate',
202
227
  func: async ({ jwt }, { token }, { setSession }) => {
203
228
  const payload = await jwt.verify(token)
@@ -228,16 +253,19 @@ export const listMessages = pikkuSessionlessFunc({
228
253
  // wirings/chat.channel.ts
229
254
  wireChannel({
230
255
  name: 'chat',
231
- onConnect: async ({ eventHub, channel }) => {
256
+ route: '/chat',
257
+ auth: true,
258
+ onConnect: async ({ eventHub }, _data, { channel }) => {
232
259
  eventHub.subscribe('chat:message', (data) => {
233
260
  channel.send(data)
234
261
  })
235
262
  },
236
- onDisconnect: async () => {},
237
263
  onMessageWiring: {
238
- auth: { func: authenticate, auth: false },
239
- send: { func: sendMessage },
240
- history: { func: listMessages, auth: false },
264
+ action: {
265
+ authenticate: { func: authenticate, auth: false },
266
+ send: { func: sendMessage },
267
+ history: { func: listMessages, auth: false },
268
+ },
241
269
  },
242
270
  })
243
271
  ```
@@ -121,8 +121,42 @@ await workflow.sleep('Wait 5 minutes', '5min')
121
121
 
122
122
  // Suspend — pause until externally resumed (e.g. awaiting approval), then continue
123
123
  await workflow.suspend('Awaiting approval')
124
+
125
+ // Approval — suspend for a human decision and resume with the answer
126
+ await workflow.approval('Manager sign-off', { ... })
124
127
  ```
125
128
 
129
+ `workflow.name`, `workflow.runId` and `await workflow.getRun()` identify the
130
+ current run if a step needs to reference it.
131
+
132
+ ### Error handling: `onError`, never try/catch
133
+
134
+ **Do not wrap steps in try/catch.** The DSL extractor serialises the body into a
135
+ step graph, and a `catch` block is control flow it cannot represent — so the
136
+ graph would no longer describe what actually runs, which is the whole point of
137
+ the DSL mode. This is a settled design decision, not a temporary limitation.
138
+
139
+ Use the `onError` step option instead: it names an RPC to invoke when the step
140
+ has failed *after* exhausting its retries.
141
+
142
+ ```typescript
143
+ await workflow.do('Charge', 'chargePayment', { orderId }, {
144
+ retries: 3,
145
+ retryDelay: '1s',
146
+ onError: 'refundReservation', // compensation RPC
147
+ })
148
+ ```
149
+
150
+ The handler receives `{ error: { message } }`, and the original error is still
151
+ thrown afterwards — so the workflow still fails. `onError` is **compensation, not
152
+ recovery**: it exists to undo work, not to swallow the failure and carry on. If
153
+ you genuinely need to branch on a failure, have the step return a result object
154
+ (`{ success: false, reason }`) and branch on that, the way the `processOrder`
155
+ example branches on `payment.success`.
156
+
157
+ Full step options: `description`, `retries`, `retryDelay`, `onError` (plus
158
+ `actor`, which is scenario-only — see `pikku-scenario`).
159
+
126
160
  ### Parallel fan-out
127
161
 
128
162
  ```typescript
@@ -161,7 +195,7 @@ export const userOnboarding = pikkuWorkflowGraph({
161
195
 
162
196
  ## Step dispatch & HTTP wiring
163
197
 
164
- For per-step inline-vs-queue dispatch (`inline: false` and the `dispatchStep` rules), the manual `workflowStart`/`workflow`/`workflowStatus` HTTP wirings, and a suspend/resume example, read `references/workflow-reference.md`.
198
+ For per-step inline-vs-queue dispatch (`workflowQueued: true` and the `dispatchStep` rules), the manual `workflowStart`/`workflow`/`workflowStatus` HTTP wirings, and a suspend/resume example, read `references/workflow-reference.md`.
165
199
 
166
200
  ## After writing
167
201
 
@@ -2,31 +2,36 @@
2
2
 
3
3
  ## Step execution: inline vs queue dispatch
4
4
 
5
- Whether a step runs **inline** (same process/session, no queue round-trip) or is **dispatched to the queue** is decided **purely by the step's function** — there is no workflow-level or per-call `inline` flag. `workflow.do(...)` options are only `retries`/`retryDelay`/`description`.
5
+ Whether a step runs **inline** (same process/session, no queue round-trip) or is **dispatched to the queue** is decided **purely by the step's function** — there is no workflow-level or per-call dispatch flag. `workflow.do(...)` options are only `description`/`retries`/`retryDelay`/`onError`.
6
6
 
7
7
  - **Steps default to inline.** Most steps don't need their own worker; running them inline avoids a queue round-trip per step, so a normally-started workflow executes its whole chain in one orchestrator pass.
8
- - **`inline: false` opts a function out.** Set `inline: false` on the **function config** (`pikkuFunc` / `pikkuSessionlessFunc`, same level as `auth`/`expose`) to dispatch that step via the queue — for expensive/long-running steps that deserve their own worker, retry isolation, and concurrency limits.
8
+ - **`workflowQueued: true` opts a function out.** Set it on the **function config** (`pikkuFunc` / `pikkuSessionlessFunc`, same level as `auth`/`expose`) to dispatch that step via the queue — for expensive/long-running steps that deserve their own worker, retry isolation, and concurrency limits. `workflowRetries` and `workflowTimeout` sit alongside it.
9
9
  - **Run-level `inline` is separate** and only controls whether the *whole run* executes in-process without queue infrastructure (set automatically when there is no `queueService`, or via `startWorkflow(..., { inline: true })`). It governs sleep handling, not per-step dispatch.
10
10
 
11
11
  The rule (`dispatchStep`):
12
12
 
13
- | Function `inline` | `queueService` present? | Result |
13
+ | Function `workflowQueued` | `queueService` present? | Result |
14
14
  |---|---|---|
15
- | default / `true` | any | **inline** |
16
- | `false` | yes | **queued** (own worker) |
17
- | `false` | no | **inline + a `logger.warn`** (misconfiguration: can't dispatch) |
15
+ | default / `false` | any | **inline** |
16
+ | `true` | yes | **queued** (own worker) |
17
+ | `true` | no | **throws** |
18
18
 
19
19
  ```typescript
20
20
  // Push this one expensive step onto the queue; every other step stays inline:
21
21
  export const renderLargeReport = pikkuSessionlessFunc({
22
- inline: false, // dispatch via queue instead of running inline
22
+ workflowQueued: true, // dispatch via queue instead of running inline
23
+ workflowRetries: 3,
24
+ workflowTimeout: '5m',
23
25
  input: ReportInput,
24
26
  output: ReportOutput,
25
27
  func: async (services, data) => { /* ... */ },
26
28
  })
27
29
  ```
28
30
 
29
- `inline: false` requires a `queueService`; without one the step still runs (so the workflow progresses) but emits a `logger.warn` so the misconfiguration is visible.
31
+ `workflowQueued: true` **requires** a `queueService`. Without one the step throws
32
+ rather than quietly running inline — a step marked for its own worker usually
33
+ carries timeout and concurrency expectations that inline execution would silently
34
+ violate, so failing loudly is safer than proceeding.
30
35
 
31
36
  ## HTTP workflow wiring (manual)
32
37
 
@@ -16,8 +16,8 @@ Use this skill as an execution checklist, not reference material.
16
16
  4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.
17
17
  5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.
18
18
 
19
- When a project has `pikkuWorkflowGraph` workflows, three React Query
20
- hooks are auto-generated alongside the standard RPC hooks. They handle
19
+ When a project defines any workflow — DSL or `pikkuWorkflowGraph` — three
20
+ React Query hooks are auto-generated alongside the standard RPC hooks. They handle
21
21
  the two common shapes: **run-and-wait** (short workflows where the
22
22
  client waits for the result) and **fire-and-poll** (long workflows where
23
23
  the client gets a `runId` and polls status).
@@ -103,8 +103,14 @@ function VideoStatus({ runId }: { runId: string }) {
103
103
 
104
104
  Status values: `'running' | 'suspended' | 'completed' | 'failed' | 'cancelled'`.
105
105
 
106
- The hook stops auto-polling when the run reaches a terminal state (set
107
- `refetchInterval` to false in those cases — pattern shown above).
106
+ **Stopping the poll is your job.** The hook adds no terminal-state logic of its
107
+ own — the `refetchInterval` callback above is what ends it, by returning `false`
108
+ once `status` is no longer `running`. Leave that out and a finished run keeps
109
+ being polled forever.
110
+
111
+ The hook is disabled until `runId` is set, so passing `undefined` while the run
112
+ has not started yet is the intended shape rather than something to guard around.
113
+ That `enabled` is owned by the hook and cannot be overridden through `options`.
108
114
 
109
115
  ## Putting it together — start + observe
110
116
 
@@ -140,8 +146,9 @@ docs).
140
146
 
141
147
  ## What NOT to do
142
148
 
143
- - Don't poll status manually — use `useWorkflowStatus` with
144
- `refetchInterval`. It dedupes and stops on terminal states.
149
+ - Don't poll status manually with `setInterval` — use `useWorkflowStatus`
150
+ with a `refetchInterval` callback, which dedupes across components and
151
+ lets you stop on a terminal state in one place.
145
152
  - Don't call `useRunWorkflow` for workflows that take more than a few
146
153
  seconds. The user-facing component will hold a long-running pending
147
154
  state with no progress indication; use start + status instead.
@@ -31,17 +31,53 @@ yarn add @pikku/ws ws
31
31
 
32
32
  ### Basic Setup
33
33
 
34
+ The package exports one function, `pikkuWebsocketHandler` — there is no server
35
+ class. You own the `http.Server` and the `WebSocketServer`; the handler attaches
36
+ the upgrade and message plumbing to them.
37
+
34
38
  ```typescript
35
- import { PikkuWSServer } from '@pikku/ws'
39
+ import { pikkuWebsocketHandler } from '@pikku/ws'
40
+ import { stopSingletonServices } from '@pikku/core'
41
+ import { Server } from 'http'
42
+ import { WebSocketServer } from 'ws'
43
+
44
+ import '../.pikku/pikku-bootstrap.gen.js'
45
+ import { createConfig, createSingletonServices } from './services.js'
46
+
47
+ const config = await createConfig()
48
+ const singletonServices = await createSingletonServices(config)
49
+
50
+ const server = new Server()
51
+ const wss = new WebSocketServer({ noServer: true })
36
52
 
37
- const wsServer = new PikkuWSServer({
38
- server: httpServer, // Node.js HTTP server
39
- singletonServices,
40
- createWireServices,
41
- channelStore,
53
+ pikkuWebsocketHandler({
54
+ server,
55
+ wss,
56
+ logger: singletonServices.logger,
57
+ logRoutes: true, // print the wired channels at startup
58
+ loadSchemas: true, // compile input schemas up front
42
59
  })
43
60
 
44
- await wsServer.init()
61
+ server.listen(4002, 'localhost')
45
62
  ```
46
63
 
47
- This runtime bridges the `ws` WebSocket library with Pikku's channel wiring. See `pikku-websocket` for channel wiring details and `pikku-deploy-fastify`/`pikku-deploy-express` for integrating with HTTP servers.
64
+ `noServer: true` is not optional decoration — the handler performs the upgrade
65
+ itself so it can run pikku's HTTP middleware chain (auth, cors) against the
66
+ upgrade request before a channel exists. Letting `ws` bind the server directly
67
+ would skip that.
68
+
69
+ Services come from the bootstrap import and the global singleton registry, which
70
+ is why nothing is passed in. The event hub is taken from
71
+ `singletonServices.eventHub` when it is a `LocalEventHubService`, and a local one
72
+ is created otherwise — so a single-process app gets pub/sub for free, while a
73
+ multi-instance deployment must register a distributed hub (see `pikku-realtime`).
74
+
75
+ The options type also extends `RunHTTPWiringOptions`, so per-request settings
76
+ such as `respondWith404`, `coerceDataFromSchema` and `bubbleErrors` are accepted
77
+ here too.
78
+
79
+ On shutdown, call `stopSingletonServices()` then close `wss` and `server`.
80
+
81
+ See `pikku-websocket` for channel wiring details, and
82
+ `pikku-deploy-fastify`/`pikku-deploy-express` when the WebSocket server shares a
83
+ port with an HTTP app.