@pikku/skills 0.12.2 → 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.
- package/dist/skills.gen.js +1 -1
- package/package.json +1 -1
- package/skills/pikku-addon/SKILL.md +56 -29
- package/skills/pikku-ai-agent/SKILL.md +197 -105
- package/skills/pikku-ai-vercel/SKILL.md +57 -18
- package/skills/pikku-ai-voice/SKILL.md +126 -52
- package/skills/pikku-audit/SKILL.md +35 -13
- package/skills/pikku-aws/SKILL.md +66 -16
- package/skills/pikku-backblaze/SKILL.md +44 -11
- package/skills/pikku-better-auth/SKILL.md +80 -34
- package/skills/pikku-cli/SKILL.md +67 -18
- package/skills/pikku-cli/references/complete-example.md +2 -0
- package/skills/pikku-concepts/SKILL.md +82 -10
- package/skills/pikku-concepts/references/concept-mapping.md +2 -2
- package/skills/pikku-config/SKILL.md +134 -52
- package/skills/pikku-cron/SKILL.md +13 -6
- package/skills/pikku-deploy-azure/SKILL.md +83 -28
- package/skills/pikku-deploy-cloudflare/SKILL.md +79 -37
- package/skills/pikku-deploy-express/SKILL.md +40 -4
- package/skills/pikku-deploy-fastify/SKILL.md +22 -1
- package/skills/pikku-deploy-lambda/SKILL.md +99 -19
- package/skills/pikku-deploy-nextjs/SKILL.md +49 -5
- package/skills/pikku-deploy-uws/SKILL.md +54 -1
- package/skills/pikku-deps/SKILL.md +29 -8
- package/skills/pikku-emails/SKILL.md +36 -5
- package/skills/pikku-fabric/SKILL.md +30 -5
- package/skills/pikku-fabric-debug/SKILL.md +5 -1
- package/skills/pikku-feature/SKILL.md +12 -7
- package/skills/pikku-gateway-slack/SKILL.md +72 -11
- package/skills/pikku-http/SKILL.md +18 -5
- package/skills/pikku-http/references/http-options.md +10 -5
- package/skills/pikku-i18n/SKILL.md +18 -7
- package/skills/pikku-info/SKILL.md +18 -8
- package/skills/pikku-jose/SKILL.md +35 -6
- package/skills/pikku-knowledge/SKILL.md +3 -3
- package/skills/pikku-kysely/SKILL.md +78 -15
- package/skills/pikku-machine-auth/SKILL.md +36 -1
- package/skills/pikku-mcp/SKILL.md +159 -149
- package/skills/pikku-middleware/SKILL.md +17 -5
- package/skills/pikku-mongodb/SKILL.md +10 -2
- package/skills/pikku-n8n-import/SKILL.md +14 -6
- package/skills/pikku-permissions/SKILL.md +102 -22
- package/skills/pikku-pino/SKILL.md +12 -4
- package/skills/pikku-product-second-opinion/SKILL.md +3 -3
- package/skills/pikku-queue/SKILL.md +45 -16
- package/skills/pikku-react/SKILL.md +41 -14
- package/skills/pikku-react-query/SKILL.md +14 -10
- package/skills/pikku-realtime/SKILL.md +44 -22
- package/skills/pikku-redis/SKILL.md +12 -3
- package/skills/pikku-rpc/SKILL.md +23 -12
- package/skills/pikku-rtl/SKILL.md +21 -17
- package/skills/pikku-scenario/SKILL.md +285 -50
- package/skills/pikku-schedule/SKILL.md +39 -6
- package/skills/pikku-schema-ajv/SKILL.md +24 -2
- package/skills/pikku-schema-cfworker/SKILL.md +22 -2
- package/skills/pikku-security/SKILL.md +54 -9
- package/skills/pikku-services/SKILL.md +49 -9
- package/skills/pikku-services/references/audit-wire-service.md +2 -1
- package/skills/pikku-software-archaeology/README.md +16 -6
- package/skills/pikku-software-archaeology/references/pikku-mapping.md +30 -30
- package/skills/pikku-template-clone/SKILL.md +10 -5
- package/skills/pikku-trigger/SKILL.md +50 -6
- package/skills/pikku-versioning/SKILL.md +46 -17
- package/skills/pikku-websocket/SKILL.md +72 -44
- package/skills/pikku-workflow/SKILL.md +35 -1
- package/skills/pikku-workflow/references/workflow-reference.md +13 -8
- package/skills/pikku-workflows-client/SKILL.md +13 -6
- package/skills/pikku-ws/SKILL.md +44 -8
|
@@ -126,12 +126,12 @@ in-app features don't.
|
|
|
126
126
|
- **Migrations are inline SQL files** in the project's migrations dir
|
|
127
127
|
(typically `sql/`). Use a numbered prefix matching existing files.
|
|
128
128
|
- **Secrets and env-vars: NEVER `process.env`.** Declare them with
|
|
129
|
-
`
|
|
130
|
-
zod schema for type-safe access. Read with
|
|
131
|
-
`services.
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
129
|
+
`defineSecret` (sensitive) or `defineVariable` (non-sensitive) — both with a
|
|
130
|
+
zod schema for type-safe access. Read variables with
|
|
131
|
+
`services.variables.get('NAME')`. Secrets are **not available in functions** —
|
|
132
|
+
read them in `services.ts` with `secrets.getSecret('NAME')` and pass the value
|
|
133
|
+
into the service the function uses. See the **pikku-config** skill for the full
|
|
134
|
+
pattern (including OAuth2 credentials). This applies even in `config.ts`.
|
|
135
135
|
|
|
136
136
|
### Conventions to copy from neighbours
|
|
137
137
|
|
|
@@ -218,10 +218,15 @@ branch.
|
|
|
218
218
|
## Stage 6 — Commit
|
|
219
219
|
|
|
220
220
|
```bash
|
|
221
|
-
git add
|
|
221
|
+
git add <the files you changed>
|
|
222
222
|
git commit -m "feat: <short title>"
|
|
223
223
|
```
|
|
224
224
|
|
|
225
|
+
Stage the files you actually touched, by path. `git add -A` / `git add .` also
|
|
226
|
+
sweeps up regenerated artifacts you didn't mean to commit and, where more than
|
|
227
|
+
one agent shares the checkout, another agent's in-progress work — which lands in
|
|
228
|
+
your branch and silently breaks theirs.
|
|
229
|
+
|
|
225
230
|
## Stage 7 — Hand off
|
|
226
231
|
|
|
227
232
|
Tell the user the branch name and how to review. Two options:
|
|
@@ -35,24 +35,67 @@ yarn add @pikku/gateway-slack @slack/web-api
|
|
|
35
35
|
```typescript
|
|
36
36
|
import { SlackGatewayAdapter } from '@pikku/gateway-slack'
|
|
37
37
|
|
|
38
|
-
const adapter = new SlackGatewayAdapter(
|
|
38
|
+
const adapter = new SlackGatewayAdapter({
|
|
39
|
+
signingSecret: string,
|
|
40
|
+
tokenResolver: (teamId: string) => Promise<string | null>,
|
|
41
|
+
})
|
|
39
42
|
```
|
|
40
43
|
|
|
41
44
|
Bridges Slack Events API webhooks with Pikku's gateway system for processing Slack events as Pikku functions.
|
|
42
45
|
|
|
46
|
+
**There is no `botToken` option.** One adapter serves every workspace, and the
|
|
47
|
+
bot token is resolved per `team_id` through `tokenResolver` — normally a lookup
|
|
48
|
+
against the row `exchangeSlackOAuthCode` wrote at install time. Returning `null`
|
|
49
|
+
throws for that event. `WebClient`s are cached per team, so call
|
|
50
|
+
`invalidateClient(teamId)` after a token rotation.
|
|
51
|
+
|
|
52
|
+
**Methods:**
|
|
53
|
+
|
|
54
|
+
- `verifyWebhook(data, request?)` — asserts the signature, then answers the `url_verification` challenge. It **fails closed**: no request access, missing headers, a stale timestamp, or an HMAC mismatch all throw `UnauthorizedError` before parse or the handler runs
|
|
55
|
+
- `parse(data)` — normalizes an `event_callback` into a `GatewayInboundMessage`, or returns `null` for anything to ignore
|
|
56
|
+
- `createBoundSend(teamId, channelId, threadTs?)` — the real send path
|
|
57
|
+
- `send(senderId, message)` — **a deliberate no-op.** The generic signature carries no channel context, and the gateway runner calls it for auto-send, so it swallows rather than throws. A reply written through it silently never reaches Slack
|
|
58
|
+
- `getClientForTeam(teamId)` / `invalidateClient(teamId)` / `close()`
|
|
59
|
+
|
|
60
|
+
`parse` returns `null` — meaning the event is dropped — for anything that isn't a
|
|
61
|
+
`message` or `app_mention`, for bot messages (loop prevention), for any subtype
|
|
62
|
+
other than `thread_broadcast`, and for events with no `user` or no `text`.
|
|
63
|
+
`metadata` carries `{ teamId, channelId, threadTs, messageTs, eventType }`, with
|
|
64
|
+
`threadTs` falling back to the message's own `ts` so replies always land
|
|
65
|
+
in-thread.
|
|
66
|
+
|
|
43
67
|
### `SlackGatewayHelper`
|
|
44
68
|
|
|
45
|
-
|
|
69
|
+
Wraps a parsed message plus the adapter and binds the channel/thread for you:
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
const slack = new SlackGatewayHelper(data, adapter)
|
|
73
|
+
await slack.sendText('Thinking…') // sends now
|
|
74
|
+
return slack.reply('Here is the answer') // auto-sent by the runner
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Also: `send(message)`, `replyBlocks(blocks)`, and the `channelId` / `threadTs` /
|
|
78
|
+
`teamId` getters.
|
|
46
79
|
|
|
47
80
|
### Slash Commands
|
|
48
81
|
|
|
49
82
|
```typescript
|
|
50
83
|
import { parseSlashCommand, respondToSlashCommand } from '@pikku/gateway-slack'
|
|
51
84
|
|
|
52
|
-
const command = parseSlashCommand(
|
|
53
|
-
|
|
85
|
+
const command = parseSlashCommand(data)
|
|
86
|
+
// { raw, subcommand, args, argsList, teamId, userId, channelId, triggerId, responseUrl }
|
|
87
|
+
await respondToSlashCommand(command.responseUrl, { text: 'Done!' })
|
|
54
88
|
```
|
|
55
89
|
|
|
90
|
+
The parsed result is camelCase — reach for `command.responseUrl`, not
|
|
91
|
+
`command.response_url`; the underlying snake_case payload is on `command.raw`.
|
|
92
|
+
`text` is split on whitespace: the first word becomes `subcommand`, the rest
|
|
93
|
+
`args`/`argsList`.
|
|
94
|
+
|
|
95
|
+
`respondToSlashCommand` posts to the `response_url` and **ignores the result** —
|
|
96
|
+
a rejected response is invisible. Use it for the delayed reply when work exceeds
|
|
97
|
+
Slack's 3-second acknowledgement window.
|
|
98
|
+
|
|
56
99
|
### OAuth Flow
|
|
57
100
|
|
|
58
101
|
```typescript
|
|
@@ -81,9 +124,19 @@ const tokens = await exchangeSlackOAuthCode({
|
|
|
81
124
|
```typescript
|
|
82
125
|
import { verifySlackSignature } from '@pikku/gateway-slack'
|
|
83
126
|
|
|
84
|
-
verifySlackSignature(signingSecret, timestamp, body
|
|
127
|
+
verifySlackSignature(signingSecret, signature, timestamp, body): boolean
|
|
85
128
|
```
|
|
86
129
|
|
|
130
|
+
**Signature before timestamp** — the two middle arguments are both strings, so
|
|
131
|
+
swapping them compiles and simply never verifies. `signature` is the raw
|
|
132
|
+
`x-slack-signature` header (`v0=…`), `timestamp` is `x-slack-request-timestamp`
|
|
133
|
+
in Unix seconds, and `body` must be the **raw** request body: any re-serialization
|
|
134
|
+
changes the HMAC.
|
|
135
|
+
|
|
136
|
+
It returns `false` rather than throwing, including for a timestamp more than 5
|
|
137
|
+
minutes off (replay protection). The adapter already calls this for you — reach
|
|
138
|
+
for it directly only outside the gateway path, e.g. in a slash-command route.
|
|
139
|
+
|
|
87
140
|
## Usage Patterns
|
|
88
141
|
|
|
89
142
|
### Slack Bot Gateway
|
|
@@ -93,22 +146,30 @@ import { SlackGatewayAdapter } from '@pikku/gateway-slack'
|
|
|
93
146
|
|
|
94
147
|
const slackGateway = new SlackGatewayAdapter({
|
|
95
148
|
signingSecret: config.slackSigningSecret,
|
|
96
|
-
|
|
149
|
+
tokenResolver: async (teamId) => {
|
|
150
|
+
const row = await kysely
|
|
151
|
+
.selectFrom('slackInstall')
|
|
152
|
+
.select('botToken')
|
|
153
|
+
.where('teamId', '=', teamId)
|
|
154
|
+
.executeTakeFirst()
|
|
155
|
+
return row?.botToken ?? null
|
|
156
|
+
},
|
|
97
157
|
})
|
|
98
|
-
|
|
99
|
-
// Register with your HTTP runner to handle /slack/events endpoint
|
|
100
158
|
```
|
|
101
159
|
|
|
102
160
|
### Slash Command Handler
|
|
103
161
|
|
|
162
|
+
Slack gives you 3 seconds to acknowledge, so anything slower answers immediately
|
|
163
|
+
and posts the real result to `responseUrl` afterwards:
|
|
164
|
+
|
|
104
165
|
```typescript
|
|
105
166
|
const handleSlashCommand = pikkuSessionlessFunc({
|
|
106
167
|
title: 'Handle Slack Command',
|
|
107
168
|
func: async ({ db }, data) => {
|
|
108
169
|
const command = parseSlashCommand(data)
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
170
|
+
await respondToSlashCommand(command.responseUrl, {
|
|
171
|
+
text: `Processed: ${command.args}`,
|
|
172
|
+
response_type: 'ephemeral',
|
|
112
173
|
})
|
|
113
174
|
},
|
|
114
175
|
})
|
|
@@ -38,8 +38,13 @@ Follow existing patterns you find (naming, tag usage, file organization). See `p
|
|
|
38
38
|
|
|
39
39
|
## API Reference
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
All three come from `#pikku` (the generated `.pikku/pikku-types.gen.js`), which
|
|
42
|
+
binds them to your project's service, session and middleware types. The
|
|
43
|
+
`@pikku/core/http` versions are the unbound generics — they compile, but you
|
|
44
|
+
lose the typing that makes the wiring worth having.
|
|
45
|
+
|
|
46
|
+
- `wireHTTP(config)` — wire one function to one endpoint.
|
|
47
|
+
- `defineHTTPRoutes(config)` + `wireHTTPRoutes(config)` — group routes with shared config; composable/nestable.
|
|
43
48
|
|
|
44
49
|
Function input/output types come from the function's own `input:`/`output:` zod schemas — never declared in the wiring. Route `:params`, query params, and body are merged into the function's `data` arg (see Data Flow).
|
|
45
50
|
|
|
@@ -144,6 +149,9 @@ wireHTTP({
|
|
|
144
149
|
|
|
145
150
|
### SSE (Server-Sent Events)
|
|
146
151
|
|
|
152
|
+
`sse: true` is only accepted on `method: 'get'` — the wiring union offers it on
|
|
153
|
+
no other verb.
|
|
154
|
+
|
|
147
155
|
```typescript
|
|
148
156
|
wireHTTP({
|
|
149
157
|
method: 'get',
|
|
@@ -154,7 +162,7 @@ wireHTTP({
|
|
|
154
162
|
|
|
155
163
|
const getTodos = pikkuFunc({
|
|
156
164
|
title: 'Get Todos',
|
|
157
|
-
func: async ({ db,
|
|
165
|
+
func: async ({ db }, {}, { channel }) => {
|
|
158
166
|
const todos = await db.getTodos()
|
|
159
167
|
|
|
160
168
|
if (channel) {
|
|
@@ -170,12 +178,17 @@ const getTodos = pikkuFunc({
|
|
|
170
178
|
})
|
|
171
179
|
```
|
|
172
180
|
|
|
181
|
+
`channel` is on the **wire** — the func's third argument — not on services, and
|
|
182
|
+
it is optional because the same function can be reached over plain HTTP or RPC,
|
|
183
|
+
where there is no stream to send on. The `if (channel)` guard is what lets one
|
|
184
|
+
function serve both; the return value is the non-streaming answer.
|
|
185
|
+
|
|
173
186
|
### Generated Fetch Client
|
|
174
187
|
|
|
175
188
|
After `npx pikku all`, a type-safe client is generated:
|
|
176
189
|
|
|
177
190
|
```typescript
|
|
178
|
-
import { pikkuFetch } from '
|
|
191
|
+
import { pikkuFetch } from '#pikku/pikku-fetch.gen.js'
|
|
179
192
|
|
|
180
193
|
pikkuFetch.setServerUrl('http://localhost:4002')
|
|
181
194
|
|
|
@@ -213,7 +226,7 @@ export const getBook = pikkuFunc({
|
|
|
213
226
|
})
|
|
214
227
|
|
|
215
228
|
// wirings/books.http.ts — same defineHTTPRoutes/wireHTTPRoutes shape as the Route Groups example above
|
|
216
|
-
import { addHTTPMiddleware } from '
|
|
229
|
+
import { addHTTPMiddleware } from '#pikku'
|
|
217
230
|
import { cors, authBearer } from '@pikku/core/middleware'
|
|
218
231
|
|
|
219
232
|
addHTTPMiddleware('*', [cors(), authBearer()])
|
|
@@ -2,25 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
## `wireHTTP(config)`
|
|
4
4
|
|
|
5
|
-
Wire a single function to an HTTP endpoint. Import from
|
|
5
|
+
Wire a single function to an HTTP endpoint. Import from `#pikku`.
|
|
6
6
|
|
|
7
7
|
| Option | Type | Notes |
|
|
8
8
|
| --- | --- | --- |
|
|
9
|
-
| `method` | `'get' \| 'post' \| 'put' \| 'patch' \| 'delete' \| 'head'` | HTTP verb |
|
|
9
|
+
| `method` | `'get' \| 'post' \| 'put' \| 'patch' \| 'delete' \| 'head' \| 'options'` | HTTP verb |
|
|
10
10
|
| `route` | `string` | e.g. `/books/:bookId` — `:params` become `data` fields |
|
|
11
11
|
| `func` | `PikkuFunc` | The function to call |
|
|
12
12
|
| `auth?` | `boolean` | Override default auth (`true` = require session) |
|
|
13
13
|
| `tags?` | `string[]` | For grouping, middleware targeting |
|
|
14
14
|
| `middleware?` | `PikkuMiddleware[]` | Per-route middleware |
|
|
15
|
-
| `sse?` | `boolean` | Enable Server-Sent Events |
|
|
15
|
+
| `sse?` | `boolean` | Enable Server-Sent Events — **`method: 'get'` only** |
|
|
16
|
+
| `query?` | `Array<keyof In>` | **`method: 'post'` only** — input fields also read from the query string |
|
|
16
17
|
| `contentType?` | `'xml' \| 'json'` | Response content type |
|
|
17
18
|
| `timeout?` | `number` | Request timeout in ms |
|
|
18
19
|
| `headers?` | `HTTPHeadersSchema` | Expected headers schema |
|
|
19
|
-
|
|
20
|
+
|
|
21
|
+
`sse` and `query` are constrained by the config union rather than by a runtime
|
|
22
|
+
check, so a `sse: true` on a `post` fails to typecheck rather than silently
|
|
23
|
+
serving a normal response. OpenAPI metadata is not declared here — it is derived
|
|
24
|
+
from the function's `description`/`summary` and its input/output schemas.
|
|
20
25
|
|
|
21
26
|
## `defineHTTPRoutes(config)` + `wireHTTPRoutes(config)`
|
|
22
27
|
|
|
23
|
-
Group routes with shared configuration. Groups are composable and nestable. Import from
|
|
28
|
+
Group routes with shared configuration. Groups are composable and nestable. Import from `#pikku`.
|
|
24
29
|
|
|
25
30
|
```typescript
|
|
26
31
|
const routes = defineHTTPRoutes({
|
|
@@ -56,18 +56,18 @@ function LoginPage() {
|
|
|
56
56
|
|
|
57
57
|
## Keys only known at runtime (enum labels, status maps)
|
|
58
58
|
|
|
59
|
-
A DB value picking a label
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
A DB value picking a label is the one case a generated message can't express.
|
|
60
|
+
Paraglide's README (§ "What about dynamic or CMS-driven keys?") is explicit: use
|
|
61
|
+
an **explicit mapping from value to message function**. Key it on the enum type,
|
|
62
|
+
never `string`:
|
|
63
63
|
|
|
64
64
|
```ts
|
|
65
65
|
import { m } from '../paraglide/messages.js'
|
|
66
66
|
|
|
67
67
|
const DOCUMENT_STATUS_LABEL: Record<DocumentStatus, () => string> = {
|
|
68
|
-
completed: m.
|
|
69
|
-
in_progress: m.
|
|
70
|
-
required: m.
|
|
68
|
+
completed: m.enum__document_status__completed,
|
|
69
|
+
in_progress: m.enum__document_status__in_progress,
|
|
70
|
+
required: m.enum__document_status__required,
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
// call site — no fallback, because there is no missing case
|
|
@@ -77,6 +77,13 @@ DOCUMENT_STATUS_LABEL[status]()
|
|
|
77
77
|
`Record<DocumentStatus, …>` is exhaustive: add a value to the enum without a
|
|
78
78
|
label and the build fails. That is the entire point.
|
|
79
79
|
|
|
80
|
+
**Don't write these maps by hand.** `@pikku/paraglide` generates them from the
|
|
81
|
+
`enum__<group>__<member>` keys in the catalog and types each one against the DB
|
|
82
|
+
enum it mirrors, so a migration adding a status is a compile error rather than a
|
|
83
|
+
map someone forgot. Use the namespace above (singular `enum`, `__` between
|
|
84
|
+
segments) so the generator picks the group up, and read `pikku-paraglide` before
|
|
85
|
+
adding one.
|
|
86
|
+
|
|
80
87
|
Do NOT write `Record<string, () => string>` with a `?? status` fallback, and do
|
|
81
88
|
NOT index the namespace with a computed key (`m[\`enums__${name}__${value}\`]`).
|
|
82
89
|
Both compile, both render the raw identifier to users when a label is missing,
|
|
@@ -132,6 +139,10 @@ The wrapper alternative — a module that walks the namespace and pipes each mes
|
|
|
132
139
|
- Don't hardcode display strings "just for now" — the message is the work.
|
|
133
140
|
- Don't edit or commit anything under `src/paraglide/` — it's regenerated; change `messages/*.json` instead.
|
|
134
141
|
- **Don't wrap `m`.** No re-export module, no branding layer, no resolver. Components import `m` from `../paraglide/messages.js` and call it. `@pikku/react`'s `I18nString` is declared as `string & { readonly __brand: 'LocalizedString' }` — deliberately identical to Paraglide's own `LocalizedString` — so `m.some__key()` satisfies the `@pikku/mantine` `I18nNode` gate natively. A wrapper adds nothing and costs per-message tree-shaking.
|
|
142
|
+
|
|
143
|
+
`packages/console` is the one place in this repo that still wraps it, in `src/i18n/messages.ts`, to keep the debug mask (`█`) it carried over from i18next. That wrapper is a leftover, not a pattern — the generated-locale approach above is how a new app gets the same masking without touching every export. Don't copy it.
|
|
144
|
+
|
|
145
|
+
The `mKey`/`mList` runtime resolvers that used to live beside it are **gone**, and must not come back. `mList` resolved indexed `prefix.0` keys that no longer exist; `mKey` took a computed string, which is exactly the type safety Paraglide exists to provide. Where a key really is dynamic, map the discriminant to a message *function* and call it — the map is type-checked, a string is not.
|
|
135
146
|
- Don't re-resolve messages by string key or re-implement `{param}` interpolation. A key-string resolver turns a missing key back into silent runtime text, surrendering the type safety that is the entire reason to use Paraglide.
|
|
136
147
|
- Don't reach for i18next/react-i18next or a runtime-fetch translation loader — Paraglide's compiled functions are the whole delivery mechanism.
|
|
137
148
|
- Don't tokenize backend error messages or logs here — those are not frontend display strings.
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pikku-info
|
|
3
3
|
description: >-
|
|
4
|
-
Discover what exists in a Pikku project — functions
|
|
5
|
-
|
|
6
|
-
find existing functions, or check what middleware and permissions are
|
|
7
|
-
user asks "what functions exist?", "show me the project structure", "list
|
|
4
|
+
Discover what exists in a Pikku project — functions (with their transport, middleware and
|
|
5
|
+
permissions), tags, middleware and permission definitions. Use when you need to understand the
|
|
6
|
+
project structure, find existing functions, or check what middleware and permissions are
|
|
7
|
+
defined. TRIGGER when: user asks "what functions exist?", "show me the project structure", "list
|
|
8
8
|
routes/middleware/permissions", or needs to understand an existing Pikku codebase. DO NOT
|
|
9
9
|
TRIGGER when: user is writing new code (use the specific wiring skill) or asking about Pikku
|
|
10
10
|
concepts (use pikku-concepts).
|
|
@@ -27,9 +27,19 @@ Use this skill as an execution checklist, not reference material.
|
|
|
27
27
|
|
|
28
28
|
Use the `pikku info` CLI commands to inspect this Pikku project. Run the commands below and present the results to the user in a clear summary.
|
|
29
29
|
|
|
30
|
+
There are exactly four subcommands — `functions`, `tags`, `middleware`,
|
|
31
|
+
`permissions`. Routes, channels, schedulers and queues are not separate
|
|
32
|
+
subcommands; they show up as the *transport* column of `info functions --verbose`.
|
|
33
|
+
|
|
30
34
|
## Available Commands
|
|
31
35
|
|
|
32
|
-
|
|
36
|
+
`--silent` suppresses the banner and the inspector's diagnostics, which is what
|
|
37
|
+
you want when parsing the table. It is read by the CLI but not declared as an
|
|
38
|
+
option, so every run also prints `Warning: Unknown option: --silent (ignored)` —
|
|
39
|
+
the warning is wrong, the flag works. Ignore that one line.
|
|
40
|
+
|
|
41
|
+
For anything you intend to parse rather than read, prefer `--json` (alias
|
|
42
|
+
`-j`, or `--output json`), which emits NDJSON instead of a formatted table.
|
|
33
43
|
|
|
34
44
|
### Functions
|
|
35
45
|
|
|
@@ -91,9 +101,9 @@ yarn pikku info permissions --verbose --silent
|
|
|
91
101
|
|
|
92
102
|
1. If the user specifies a subcommand (e.g., `/pikku-info functions`), run only that command.
|
|
93
103
|
2. If no subcommand is specified, run all four commands to give a complete project overview.
|
|
94
|
-
3. Always use `--silent` to suppress the Pikku banner and inspector logs.
|
|
95
|
-
4. Use `--verbose` when the user asks for details, file paths, or "more info".
|
|
96
|
-
5. Use `--limit N` to control output size (default is 50 rows).
|
|
104
|
+
3. Always use `--silent` to suppress the Pikku banner and inspector logs, and disregard the spurious "Unknown option" warning it prints.
|
|
105
|
+
4. Use `--verbose` when the user asks for details, file paths, or "more info". On `tags` it swaps counts for names; elsewhere it adds columns.
|
|
106
|
+
5. Use `--limit N` to control output size (default is 50 rows) — the footer tells you how many were withheld.
|
|
97
107
|
6. After running the commands, summarize the findings concisely:
|
|
98
108
|
- Total count of functions, tags, middleware, and permissions
|
|
99
109
|
- Notable patterns (e.g., which transport types are in use, which tags group the most functions)
|
|
@@ -45,15 +45,28 @@ await jwt.init()
|
|
|
45
45
|
|
|
46
46
|
**Constructor Parameters:**
|
|
47
47
|
|
|
48
|
-
- `getSecrets` — Async function returning an array of `{ id, value }` key pairs.
|
|
48
|
+
- `getSecrets` — Async function returning an array of `{ id, value }` key pairs. The **first** entry signs; every entry can verify.
|
|
49
49
|
- `logger` — Optional logger instance.
|
|
50
50
|
|
|
51
51
|
**Methods:**
|
|
52
52
|
|
|
53
53
|
- `init(): Promise<void>` — Fetch and cache secrets. Call at startup.
|
|
54
|
-
- `encode<T>(expiresIn: RelativeTimeInput, payload: T): Promise<string>` — Create a signed JWT.
|
|
55
|
-
- `decode<T>(token: string): Promise<T>` —
|
|
56
|
-
- `verify(token: string): Promise<void>` —
|
|
54
|
+
- `encode<T>(expiresIn: RelativeTimeInput, payload: T): Promise<string>` — Create a signed JWT, stamping the signing key's `id` as the token's `kid` header.
|
|
55
|
+
- `decode<T>(token: string): Promise<T>` — **Verifies** the signature and expiry, then returns the payload.
|
|
56
|
+
- `verify(token: string): Promise<void>` — The same check, discarding the payload.
|
|
57
|
+
|
|
58
|
+
`decode` is not an unchecked read: both methods run `jose.jwtVerify` and both
|
|
59
|
+
throw on a bad signature or an expired token. There is no way to inspect an
|
|
60
|
+
untrusted payload through this service — reach for `jose.decodeJwt` directly if
|
|
61
|
+
you genuinely need that, and treat the result as unauthenticated input.
|
|
62
|
+
|
|
63
|
+
Tokens are signed **HS256** with a symmetric secret. The algorithm is fixed and
|
|
64
|
+
pinned on verification, so a token arriving with any other `alg` is rejected —
|
|
65
|
+
but it also means this service has no asymmetric (RS256/ES256) mode.
|
|
66
|
+
|
|
67
|
+
`init()` is not strictly required: `encode` calls it lazily on first use. Call it
|
|
68
|
+
at startup anyway so a missing or unreachable secret fails at boot rather than
|
|
69
|
+
on the first request that needs a token.
|
|
57
70
|
|
|
58
71
|
## Usage Patterns
|
|
59
72
|
|
|
@@ -63,15 +76,20 @@ await jwt.init()
|
|
|
63
76
|
import { JoseJWTService } from '@pikku/jose'
|
|
64
77
|
|
|
65
78
|
const jwt = new JoseJWTService(
|
|
66
|
-
async () => [{ id: 'key-1', value:
|
|
79
|
+
async () => [{ id: 'key-1', value: await secrets.getSecret('JWT_SECRET') }],
|
|
67
80
|
logger
|
|
68
81
|
)
|
|
69
82
|
await jwt.init()
|
|
70
83
|
```
|
|
71
84
|
|
|
85
|
+
A signing key is a secret, so it comes from the secrets service rather than
|
|
86
|
+
`process.env` — and because `getSecrets` is a function called on demand, reading
|
|
87
|
+
it there (not once at construction) is what makes the re-init-on-unknown-kid path
|
|
88
|
+
above actually see a rotated key. See `pikku-config`.
|
|
89
|
+
|
|
72
90
|
### Secret Rotation
|
|
73
91
|
|
|
74
|
-
Supply multiple keys. The first
|
|
92
|
+
Supply multiple keys. The first signs; the rest stay available for verification:
|
|
75
93
|
|
|
76
94
|
```typescript
|
|
77
95
|
const jwt = new JoseJWTService(async () => [
|
|
@@ -80,6 +98,17 @@ const jwt = new JoseJWTService(async () => [
|
|
|
80
98
|
])
|
|
81
99
|
```
|
|
82
100
|
|
|
101
|
+
Verification resolves the key by the token's `kid` header rather than trying each
|
|
102
|
+
secret in turn — which is why `encode` stamps the signing key's `id` there, and
|
|
103
|
+
why the ids must stay stable across a rotation. Keep an id in the list for as
|
|
104
|
+
long as tokens bearing it can still be in flight.
|
|
105
|
+
|
|
106
|
+
When a `kid` isn't in the cache, the service re-runs `getSecrets()` once before
|
|
107
|
+
giving up with `Missing secret for id: <kid>`. That is what lets a running server
|
|
108
|
+
pick up a newly added key without a restart, provided `getSecrets` reads from
|
|
109
|
+
something live (a secret store) rather than a value captured at boot. A token
|
|
110
|
+
with no `kid` at all falls back to the current signing key.
|
|
111
|
+
|
|
83
112
|
### With Pikku Services
|
|
84
113
|
|
|
85
114
|
```typescript
|
|
@@ -161,8 +161,8 @@ And writing again replaces it rather than adding a second
|
|
|
161
161
|
| `channel:` | a channel name | generated channel meta |
|
|
162
162
|
| `table:` | a table name | the generated db schema |
|
|
163
163
|
| `addon:` | `@pikku/addon-x` or bare `x` | the manifests that declare the dependency |
|
|
164
|
-
| `scope:` | a scope name | the `scopes:` a function gates itself with, plus the
|
|
165
|
-
| `persona:` | a persona name | `
|
|
164
|
+
| `scope:` | a scope name | the `scopes:` a function gates itself with, plus the scopes a `defineSystemRole()` confers |
|
|
165
|
+
| `persona:` | a persona name | `definePersonas()` |
|
|
166
166
|
|
|
167
167
|
Ids are case-sensitive: `createEntry` is not `createentry`.
|
|
168
168
|
|
|
@@ -176,7 +176,7 @@ These are all things that exist somewhere better, so a note is always the copy t
|
|
|
176
176
|
|
|
177
177
|
| Do not write | Because it lives in |
|
|
178
178
|
| ----------------------------------- | ---------------------------------------------------------- |
|
|
179
|
-
| a `personas/` section | `
|
|
179
|
+
| a `personas/` section | `definePersonas()` in the project's own code |
|
|
180
180
|
| a `scenarios/` section | the gherkin block inside the slice it belongs to |
|
|
181
181
|
| a `permissions/` section | a decision note under `decisions/security/` |
|
|
182
182
|
| a list of tables, columns or routes | `pikku meta` — the generated schema _is_ the schema |
|
|
@@ -28,7 +28,7 @@ Use this skill as an execution checklist, not reference material.
|
|
|
28
28
|
|
|
29
29
|
## Writing Queries — the Kysely query builder
|
|
30
30
|
|
|
31
|
-
In a Pikku function body the injected `kysely` IS the `Kysely<DB>` instance — query it directly.
|
|
31
|
+
In a Pikku function body the injected `kysely` IS the `Kysely<DB>` instance — query it directly. Every connection factory below wires the **CamelCasePlugin** by default, so you write **camelCase everywhere in TS** (columns, aliases) and raw **snake_case ONLY inside a `` sql`` `` literal**. If a project opted out (`createNodeSqliteKysely({ camelCase: false })`), that inverts — check how the instance was built before assuming. Kysely is a query builder, NOT an ORM — there are no relations; shape nested data with the JSON helpers below. Never hand-roll SQL strings; never annotate the return type (in Pikku the output zod schema IS the type).
|
|
32
32
|
|
|
33
33
|
```typescript
|
|
34
34
|
import { sql } from 'kysely'
|
|
@@ -92,12 +92,19 @@ await kysely.transaction().execute(async (trx) => {
|
|
|
92
92
|
})
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
-
Pikku provides SQL database services through
|
|
95
|
+
Pikku provides SQL database services through six packages:
|
|
96
96
|
|
|
97
|
-
- `@pikku/kysely` — Base service implementations (database-agnostic)
|
|
98
|
-
- `@pikku/kysely-postgres` — PostgreSQL-specific implementations + `PikkuKysely` connection wrapper
|
|
97
|
+
- `@pikku/kysely` — Base service implementations (database-agnostic), the serialize plugins, `createAuditedKysely` and the `pikkuSchemas` helpers
|
|
98
|
+
- `@pikku/kysely-postgres` — PostgreSQL-specific implementations + the `PikkuKysely` connection wrapper and `PgEventHubService` (LISTEN/NOTIFY-backed)
|
|
99
99
|
- `@pikku/kysely-mysql` — MySQL-specific implementations
|
|
100
|
-
- `@pikku/kysely-sqlite` — SQLite-specific implementations
|
|
100
|
+
- `@pikku/kysely-sqlite` — SQLite-specific implementations, `createSQLiteKysely`, and the `LibsqlWebDialect`
|
|
101
|
+
- `@pikku/kysely-node-sqlite` — `createNodeSqliteKysely` over `node:sqlite`, plus user-defined SQL functions and the coercion plugin
|
|
102
|
+
- `@pikku/kysely-bun-sqlite` — the same over `bun:sqlite`
|
|
103
|
+
|
|
104
|
+
The last two are runtime adapters rather than service sets: they build the
|
|
105
|
+
`Kysely<DB>` you inject into functions, while the dialect packages above supply
|
|
106
|
+
Pikku's own stores. They differ in one place — `bun:sqlite` cannot register
|
|
107
|
+
scalar functions, so `createBunSqliteKysely` throws if you pass `functions`.
|
|
101
108
|
|
|
102
109
|
All implement standard Pikku interfaces from `@pikku/core`.
|
|
103
110
|
|
|
@@ -105,9 +112,11 @@ All implement standard Pikku interfaces from `@pikku/core`.
|
|
|
105
112
|
|
|
106
113
|
```bash
|
|
107
114
|
# Pick your database
|
|
108
|
-
yarn add @pikku/kysely @pikku/kysely-postgres
|
|
109
|
-
yarn add @pikku/kysely @pikku/kysely-mysql
|
|
110
|
-
yarn add @pikku/kysely @pikku/kysely-sqlite
|
|
115
|
+
yarn add @pikku/kysely @pikku/kysely-postgres # PostgreSQL
|
|
116
|
+
yarn add @pikku/kysely @pikku/kysely-mysql # MySQL
|
|
117
|
+
yarn add @pikku/kysely @pikku/kysely-sqlite # SQLite (stores)
|
|
118
|
+
yarn add @pikku/kysely-node-sqlite # SQLite on Node
|
|
119
|
+
yarn add @pikku/kysely-bun-sqlite # SQLite on Bun
|
|
111
120
|
```
|
|
112
121
|
|
|
113
122
|
## API Reference
|
|
@@ -120,7 +129,8 @@ import { PikkuKysely } from '@pikku/kysely-postgres'
|
|
|
120
129
|
const db = new PikkuKysely<DB>(
|
|
121
130
|
logger: Logger,
|
|
122
131
|
connectionOrConfig: postgres.Sql | postgres.Options | string,
|
|
123
|
-
defaultSchemaName?: string
|
|
132
|
+
defaultSchemaName?: string,
|
|
133
|
+
poolConfig?: PostgresConfig // maxPool, connectTimeout, idleTimeout, maxLifetime, prepare, statementTimeout
|
|
124
134
|
)
|
|
125
135
|
|
|
126
136
|
await db.init()
|
|
@@ -128,14 +138,39 @@ db.kysely // Kysely<DB> instance for queries
|
|
|
128
138
|
await db.close()
|
|
129
139
|
```
|
|
130
140
|
|
|
131
|
-
|
|
141
|
+
It builds a postgres.js-backed Kysely with the CamelCasePlugin. Pass an existing
|
|
142
|
+
`postgres.Sql` when something else owns the pool — the wrapper then leaves it
|
|
143
|
+
open on `close()`. `poolConfig` keys are only forwarded when set, so postgres.js
|
|
144
|
+
keeps its own defaults for the rest, and it is ignored entirely when you hand in
|
|
145
|
+
an already-constructed connection.
|
|
146
|
+
|
|
147
|
+
### SQLite factories
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
import { createNodeSqliteKysely } from '@pikku/kysely-node-sqlite'
|
|
151
|
+
|
|
152
|
+
// Your application DB — CamelCasePlugin on by default
|
|
153
|
+
const kysely = createNodeSqliteKysely<DB>({
|
|
154
|
+
filename: 'app.db', // or ':memory:'
|
|
155
|
+
camelCase: true,
|
|
156
|
+
plugins: [], // layered on top
|
|
157
|
+
functions: {}, // scalar UDFs, registered as deterministic (Node only)
|
|
158
|
+
})
|
|
159
|
+
```
|
|
132
160
|
|
|
133
161
|
```typescript
|
|
134
162
|
import { createSQLiteKysely } from '@pikku/kysely-sqlite'
|
|
135
163
|
|
|
136
|
-
|
|
164
|
+
// Pikku's own tables — returns Kysely<KyselyPikkuDB>, not your DB
|
|
165
|
+
const pikkuDb = createSQLiteKysely(database: SqliteDatabase | (() => Promise<SqliteDatabase>))
|
|
137
166
|
```
|
|
138
167
|
|
|
168
|
+
These two are not interchangeable. `createSQLiteKysely` is typed to
|
|
169
|
+
`KyselyPikkuDB` and wires the `SerializePlugin` (JSON columns in and out) rather
|
|
170
|
+
than the CamelCasePlugin, because it exists to back the stores below. Reach for
|
|
171
|
+
`createNodeSqliteKysely` / `createBunSqliteKysely` for the instance your
|
|
172
|
+
functions query.
|
|
173
|
+
|
|
139
174
|
### Available Services
|
|
140
175
|
|
|
141
176
|
Each database variant exports these services with a prefix (`Pg`, `MySQL`, `SQLite`, or base `Kysely`):
|
|
@@ -151,24 +186,52 @@ Each database variant exports these services with a prefix (`Pg`, `MySQL`, `SQLi
|
|
|
151
186
|
| `*AgentRunService` | `AgentRunService` | Agent execution tracking |
|
|
152
187
|
| `*SecretService` | `SecretService` | Encrypted secret storage (envelope encryption) |
|
|
153
188
|
|
|
189
|
+
A handful more live only on the base package — there is no `Pg`/`MySQL`/`SQLite`
|
|
190
|
+
variant to reach for, you import them from `@pikku/kysely` whatever the engine:
|
|
191
|
+
|
|
192
|
+
| Service | Purpose |
|
|
193
|
+
| -------------------------- | --------------------------------------------- |
|
|
194
|
+
| `KyselySessionStore` | Persisted user sessions |
|
|
195
|
+
| `KyselyScopeService` | Scope and role storage |
|
|
196
|
+
| `KyselyWebhookService` | Webhook registrations and deliveries |
|
|
197
|
+
| `KyselyCredentialService` | Encrypted third-party credentials |
|
|
198
|
+
| `KyselyAIRunStateService` | AI run state (also implemented by AIStorage) |
|
|
199
|
+
| `KyselyWorkflowMirror` | Mirrors workflow runs into queryable tables |
|
|
200
|
+
| `KyselyAuditService` | Durable audit sink (see `pikku-audit`) |
|
|
201
|
+
|
|
154
202
|
All services take a `Kysely<KyselyPikkuDB>` instance in their constructor and have an `init()` method that creates tables if needed.
|
|
155
203
|
|
|
156
204
|
### Secret Service
|
|
157
205
|
|
|
206
|
+
Envelope encryption: each secret gets its own DEK, wrapped by a KEK derived from
|
|
207
|
+
`key` plus a stored per-version salt. Keeping `previousKey` around is what makes
|
|
208
|
+
rotation possible — `rotateKEK` re-wraps every secret from the old key to the
|
|
209
|
+
current one and returns the new version, and it throws if no `previousKey` is
|
|
210
|
+
configured.
|
|
211
|
+
|
|
158
212
|
```typescript
|
|
159
213
|
import { PgKyselySecretService } from '@pikku/kysely-postgres'
|
|
160
214
|
|
|
161
215
|
const secrets = new PgKyselySecretService(db.kysely, {
|
|
162
|
-
|
|
163
|
-
|
|
216
|
+
key: 'your-key-encryption-passphrase',
|
|
217
|
+
keyVersion: 2, // defaults to 1
|
|
218
|
+
previousKey: 'the-passphrase-you-are-rotating-away-from',
|
|
219
|
+
audit: true, // log write/delete/rotate through the audit sink
|
|
220
|
+
auditReads: false, // reads too — noisy, off by default
|
|
164
221
|
})
|
|
165
222
|
await secrets.init()
|
|
166
223
|
|
|
167
224
|
await secrets.setSecret('api-key', { key: 'sk-...' })
|
|
168
|
-
const
|
|
169
|
-
await secrets.
|
|
225
|
+
const secret = await secrets.getSecret<{ key: string }>('api-key')
|
|
226
|
+
await secrets.hasSecret('api-key')
|
|
227
|
+
await secrets.deleteSecret('api-key')
|
|
228
|
+
const newVersion = await secrets.rotateKEK()
|
|
170
229
|
```
|
|
171
230
|
|
|
231
|
+
`getSecret` hands back a `SecretValue<T>`, not the bare value — it serializes as
|
|
232
|
+
`[secret]` until something reveals it, which is what stops a secret drifting into
|
|
233
|
+
a log line or an audit row. See `pikku-config` for the reveal rules.
|
|
234
|
+
|
|
172
235
|
## Usage Patterns
|
|
173
236
|
|
|
174
237
|
### PostgreSQL Setup
|