@pikku/skills 0.12.4 → 0.12.8

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 (65) hide show
  1. package/dist/skills.gen.js +1 -1
  2. package/package.json +1 -1
  3. package/skills/pikku-addon/SKILL.md +74 -33
  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-knowledge/SKILL.md +50 -7
  35. package/skills/pikku-kysely/SKILL.md +78 -15
  36. package/skills/pikku-machine-auth/SKILL.md +36 -1
  37. package/skills/pikku-mcp/SKILL.md +159 -149
  38. package/skills/pikku-middleware/SKILL.md +17 -5
  39. package/skills/pikku-mongodb/SKILL.md +10 -2
  40. package/skills/pikku-n8n-import/SKILL.md +14 -6
  41. package/skills/pikku-permissions/SKILL.md +102 -22
  42. package/skills/pikku-pino/SKILL.md +12 -4
  43. package/skills/pikku-product-second-opinion/SKILL.md +3 -3
  44. package/skills/pikku-queue/SKILL.md +45 -16
  45. package/skills/pikku-react/SKILL.md +41 -14
  46. package/skills/pikku-react-query/SKILL.md +14 -10
  47. package/skills/pikku-realtime/SKILL.md +44 -22
  48. package/skills/pikku-redis/SKILL.md +12 -3
  49. package/skills/pikku-rpc/SKILL.md +23 -12
  50. package/skills/pikku-rtl/SKILL.md +21 -17
  51. package/skills/pikku-scenario/SKILL.md +141 -76
  52. package/skills/pikku-schedule/SKILL.md +39 -6
  53. package/skills/pikku-schema-ajv/SKILL.md +24 -2
  54. package/skills/pikku-schema-cfworker/SKILL.md +22 -2
  55. package/skills/pikku-security/SKILL.md +54 -9
  56. package/skills/pikku-services/SKILL.md +49 -9
  57. package/skills/pikku-services/references/audit-wire-service.md +2 -1
  58. package/skills/pikku-template-clone/SKILL.md +10 -5
  59. package/skills/pikku-trigger/SKILL.md +50 -6
  60. package/skills/pikku-versioning/SKILL.md +46 -17
  61. package/skills/pikku-websocket/SKILL.md +72 -44
  62. package/skills/pikku-workflow/SKILL.md +123 -11
  63. package/skills/pikku-workflow/references/workflow-reference.md +13 -8
  64. package/skills/pikku-workflows-client/SKILL.md +13 -6
  65. package/skills/pikku-ws/SKILL.md +44 -8
@@ -47,10 +47,52 @@ await appServer.start()
47
47
 
48
48
  **Config extends CoreConfig with:** `port`, `hostname`, `healthCheckPath?`
49
49
 
50
- **Methods:** `init(httpOptions?)`, `start()`, `stop()`, `enableExitOnSigInt()`
50
+ **Methods:** `init(httpOptions?: RunHTTPWiringOptions)`, `start()`, `stop()`, `enableExitOnSigInt()`
51
51
 
52
52
  **Property:** `app: uWS.App` — Direct access to uWebSockets app instance.
53
53
 
54
+ ### What the server does and does not give you
55
+
56
+ `init()` registers three things in order: the health check (`healthCheckPath`,
57
+ default `/health-check`), a catch-all `app.any('/*')` HTTP handler, and a
58
+ catch-all `app.ws('/*')` websocket handler. Nothing is registered by the
59
+ constructor, so nothing answers before `init` runs.
60
+
61
+ **There is no `enableCors`, no static assets and no `content` support** — unlike
62
+ the Express server. The class is explicitly a prototyping convenience; for
63
+ anything that needs extra handlers, use `@pikku/uws-handler` directly and treat
64
+ `pikku-uws-server.ts` as the template (that is what its own JSDoc says).
65
+
66
+ `httpOptions` reaches the HTTP handler only. The websocket handler is
67
+ constructed with a fixed `{ logger, logRoutes: true }`, so per-request options
68
+ do not apply to the upgrade path. `loadSchemas` is also never passed by the
69
+ server, so schemas compile lazily on first use rather than at startup — pass
70
+ `loadSchemas: true` in `httpOptions` if you want the startup cost paid up front.
71
+
72
+ `stop()` closes the listen socket and then waits a fixed 2 seconds for
73
+ connections to drain. Called before `start()`, it throws a bare **string**, not
74
+ an `Error`, so `catch (e) { e.message }` reads `undefined`.
75
+
76
+ ### Body limits
77
+
78
+ uWS hands over raw chunks with no limit of its own, so the handler counts the
79
+ bytes itself. A request over `maxBodySize` (default `DEFAULT_MAX_BODY_SIZE`) is
80
+ answered `413` with a `PayloadTooLargeError` body, and the chunks are dropped
81
+ rather than concatenated — an oversized request never accumulates in memory. A
82
+ `content-length` header that already exceeds the limit short-circuits before any
83
+ data arrives.
84
+
85
+ ### Handlers directly (own uWS app)
86
+
87
+ ```typescript
88
+ import { pikkuHTTPHandler, pikkuWebsocketHandler } from '@pikku/uws-handler'
89
+
90
+ app.any('/*', pikkuHTTPHandler({ logger, logRoutes: true, loadSchemas: true }))
91
+ app.ws('/*', pikkuWebsocketHandler({ logger, logRoutes: true }))
92
+ ```
93
+
94
+ Both take `{ logger, logRoutes?, loadSchemas? } & RunHTTPWiringOptions`.
95
+
54
96
  ## WebSocket Standalone (ws library)
55
97
 
56
98
  For WebSocket-only servers using the `ws` library:
@@ -86,3 +128,14 @@ process.on('SIGINT', async () => {
86
128
  process.exit(0)
87
129
  })
88
130
  ```
131
+
132
+ `pikkuWebsocketHandler` takes `{ server, wss, logger, logRoutes?, loadSchemas? }`
133
+ plus `RunHTTPWiringOptions`, and there is no server class in `@pikku/ws` — the
134
+ handler attaches to a `Server` you own.
135
+
136
+ `noServer: true` is required, not stylistic: the handler listens for the HTTP
137
+ server's own `upgrade` event, opens the channel (running middleware and auth
138
+ first), and only then calls `wss.handleUpgrade`. A `WebSocketServer` bound to
139
+ the server would take the socket before any of that ran. An upgrade the channel
140
+ rejects gets the socket destroyed, and an auth failure is written as a real HTTP
141
+ response on the raw socket rather than a silent drop.
@@ -36,19 +36,29 @@ installGroups: [core]
36
36
 
37
37
  - `pikku audit` — reports **security advisories** only.
38
38
  - `pikku audit --outdated` — also reports **available dependency updates**.
39
- - Package-manager detection is by **lockfile** (walks up: `bun.lock`/`bun.lockb`,
40
- then `yarn.lock`). Only **bun** runs a real audit (`bun audit --json` +
39
+ - Package-manager detection is by **lockfile**, walking up to 12 levels to the
40
+ workspace root, checking in this order: `bun.lock`/`bun.lockb`,
41
+ `pnpm-lock.yaml`, `yarn.lock`, `package-lock.json`. A project with several
42
+ lockfiles resolves as bun. Only **bun** runs a real audit (`bun audit --json` +
41
43
  `bun outdated`, normalised into one `SecurityAuditReport` with per-severity /
42
44
  per-update-level counts). Other PMs are detected but **stubbed** with a `note`
43
45
  field until their shapes are normalised — issues/updates come back empty.
44
- - `bun audit` exits non-zero when it *finds* advisories but still writes a valid
45
- report — treat any non-zero exit as data, not failure.
46
+ - `bun audit` exits non-zero when it *finds* advisories but still writes the
47
+ payload to stdout, so a non-zero exit **with output** is data. A non-zero exit
48
+ with **no** output — or a launch failure, timeout, or a blown 32MB buffer —
49
+ throws, precisely so a failed run can't masquerade as "0 advisories".
46
50
 
47
51
  ## Console integration (@pikku/addon-console)
48
52
 
49
53
  Three RPCs, all reading/writing the same artifact via the meta service. Shared
50
54
  spawn/read helpers live in `lib/audit-exec.ts` (`readAuditReport`,
51
- `runPikkuAudit`, `spawnProcess`, `findBin`) — reuse them, don't re-implement.
55
+ `runPikkuAudit`, `spawnProcess`, `findBin`), alongside `lib/find-project-root.ts`
56
+ and `lib/resolve-package-manager.ts` (`resolvePackageManager`, `installArgs`,
57
+ `execPrefix`) — reuse them, don't re-implement. `resolvePackageManager` reads
58
+ package.json's corepack `packageManager` field first and only falls back to
59
+ lockfiles, because that field states intent before a lockfile exists and a
60
+ project can carry a stale one from another tool. Guessing wrong is not a soft
61
+ failure: the spawn dies with `Executable not found in $PATH`.
52
62
  Like every console RPC these require an **authenticated session** (the console
53
63
  is admin-only), so the host must have Better Auth wired — see `pikku-better-auth`.
54
64
 
@@ -84,15 +94,26 @@ is admin-only), so the host must have Better Auth wired — see `pikku-better-au
84
94
 
85
95
  ```ts
86
96
  {
97
+ schemaVersion: number
87
98
  tool: string // e.g. 'bun'
99
+ generatedAt: string // ISO timestamp
88
100
  note?: string // set when the audit could NOT run (unsupported PM);
89
101
  // render ONLY the note — never a reassuring "no vulnerabilities"
90
- summary: { critical, high, moderate, low, info: number }
91
- issues: SecurityAuditIssue[] // package, severity, title, advisoryId, cwe[], cvssScore?,
92
- // url?, vulnerableVersions, recommendedVersion?
102
+ summary: {
103
+ totalIssues, critical, high, moderate, low: number // no `info` bucket
104
+ totalUpdates, major, minor, patch: number
105
+ }
106
+ issues: SecurityAuditIssue[] // package, severity, title, advisoryId, url,
107
+ // vulnerableVersions, cwe[], cvssScore, recommendedVersion
93
108
  updates: SecurityAuditUpdate[] // package, current, latest, level (major|minor|patch|unknown)
94
109
  }
95
110
  ```
96
111
 
112
+ `severity` is one of `critical | high | moderate | low | info`, but `summary`
113
+ has no `info` count — an informational advisory raises `totalIssues` without
114
+ landing in a severity bucket, so don't sum the four to get the total. On an
115
+ issue, `url`, `cvssScore` and `recommendedVersion` are always present and
116
+ **nullable** rather than optional: check for `null`, not `undefined`.
117
+
97
118
  When `note` is present the audit did not run — show only the note (an "Audit not
98
119
  run" state), never the "no known vulnerabilities / up to date" copy.
@@ -23,7 +23,8 @@ generated output is never edited by hand.
23
23
 
24
24
  ## Agent Operating Procedure
25
25
 
26
- 1. Edit source files under `emailTemplatesDir` only. Never edit `.pikku/email/*`.
26
+ 1. Edit source files under `emailTemplatesDir` only. Never edit `.pikku/email/*`. If the
27
+ directory does not exist yet, run `pikku emails init` rather than creating it by hand.
27
28
  2. After any change run `pikku emails generate` (it is also part of `prebuild`, usually
28
29
  `pikku bootstrap; pikku all; pikku emails generate`).
29
30
  3. Validate by importing `renderEmailTemplate` and rendering with sample data, or run the
@@ -40,7 +41,14 @@ generated output is never edited by hand.
40
41
  }
41
42
  ```
42
43
 
43
- If `emailTemplatesDir` is unset the command is a no-op.
44
+ If `emailTemplatesDir` is unset the command is a no-op — it logs
45
+ `Skipping emails (set emailTemplatesDir in pikku.config.json to enable).` and exits
46
+ cleanly, so a silent generate is a config problem, not a template problem.
47
+
48
+ `pikku emails init` scaffolds the directory (starter locales, theme, partials and a
49
+ hello-world template) **and** writes `emailTemplatesDir` into `pikku.config.json` for
50
+ you. Use it rather than hand-creating the tree; `--force` overwrites an existing
51
+ scaffold.
44
52
 
45
53
  ## Directory layout
46
54
 
@@ -97,8 +105,17 @@ import {
97
105
  // { appName?: ...; inviteUrl?: ...; inviterName?: ...; organizationName?: ... }
98
106
  ```
99
107
 
100
- To make a variable required-and-typed, reference it directly in the template body (not
101
- only in a locale string), so it shows up as that template's variable.
108
+ Every extracted variable is emitted **optional** and typed `EmailTemplateValue`
109
+ (`string | number | boolean | null | undefined | object | array`). The type tells you
110
+ which variables a template can consume, not which ones it needs — there is no way to
111
+ mark one required, and a template that references none types as `Record<string, never>`.
112
+ Referencing a variable in the template body (rather than only in a locale string) is
113
+ what gets it into the type at all.
114
+
115
+ That matters because a placeholder with nothing behind it renders as the **empty
116
+ string** — no error, no leftover `{{…}}`. A typo'd variable name, a missing `data` key
117
+ and a value that isn't a string or number all produce the same silently blank output, so
118
+ render with sample data and read the result rather than trusting that it compiled.
102
119
 
103
120
  ## Rendering
104
121
 
@@ -111,7 +128,17 @@ const rendered = renderEmailTemplate({
111
128
  // rendered: { name, locale, subject, html, text?, variables, hash }
112
129
  ```
113
130
 
131
+ It is synchronous, and it throws on an unknown template name or an unknown locale —
132
+ those are the only two failure modes; everything else degrades to blank output.
133
+
114
134
  `hash` is a stable content hash (useful as an idempotency / dedupe key on outgoing mail).
135
+ The meta file also carries per-locale `htmlHash` / `subjectHash` / `textHash` if you need
136
+ to tell which part changed.
137
+
138
+ `{{locale}}` is in scope alongside `{{appName}}`, and placeholders are resolved by
139
+ repeated passes so a locale string containing `{{verifyUrl}}` expands. The loop stops
140
+ after 5 passes, which only becomes visible with placeholders nested more deeply than
141
+ that — a shape worth avoiding rather than working around.
115
142
 
116
143
  ## Sending through an EmailService
117
144
 
@@ -160,4 +187,8 @@ async send(input: SendEmailInput) {
160
187
  reference it in this template to scope it in.
161
188
  - Editing a locale string changes that template's content hash — expected; the hash covers
162
189
  the strings the template uses.
163
- - `layout.html` must contain `{{content}}` or the body is dropped.
190
+ - `layout.html` must contain `{{content}}` or the body is dropped. It is matched by the
191
+ partial name `layout`, so renaming the file opts every template out of the wrapper.
192
+ - A blank spot where a value should be is an unresolved placeholder, not a render
193
+ failure — check the key's spelling and that the value is a string or number (objects
194
+ and arrays resolve to empty).
@@ -96,6 +96,25 @@ Run migrations: `pikku db migrate`. It also regenerates `.pikku/db/schema.gen.ts
96
96
 
97
97
  **NEVER hand-edit the generated schema** — write a migration and re-run.
98
98
 
99
+ ### Dev seed data
100
+
101
+ Alongside the migrations sits `db/<engine>-dev-seed.sql` — `db/sqlite-dev-seed.sql`
102
+ or `db/postgres-dev-seed.sql`. There is no seed command. `pikku db reset` is the
103
+ only thing that applies it: wipe, migrate, seed. `--no-seed` stops after the
104
+ migration, for working on an empty-state or onboarding flow the test data hides.
105
+
106
+ Because reset always arrives at a database it has just wiped, **the seed file is
107
+ plain `INSERT`s** — no `INSERT OR IGNORE`, no `ON CONFLICT DO NOTHING`, no
108
+ `IF NOT EXISTS`. Nothing applies it twice, so it never has to defend itself. If
109
+ you find yourself reaching for an idempotent form, that's a sign the data wants
110
+ to be a migration instead.
111
+
112
+ This is **test data only**: enough rows that a fresh dev database isn't an empty
113
+ app. It never reaches staging or production — reset refuses `NODE_ENV=production`
114
+ and refuses a database outside the runtime directory. Anything a real environment
115
+ needs — accounts, role grants — is provisioning, not seeding, and belongs in
116
+ `pikku persona sync` or a migration.
117
+
99
118
  A Better Auth app has a second constraint: the plugins you enable (`admin()`,
100
119
  `actor()`, …) each declare columns, and `pikku db migrate` refuses to run while
101
120
  the applied schema is missing any of them. `pikku db generate` writes the
@@ -143,7 +162,7 @@ packages/functions/
143
162
  *.channel.ts # wireChannel
144
163
  *.queue.ts # wireQueueWorker
145
164
  *.schedule.ts # wireScheduler
146
- *.mcp.ts # wireMCPTool
165
+ *.mcp.ts # wireMCPResource / wireMCPPrompt (an MCP tool is just a function with `mcp: true`)
147
166
  *.cli.ts # wireCLI
148
167
  services.ts # pikkuServices factory (singleton)
149
168
  middleware.ts # Shared middleware
@@ -152,6 +171,7 @@ packages/functions/
152
171
  db/schema.gen.ts # Kysely types, written by `pikku db migrate` — NEVER hand-edit
153
172
  apps/app/ # Frontend(s)
154
173
  db/sqlite/ # Plain .sql migrations, numbered, gap-free (project root)
174
+ db/sqlite-dev-seed.sql # Dev-only test data, applied by `pikku db reset`
155
175
  pikku.config.json # Pikku + deploy config (project root)
156
176
  pikkufabric.config.json # Fabric project link + frontends (project root)
157
177
  ```
@@ -189,6 +209,11 @@ Links the repo to a Fabric project and declares its frontends:
189
209
  without a domain it lives on the platform `*.pikkufabric.app` hostnames.
190
210
  - `frontends`: each entry declares a frontend app with its dev command and port
191
211
 
212
+ Several CLI messages call this file `fabric.config.json` — `fabric init --force`,
213
+ `fabric link --apiUrl`, and the `domains` commands' "No fabric.config.json found".
214
+ The file the CLI actually reads and writes is `pikkufabric.config.json`; don't
215
+ create the shorter name to satisfy an error message.
216
+
192
217
  ## RPC is the default transport
193
218
 
194
219
  In Fabric apps, most features don't need HTTP wirings. Just write the function with `expose: true` — Pikku generates an RPC client and React Query hooks automatically.
@@ -313,6 +338,6 @@ Fix every `error` and `warn` in the output before continuing. Then:
313
338
  3. **Replace DI/IoC with pikkuServices**: move service construction to `createSingletonServices` in `services.ts`.
314
339
  4. **Replace `process.env` calls**: plain config becomes `defineVariable` + `variables.get()`, anything sensitive becomes `defineSecret` + `secrets.getSecret()`.
315
340
  5. **Add `pikku.config.json`** at project root with `srcDirectories`, `outDir`, and `clientFiles`.
316
- 6. **Add `fabric.config.json`** at project root with `projectId`, `production.branch`, and `frontends`.
341
+ 6. **Add `pikkufabric.config.json`** at project root with `projectId`, `production.domain`, and `frontends` (production is always `main`, so there is no `production.branch`).
317
342
  7. **Run `pikku all`** — verify codegen succeeds and there are no type errors.
318
343
  8. **Run `pikku fabric validate`** once more to confirm no structural issues remain.
@@ -63,6 +63,8 @@ pikku fabric metrics -b main # last 24h
63
63
  pikku fabric metrics -b main --hours 2 --function createOrder
64
64
  ```
65
65
 
66
+ `--branch` is **required** here too; `--hours` defaults to 24.
67
+
66
68
  Rows are `reqs= err= (rate%) avg= min= max=` per bucket. A single bad request
67
69
  with a healthy error rate is a data problem; a climbing error rate is a
68
70
  deployment or dependency problem. `--json` additionally returns a `wireTypes`
@@ -95,7 +97,9 @@ deploy stuck in flight, explains a whole class of "my fix did nothing".
95
97
  `--since 15m` silently returns the same default window as no flag at all. Do
96
98
  not conclude "nothing happened in the last 15 minutes" from it. Narrow by
97
99
  `--level`, or by `--function` via `errors`, instead.
98
- - **`--follow` is a 2-second client-side poll, not a server stream.** It
100
+ - **`--follow` is a 2-second client-side poll, not a server stream** — despite
101
+ its own help text reading "Stream new logs (SSE)". Server-side SSE is planned;
102
+ the backend doesn't push natively today. It
99
103
  dedups against what it already printed, so it behaves like `tail -f`, but new
100
104
  entries can appear up to ~2s late and it holds the process open until killed.
101
105
 
@@ -218,10 +218,15 @@ branch.
218
218
  ## Stage 6 — Commit
219
219
 
220
220
  ```bash
221
- git add -A
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(options: SlackGatewayAdapterOptions)
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
- Helper for handling Slack messages and metadata within gateway functions.
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(request)
53
- await respondToSlashCommand(responseUrl, { text: 'Done!' })
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, signature)
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
- botToken: config.slackBotToken,
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
- // Process command...
110
- await respondToSlashCommand(command.response_url, {
111
- text: `Processed: ${command.text}`,
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
- - `wireHTTP(config)` (from `@pikku/core/http`) — wire one function to one endpoint.
42
- - `defineHTTPRoutes(config)` + `wireHTTPRoutes(config)` (from `.pikku/pikku-types.gen.js`) — group routes with shared config; composable/nestable.
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, channel }, {}) => {
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 '.pikku/pikku-fetch.gen.js'
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 '@pikku/core/http'
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 `@pikku/core/http`.
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
- | `docs?` | `HTTPRouteDocsConfig` | OpenAPI docs config |
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 `.pikku/pikku-types.gen.js`.
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 (`enums__document_status__${status}`) is the one case a
60
- generated message can't express. Paraglide's README (§ "What about dynamic or
61
- CMS-driven keys?") is explicit: use an **explicit mapping from value to message
62
- function**. Key it on the enum type, never `string`:
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.enums__document_status__completed,
69
- in_progress: m.enums__document_status__in_progress,
70
- required: m.enums__document_status__required,
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, tags, middleware, permissions, HTTP routes,
5
- channels, schedulers, queues, and more. Use when you need to understand the project structure,
6
- find existing functions, or check what middleware and permissions are defined. TRIGGER when:
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
- Always use `--silent` to suppress the banner and inspector logs.
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)