@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.
- package/dist/skills.gen.js +1 -1
- package/package.json +1 -1
- package/skills/pikku-addon/SKILL.md +74 -33
- 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 +45 -10
- package/skills/pikku-cli/SKILL.md +67 -18
- package/skills/pikku-cli/references/complete-example.md +2 -0
- package/skills/pikku-concepts/SKILL.md +75 -10
- package/skills/pikku-config/SKILL.md +56 -14
- 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 +27 -2
- package/skills/pikku-fabric-debug/SKILL.md +5 -1
- package/skills/pikku-feature/SKILL.md +6 -1
- 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 +50 -7
- 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 +141 -76
- 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-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 +123 -11
- 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
|
@@ -47,10 +47,52 @@ await appServer.start()
|
|
|
47
47
|
|
|
48
48
|
**Config extends CoreConfig with:** `port`, `hostname`, `healthCheckPath?`
|
|
49
49
|
|
|
50
|
-
**Methods:** `init(httpOptions
|
|
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
|
|
40
|
-
|
|
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
|
|
45
|
-
|
|
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`)
|
|
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: {
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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 #
|
|
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 `
|
|
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
|
|
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
|
|
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)
|