@pikku/skills 0.12.57 → 0.12.59

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 (72) hide show
  1. package/dist/skills.gen.js +2 -2
  2. package/package.json +10 -7
  3. package/skills/pikku-a11y/SKILL.md +5 -3
  4. package/skills/pikku-addon/SKILL.md +8 -9
  5. package/skills/pikku-agent/SKILL.md +16 -13
  6. package/skills/pikku-agent/references/runner-vercel.md +1 -1
  7. package/skills/pikku-architect/SKILL.md +7 -10
  8. package/skills/pikku-auth/SKILL.md +16 -20
  9. package/skills/pikku-auth/references/better-auth.md +74 -37
  10. package/skills/pikku-auth/references/sessions.md +8 -0
  11. package/skills/pikku-build/SKILL.md +36 -68
  12. package/skills/pikku-build/references/app.md +27 -828
  13. package/skills/pikku-build/references/feature.md +3 -5
  14. package/skills/pikku-build/references/phase-1-what-it-is.md +167 -0
  15. package/skills/pikku-build/references/phase-2-people-and-apps.md +133 -0
  16. package/skills/pikku-build/references/phase-3-plan.md +62 -0
  17. package/skills/pikku-build/references/phase-4-build.md +249 -0
  18. package/skills/pikku-build/references/phase-5-prove-and-ship.md +148 -0
  19. package/skills/pikku-build/references/platform.md +5 -6
  20. package/skills/pikku-build/references/quick.md +2 -2
  21. package/skills/pikku-build/references/ship.md +4 -5
  22. package/skills/pikku-changes/SKILL.md +13 -7
  23. package/skills/pikku-concepts/SKILL.md +19 -258
  24. package/skills/pikku-concepts/references/cli.md +63 -0
  25. package/skills/pikku-concepts/references/functions.md +117 -0
  26. package/skills/pikku-concepts/references/language.md +5 -5
  27. package/skills/pikku-concepts/references/packages.md +6 -4
  28. package/skills/pikku-concepts/references/testing.md +14 -0
  29. package/skills/pikku-convert-screen/SKILL.md +7 -1
  30. package/skills/pikku-deploy/SKILL.md +29 -30
  31. package/skills/pikku-deploy/references/ws.md +1 -1
  32. package/skills/pikku-emails/SKILL.md +7 -3
  33. package/skills/pikku-guide/SKILL.md +7 -10
  34. package/skills/pikku-i18n/SKILL.md +18 -19
  35. package/skills/pikku-i18n/references/messages.md +6 -8
  36. package/skills/pikku-knowledge/SKILL.md +42 -45
  37. package/skills/pikku-kysely/SKILL.md +50 -13
  38. package/skills/pikku-list-query/SKILL.md +27 -14
  39. package/skills/pikku-meta/SKILL.md +13 -16
  40. package/skills/pikku-meta/references/audit.md +1 -45
  41. package/skills/pikku-meta/references/versioning.md +1 -1
  42. package/skills/pikku-middleware/SKILL.md +23 -13
  43. package/skills/pikku-middleware/references/middleware-patterns.md +2 -2
  44. package/skills/pikku-n8n-import/SKILL.md +7 -8
  45. package/skills/pikku-permissions/SKILL.md +14 -8
  46. package/skills/pikku-react/SKILL.md +7 -6
  47. package/skills/pikku-realtime/SKILL.md +16 -11
  48. package/skills/pikku-report/SKILL.md +8 -10
  49. package/skills/pikku-scenario/SKILL.md +15 -18
  50. package/skills/pikku-scenario/references/steps.md +3 -4
  51. package/skills/pikku-scenario/references/virtual-users.md +1 -1
  52. package/skills/pikku-seo/SKILL.md +16 -8
  53. package/skills/pikku-service-backends/SKILL.md +26 -28
  54. package/skills/pikku-services/SKILL.md +8 -9
  55. package/skills/pikku-services/references/services.md +1 -1
  56. package/skills/pikku-software-archaeology/README.md +12 -1
  57. package/skills/pikku-software-archaeology/SKILL.md +9 -2
  58. package/skills/pikku-software-archaeology/references/pikku-mapping.md +1 -1
  59. package/skills/pikku-software-archaeology/references/sources/express.md +75 -0
  60. package/skills/pikku-software-archaeology/references/sources/laravel.md +77 -0
  61. package/skills/pikku-software-archaeology/references/sources/nestjs.md +73 -0
  62. package/skills/pikku-software-archaeology/references/sources/rails.md +72 -0
  63. package/skills/pikku-software-archaeology/references/tests-to-scenarios.md +72 -0
  64. package/skills/pikku-tailwind/SKILL.md +15 -16
  65. package/skills/pikku-theme/SKILL.md +15 -17
  66. package/skills/pikku-ui/SKILL.md +4 -3
  67. package/skills/pikku-webhook/SKILL.md +10 -11
  68. package/skills/pikku-wiring/SKILL.md +45 -47
  69. package/skills/pikku-wiring/references/channel.md +1 -1
  70. package/skills/pikku-wiring/references/scheduler.md +1 -1
  71. package/skills/pikku-wiring/references/trigger.md +24 -1
  72. package/skills/pikku-workflow/SKILL.md +7 -8
@@ -1,9 +1,14 @@
1
1
  ---
2
2
  name: pikku-kysely
3
3
  description: >-
4
- Use when WRITING KYSELY QUERIES (select, join, aggregate, insert, update, delete) inside a Pikku function body, or when setting up SQL database services with Kysely. Covers the query builder API (joins, groupBy/having, returning, sql template, $if, transactions, jsonArrayFrom), how many round trips a function body costs and how to collapse sequential awaits into one statement, and @pikku/kysely service setup (channel stores, workflow, secret, AI storage and deployment services).
5
- TRIGGER when: writing any non-trivial kysely query (join, count/sum, groupBy, subquery, transaction, conditional query), a function body awaits more than one query, the injected `kysely` service is used, code uses PikkuKysely, KyselyChannelStore, KyselyWorkflowService or KyselySecretService, or the user asks about SQL setup with Pikku.
6
- DO NOT TRIGGER when: the services are MongoDB or Redis backed (use pikku-service-backends), or the task is the pagination contract rather than the query (use pikku-list-query).
4
+ Writing Kysely queries (select, join, aggregate, insert, update, delete, transactions, sql
5
+ template, jsonArrayFrom) in Pikku function bodies, collapsing sequential awaits into fewer round
6
+ trips, and @pikku/kysely service setup (channel stores, workflow, secret, AI storage, deployment).
7
+ TRIGGER when: writing a non-trivial kysely query, a function body awaits several queries, the
8
+ injected `kysely` service or
9
+ PikkuKysely/KyselyChannelStore/KyselyWorkflowService/KyselySecretService is used, or asked about
10
+ SQL setup. DO NOT TRIGGER for MongoDB/Redis services (use pikku-service-backends) or the
11
+ pagination contract (use pikku-list-query).
7
12
  installGroups: [core]
8
13
  ---
9
14
 
@@ -119,15 +124,27 @@ just stop queuing behind each other.
119
124
 
120
125
  ```typescript
121
126
  // Three hops, in series
122
- const item = await kysely.selectFrom('item').where('id','=',id).selectAll().executeTakeFirst()
123
- const bins = await kysely.selectFrom('bin').where('warehouseId','=',wid).selectAll().execute()
124
- const moves = await kysely.selectFrom('stockMove').where('itemId','=',id).selectAll().execute()
127
+ const item = await kysely
128
+ .selectFrom('item')
129
+ .where('id', '=', id)
130
+ .selectAll()
131
+ .executeTakeFirst()
132
+ const bins = await kysely
133
+ .selectFrom('bin')
134
+ .where('warehouseId', '=', wid)
135
+ .selectAll()
136
+ .execute()
137
+ const moves = await kysely
138
+ .selectFrom('stockMove')
139
+ .where('itemId', '=', id)
140
+ .selectAll()
141
+ .execute()
125
142
 
126
143
  // One hop's worth of latency
127
144
  const [item, bins, moves] = await Promise.all([
128
- kysely.selectFrom('item').where('id','=',id).selectAll().executeTakeFirst(),
129
- kysely.selectFrom('bin').where('warehouseId','=',wid).selectAll().execute(),
130
- kysely.selectFrom('stockMove').where('itemId','=',id).selectAll().execute(),
145
+ kysely.selectFrom('item').where('id', '=', id).selectAll().executeTakeFirst(),
146
+ kysely.selectFrom('bin').where('warehouseId', '=', wid).selectAll().execute(),
147
+ kysely.selectFrom('stockMove').where('itemId', '=', id).selectAll().execute(),
131
148
  ])
132
149
  ```
133
150
 
@@ -142,7 +159,11 @@ wants.
142
159
  // N+1 — one extra hop per warehouse
143
160
  const warehouses = await kysely.selectFrom('warehouse').selectAll().execute()
144
161
  for (const w of warehouses) {
145
- w.bins = await kysely.selectFrom('bin').where('warehouseId','=',w.id).selectAll().execute()
162
+ w.bins = await kysely
163
+ .selectFrom('bin')
164
+ .where('warehouseId', '=', w.id)
165
+ .selectAll()
166
+ .execute()
146
167
  }
147
168
  // One hop — see NESTED DATA above
148
169
  ```
@@ -158,7 +179,11 @@ and what comes back tells you which branch happened.
158
179
 
159
180
  ```typescript
160
181
  // Two hops and a race
161
- const existing = await kysely.selectFrom('item').where('sku','=',sku).select('id').executeTakeFirst()
182
+ const existing = await kysely
183
+ .selectFrom('item')
184
+ .where('sku', '=', sku)
185
+ .select('id')
186
+ .executeTakeFirst()
162
187
  if (existing) throw new ConflictError()
163
188
  await kysely.insertInto('item').values({ sku, name }).execute()
164
189
 
@@ -183,8 +208,20 @@ never have crossed the wire.
183
208
 
184
209
  ```typescript
185
210
  // Two hops — the ids are only ever used as a filter
186
- const ids = await kysely.selectFrom('bin').where('warehouseId','=',wid).select('id').execute()
187
- const stock = await kysely.selectFrom('stock').where('binId','in', ids.map((b) => b.id)).selectAll().execute()
211
+ const ids = await kysely
212
+ .selectFrom('bin')
213
+ .where('warehouseId', '=', wid)
214
+ .select('id')
215
+ .execute()
216
+ const stock = await kysely
217
+ .selectFrom('stock')
218
+ .where(
219
+ 'binId',
220
+ 'in',
221
+ ids.map((b) => b.id)
222
+ )
223
+ .selectAll()
224
+ .execute()
188
225
 
189
226
  // One hop
190
227
  const stock = await kysely
@@ -1,9 +1,11 @@
1
1
  ---
2
2
  name: pikku-list-query
3
3
  description: >-
4
- Use when building a paginated or infinite-scroll list: any RPC that returns rows a user scrolls through (tables, card grids, search results). Covers the backend pikkuListFunc, the ListInput/ListOutput cursor contract, the filter tree, and the generated usePikkuInfiniteQuery hook.
5
- TRIGGER when: user asks for infinite scroll, "load more", a paginated table/list/grid, or a list that could grow beyond a single page.
6
- DO NOT TRIGGER when: the list is small and fixed (for example 5 settings items), where a plain pikkuFunc plus usePikkuQuery returning a full array is correct, or when the task is general React provider and hook setup (use pikku-react).
4
+ Paginated or infinite-scroll lists: pikkuListFunc, the ListInput/ListOutput cursor contract, the
5
+ filter tree, the generated usePikkuInfiniteQuery hook. TRIGGER when: asked for infinite scroll,
6
+ "load more", a paginated table/list/grid, or a list that could outgrow one page. DO NOT TRIGGER
7
+ for small fixed lists (a plain pikkuFunc plus usePikkuQuery is correct) or general React provider
8
+ and hook setup (use pikku-react).
7
9
  installGroups: [core, client]
8
10
  ---
9
11
 
@@ -22,7 +24,10 @@ installGroups: [core, client]
22
24
  A list function is a normal `pikkuFunc`/`pikkuSessionlessFunc` whose input/output conform to two shared shapes from `@pikku/core`:
23
25
 
24
26
  ```typescript
25
- interface ListInput<F extends Record<string, unknown> = {}, S extends string = never> {
27
+ interface ListInput<
28
+ F extends Record<string, unknown> = {},
29
+ S extends string = never,
30
+ > {
26
31
  cursor?: string // opaque — echo back whatever you returned as nextCursor
27
32
  limit?: number // page size; server may cap it
28
33
  sort?: Array<{ column: S; direction: 'asc' | 'desc' }>
@@ -57,7 +62,10 @@ export const listItems = pikkuListFunc<{ status?: string }, Item>({
57
62
  func: async ({ kysely }, input, { session }) => {
58
63
  // `limit` is caller-supplied on an exposed RPC, so it is CAPPED, not trusted —
59
64
  // `ListInput` says "server may cap" and this is where that happens.
60
- const limit = Math.min(Math.max(Math.trunc(input.limit ?? 20) || 20, 1), 100)
65
+ const limit = Math.min(
66
+ Math.max(Math.trunc(input.limit ?? 20) || 20, 1),
67
+ 100
68
+ )
61
69
  const parsed = input.cursor ? Number(input.cursor) : 0
62
70
  const offset = Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : 0
63
71
 
@@ -67,7 +75,11 @@ export const listItems = pikkuListFunc<{ status?: string }, Item>({
67
75
  query = query.where('status', '=', status)
68
76
  }
69
77
 
70
- const rows = await query.orderBy('createdAt', 'desc').offset(offset).limit(limit).execute()
78
+ const rows = await query
79
+ .orderBy('createdAt', 'desc')
80
+ .offset(offset)
81
+ .limit(limit)
82
+ .execute()
71
83
  const nextOffset = offset + rows.length
72
84
  const totalCount = await query
73
85
  .select((eb) => eb.fn.countAll<number>().as('count'))
@@ -92,10 +104,11 @@ Generated automatically alongside `usePikkuQuery`/`usePikkuMutation` once `react
92
104
  import { usePikkuInfiniteQuery } from '.pikku/pikku-react-query.gen'
93
105
 
94
106
  function ItemList() {
95
- const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = usePikkuInfiniteQuery(
96
- 'listItems',
97
- { limit: 20 }, // never pass cursor here — the hook manages it
98
- )
107
+ const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
108
+ usePikkuInfiniteQuery(
109
+ 'listItems',
110
+ { limit: 20 } // never pass cursor here — the hook manages it
111
+ )
99
112
 
100
113
  const rows = data?.pages.flatMap((page) => page.rows) ?? []
101
114
 
@@ -142,10 +155,10 @@ Read exactly the shape you support, and refuse the rest rather than ignoring it:
142
155
  import type { Filter } from '@pikku/core/function'
143
156
 
144
157
  /** The one shape this endpoint accepts: a single-key leaf with a plain value. */
145
- function leafEquals<F extends Record<string, unknown>, K extends keyof F & string>(
146
- filter: Filter<F> | undefined,
147
- field: K,
148
- ): F[K] | undefined {
158
+ function leafEquals<
159
+ F extends Record<string, unknown>,
160
+ K extends keyof F & string,
161
+ >(filter: Filter<F> | undefined, field: K): F[K] | undefined {
149
162
  if (!filter || Array.isArray(filter)) return undefined
150
163
  const keys = Object.keys(filter)
151
164
  if (keys.length !== 1 || keys[0] !== field) return undefined
@@ -1,16 +1,13 @@
1
1
  ---
2
2
  name: pikku-meta
3
3
  description: >-
4
- Use to inspect or evolve a project you did not just write — `pikku meta` and `pikku info` for
5
- what the project declares (functions, schemas, wires, workflows, middleware, permissions) and
6
- `pikku meta apply` to change it, `pikku versions` / `pikku release` for contract hashes,
7
- breaking-change detection, the semver a release should get and shipping it, and `pikku audit` / `pikku
8
- update` for dependency advisories and moving Pikku forward. TRIGGER when: user asks what
9
- functions or routes exist, wants a function's input/output shape, wants to retag a function or
10
- set config on a declaration, asks about API versioning, breaking changes, what semver a release
11
- deserves, dependency vulnerabilities, the console Security screen, or upgrading Pikku. DO NOT
12
- TRIGGER when: user is writing a new function or wiring (use the wiring skill) or asking about
13
- Pikku concepts (use pikku-concepts).
4
+ Inspect or evolve a project you did not just write: `pikku meta`/`pikku info` for functions,
5
+ schemas, wires, workflows, middleware, permissions; `pikku meta apply`; `pikku versions`/`pikku
6
+ release` for contract hashes, breaking changes and semver; `pikku audit`/`pikku update` for
7
+ advisories and upgrades. TRIGGER when: asked what functions or routes exist, a function's
8
+ input/output shape, retagging, API versioning, breaking changes, release semver, dependency
9
+ vulnerabilities or upgrading Pikku. DO NOT TRIGGER when writing a new function or wiring (use the
10
+ wiring skill) or on Pikku concepts (use pikku-concepts).
14
11
  installGroups: [core]
15
12
  allowed-tools: Bash(yarn pikku meta *), Bash(yarn pikku info *)
16
13
  argument-hint: '[context|functions|schemas|workflows|middleware|permissions|wires|apply|versions|release|audit|update]'
@@ -23,11 +20,11 @@ and change it through the write path rather than by hand.
23
20
 
24
21
  ## Pick the reference
25
22
 
26
- | You are… | Read |
27
- | --- | --- |
28
- | Asking what exists, or setting config on a declaration | `references/meta.md` |
23
+ | You are… | Read |
24
+ | ----------------------------------------------------------- | -------------------------- |
25
+ | Asking what exists, or setting config on a declaration | `references/meta.md` |
29
26
  | Versioning a contract, or versioning and shipping a release | `references/versioning.md` |
30
- | Chasing a dependency advisory, or upgrading Pikku | `references/audit.md` |
27
+ | Chasing a dependency advisory, or upgrading Pikku | `references/audit.md` |
31
28
 
32
29
  ## Start with `pikku meta context`
33
30
 
@@ -48,7 +45,7 @@ that asymmetry built in, so let it decide rather than eyeballing a diff.
48
45
 
49
46
  - **Do not infer a function's input or output by reading its body**, and do not
50
47
  cast a call site to make it compile. The schema is the type; `pikku meta
51
- functions get <id>` has it.
48
+ functions get <id>` has it.
52
49
  - **Do not expect an unversioned function to be promoted for you.** Without an
53
50
  explicit `version: 2` it is version 1 of its contract, collides with the
54
51
  pinned `@v1`, and `pikku versions check` reports the published contract as
@@ -63,5 +60,5 @@ that asymmetry built in, so let it decide rather than eyeballing a diff.
63
60
  companions come from `@pikku/core`; the CLI writes it, the addon reads it, the
64
61
  UI renders it.
65
62
  - **Do not treat a failed audit run as a clean one.** `bun audit` exits non-zero
66
- when it *finds* advisories and still writes its payload, so non-zero with
63
+ when it _finds_ advisories and still writes its payload, so non-zero with
67
64
  output is data; non-zero with no output throws on purpose.
@@ -5,8 +5,6 @@
5
5
  - [Agent Operating Procedure](#agent-operating-procedure)
6
6
  - [The `pikku audit` command](#the-pikku-audit-command)
7
7
  - [The `pikku update` command](#the-pikku-update-command)
8
- - [Console integration (@pikku/addon-console)](#console-integration-pikkuaddon-console)
9
- - [Console UI (@pikku/console)](#console-ui-pikkuconsole)
10
8
  - [Report shape (SecurityAuditReport)](#report-shape-securityauditreport)
11
9
 
12
10
  ## Agent Operating Procedure
@@ -14,7 +12,7 @@
14
12
  1. The audit is a generated artifact, not live state. `pikku audit` writes the
15
13
  normalised report to `.pikku/audit.json` (config `outDir`), so it rides the
16
14
  same meta pipeline as every other codegen output — uploaded on deploy,
17
- readable by the console addon and any tooling. Read it via
15
+ readable by any tooling. Read it via
18
16
  `metaService.readFile('audit.json')`, never by shelling out to the package
19
17
  manager from a function.
20
18
  2. One source of truth for the shape: `SecurityAuditReport` (and
@@ -81,48 +79,6 @@ the point is what the target needs. An @pikku peer the same run already brings
81
79
  forward is not reported, and an unsatisfied _optional_ peer the project never
82
80
  declared is skipped.
83
81
 
84
- ## Console integration (@pikku/addon-console)
85
-
86
- Three RPCs, all reading/writing the same artifact via the meta service. Shared
87
- spawn/read helpers live in `lib/audit-exec.ts` (`readAuditReport`,
88
- `runPikkuAudit`, `spawnProcess`, `findBin`), alongside `lib/find-project-root.ts`
89
- and `lib/resolve-package-manager.ts` (`resolvePackageManager`, `installArgs`,
90
- `execPrefix`) — reuse them, don't re-implement. `resolvePackageManager` reads
91
- package.json's corepack `packageManager` field first and only falls back to
92
- lockfiles, because that field states intent before a lockfile exists and a
93
- project can carry a stale one from another tool. Guessing wrong is not a soft
94
- failure: the spawn dies with `Executable not found in $PATH`.
95
- Like every console RPC these require an **authenticated session** (the console
96
- is admin-only), so the host must have Better Auth wired — see `pikku-auth`.
97
-
98
- - `getSecurityAudit` — reads `.pikku/audit.json`, returns the report (or `null`).
99
- - `runSecurityAudit` — runs `pikku audit --outdated` server-side (regenerates the
100
- artifact) then returns the fresh report. Same shape as the Run Tests action.
101
- - `updateDependency({ package, version })` — bumps the package in `package.json`
102
- (preserving the `^`/`~` range prefix), runs `bun install`, re-audits, and
103
- returns the fresh report. Throws if the package is not a direct dependency.
104
- NOTE: `bun install` must be scoped to a standalone project — do not run it
105
- inside a yarn/bun monorepo member (it resolves the whole workspace).
106
-
107
- ## Console UI (@pikku/console)
108
-
109
- - `SecurityPage` — the page: **Run audit** button (`lead`) + responsive
110
- `ShellHeader` (structured `search` + `selection` for the Issues/Dependencies
111
- lens; never cram raw controls into the non-collapsing `filters`/`view` escape
112
- hatch). Empty state until an audit has run.
113
- - `SecurityAuditView` — exported presentational component. Two lenses
114
- (Issues grouped by severity; Dependencies table). Each finding row carries its
115
- actions **right-aligned in the row header** (`Accordion.Control` sibling, so a
116
- click acts instead of toggling): "View advisory" + a per-finding
117
- **remediation slot**.
118
- - `renderRemediation({ pkg, version, issue })` — the extension seam. OSS default
119
- is `UpdateDependencyButton` (the free bump + `bun install`). Downstream
120
- consoles pass their own sandbox-verified action here — replace the
121
- action, keep the view.
122
- - Hooks: `useSecurityAudit` (read), `useRunSecurityAudit` (run),
123
- `useUpdateDependency` (bump). All are `useMutation`/`useQuery` — surface
124
- `mutation.error`, never hand-roll loading/error state or swallow the error.
125
-
126
82
  ## Report shape (SecurityAuditReport)
127
83
 
128
84
  ```ts
@@ -208,7 +208,7 @@ A release with neither says it holds internal changes only. Branch names are con
208
208
  while it waits to ship.
209
209
 
210
210
  Functions and wirings defined under a `scaffold/` directory or in a generated
211
- `*.gen.*` file are platform plumbing (auth, console, a host's injected shims)
211
+ `*.gen.*` file are platform plumbing (auth, a host's injected shims)
212
212
  and are left out of the surface, so the same app diffs the same locally and in
213
213
  a platform's build.
214
214
 
@@ -1,9 +1,13 @@
1
1
  ---
2
2
  name: pikku-middleware
3
3
  description: >-
4
- Use when adding middleware to a Pikku app: global (addGlobalMiddleware), HTTP and prefix (addHTTPMiddleware), tag-scoped (addTagMiddleware), per-wiring, session-setting middleware, and execution order and priority. Covers the pikkuMiddleware factory, where a token is resolved into a session (HTTP middleware, cron tasks), and service-to-service bearer gates.
5
- TRIGGER when: user wants middleware on some or all routes, tag-scoped cross-cutting concerns, global interceptors, middleware priority or order questions, or middleware that calls setSession for a machine caller.
6
- DO NOT TRIGGER when: the question is choosing or configuring an auth strategy (authBearer, authCookie, Better Auth, API keys, `pikku login`), permissions or scopes (use pikku-permissions), or deployment.
4
+ Middleware in a Pikku app: global (addGlobalMiddleware), HTTP/prefix (addHTTPMiddleware),
5
+ tag-scoped (addTagMiddleware), per-wiring, session-setting; execution order and priority; the
6
+ pikkuMiddleware factory; resolving a token into a session (HTTP, cron); service-to-service bearer
7
+ gates. TRIGGER when: middleware on some or all routes, tag-scoped concerns, global interceptors,
8
+ priority/order questions, or middleware calling setInitialSession for a machine caller. DO NOT TRIGGER
9
+ for choosing an auth strategy (authBearer, authCookie, Better Auth, API keys, `pikku login`),
10
+ permissions or scopes (use pikku-permissions), or deployment.
7
11
  installGroups: [core]
8
12
  ---
9
13
 
@@ -41,7 +45,8 @@ const telemetryMiddleware = pikkuMiddleware({
41
45
  The `wire` object gives you:
42
46
 
43
47
  - `wire.http` — inbound HTTP context (headers, URL, cookies)
44
- - `wire.setSession(session)` — set the session for this request
48
+ - `wire.setInitialSession(session)` — establish who the caller is for this request; nothing is persisted and no cookie is re-issued. What auth middleware calls.
49
+ - `wire.setSession(session)` / `wire.clearSession()` — sign the caller in or out: marks the session changed (so `authCookie` re-issues the cookie) and writes the session store. Same meaning as in a function body; await it.
45
50
  - `wire.getSession()` — read the current session
46
51
  - `wire.session` — the session set so far (may be undefined)
47
52
 
@@ -77,7 +82,7 @@ wireHTTP({
77
82
  Runs before everything else, across every wire type: HTTP, Queue, Channel, Trigger, Scheduler, Workflow, Agent, CLI, MCP. Use it for cross-cutting concerns (e.g. telemetry) that must wrap every invocation regardless of transport.
78
83
 
79
84
  ```typescript
80
- import { addGlobalMiddleware } from '@pikku/core'
85
+ import { addGlobalMiddleware } from '@pikku/core/middleware'
81
86
  import { telemetryOuter, telemetryInner } from '@pikku/core/middleware'
82
87
 
83
88
  addGlobalMiddleware([telemetryOuter({ environmentId: env.STAGE_ID })]) // wraps the full call
@@ -177,20 +182,20 @@ Within the same priority level, the collection order above is preserved. Use pri
177
182
 
178
183
  ## ⛔ MACHINE AUTH: THE TOKEN BECOMES A SESSION. ⛔
179
184
 
180
- **A caller that has an identity — a sandbox, a deployed stage, a pool host, a device — is authenticated ONCE, in middleware, which calls `setSession`. The function is then an ordinary `pikkuFunc` gated with `scopes`. It reads `session`. It NEVER re-derives who the caller is.**
185
+ **A caller that has an identity — a sandbox, a deployed stage, a pool host, a device — is authenticated ONCE, in middleware, which calls `setInitialSession`. The function is then an ordinary `pikkuFunc` gated with `scopes`. It reads `session`. It NEVER re-derives who the caller is.**
181
186
 
182
187
  Either a function is sessionless (genuinely public) or it has a session. Anything in between — a token verified inside `func`, a token verified in a `permissions` check that returns `true`, an identity passed in the input schema, the same resolver memoised per request so N functions can each call it — is the anti-pattern this section exists to kill.
183
188
 
184
189
  ```typescript
185
190
  // middleware.ts — resolve the bearer ONCE, for every route
186
191
  const sandboxBearerAuth = pikkuMiddleware<SingletonServices>(
187
- async ({ kysely, auth }, { http, getSession, setSession }, next) => {
192
+ async ({ kysely, auth }, { http, getSession, setInitialSession }, next) => {
188
193
  if (await getSession?.()) return next()
189
194
  const header = http?.request?.header?.('authorization')
190
195
  if (!header?.startsWith('Bearer ')) return next()
191
196
  const sandbox = await resolveSandboxSession(kysely, auth, header.slice(7).trim())
192
197
  if (sandbox) {
193
- setSession?.({ userId: sandbox.createdByUserId ?? sandbox.sandboxInstanceId,
198
+ setInitialSession?.({ userId: sandbox.createdByUserId ?? sandbox.sandboxInstanceId,
194
199
  orgId: sandbox.organizationId, scopes: ['machine:sandbox'], sandbox } as UserSession)
195
200
  }
196
201
  return next()
@@ -218,17 +223,22 @@ An unresolved token leaves the session unset and the function throws `MissingSes
218
223
 
219
224
  ### It MUST be `addHTTPMiddleware`, never `addTagMiddleware`
220
225
 
221
- **A session set in tag middleware is invisible to the function when the call arrives over `POST /rpc/:rpcName`.** Tag middleware runs inside `runPikkuFunc`, and the RPC dispatch calls it without a `sessionService`, so `invocationWire.session` is never re-read after your `setSession` — the function sees the session the OUTER wire had, which is none. `addHTTPMiddleware('*')` runs on the `/rpc` route itself, before its handler, and that session is the one the dispatched function inherits. Tag middleware is still right for a gate that only says yes/no.
226
+ **A session set in tag middleware is invisible to the function when the call arrives over `POST /rpc/:rpcName`.** Tag middleware runs inside `runPikkuFunc`, and the RPC dispatch calls it without a `sessionService`, so `invocationWire.session` is never re-read after your `setInitialSession` — the function sees the session the OUTER wire had, which is none. `addHTTPMiddleware('*')` runs on the `/rpc` route itself, before its handler, and that session is the one the dispatched function inherits. Tag middleware is still right for a gate that only says yes/no.
222
227
 
223
228
  ### A cron is a machine identity too — set it in the task's own middleware
224
229
 
225
230
  A scheduled task has no caller and no header, but it is still a machine principal, and without a session it cannot invoke a gated RPC or be attributed in anything it writes. Give it one the same way, in the task's own `middleware`:
226
231
 
227
232
  ```typescript
228
- const cronSession = pikkuMiddleware(async (_services, { scheduledTask, setSession }, next) => {
229
- setSession?.({ userId: `cron:${scheduledTask?.name}`, scopes: ['machine:cron'] } as UserSession)
230
- return next()
231
- })
233
+ const cronSession = pikkuMiddleware(
234
+ async (_services, { scheduledTask, setInitialSession }, next) => {
235
+ setInitialSession?.({
236
+ userId: `cron:${scheduledTask?.name}`,
237
+ scopes: ['machine:cron'],
238
+ } as UserSession)
239
+ return next()
240
+ }
241
+ )
232
242
 
233
243
  wireScheduler({
234
244
  name: 'tickVirtualUserSchedules',
@@ -32,7 +32,7 @@ export function getServiceRPC(baseUrl: string, token: string): RPCInvoke {
32
32
 
33
33
  ```typescript
34
34
  const apiKeyAuth = pikkuMiddleware(
35
- async ({ kysely }, { http, setSession, session }, next) => {
35
+ async ({ kysely }, { http, setInitialSession, session }, next) => {
36
36
  if (session) return next() // already authenticated
37
37
 
38
38
  const header = http?.request?.header?.('x-api-key')
@@ -43,7 +43,7 @@ const apiKeyAuth = pikkuMiddleware(
43
43
  .select('userId')
44
44
  .where('key', '=', header)
45
45
  .executeTakeFirst()
46
- if (row) setSession?.({ userId: row.userId })
46
+ if (row) setInitialSession?.({ userId: row.userId })
47
47
 
48
48
  return next()
49
49
  }
@@ -1,14 +1,13 @@
1
1
  ---
2
2
  name: pikku-n8n-import
3
3
  description: >-
4
- Use when importing an n8n workflow JSON export into a runnable Pikku workflow. Owns the whole
5
- flow: run `pikku import n8n`, triage what it could not map, fill each stub, report missing
6
- `@pikku/addon-*` integrations, and verify the result compiles with no surviving stubs. TRIGGER
7
- when: the user says "import this n8n workflow" or "convert this n8n export to pikku", points at
8
- an n8n `.json` export or a directory of them, or continues after `pikku import n8n` left throwing
9
- stub functions (`STUB — generated from n8n …`, `— implement me`) or a `*.integrations.json`
10
- manifest. DO NOT TRIGGER for hand-written addon wiring unrelated to an n8n import (use
11
- pikku-addon), or for authoring workflows from scratch (use pikku-workflow).
4
+ Import an n8n workflow JSON export into a runnable Pikku workflow: run `pikku import n8n`, triage
5
+ unmapped nodes, fill each stub, report missing `@pikku/addon-*` integrations, verify it compiles
6
+ with no stubs left. TRIGGER when: asked to import/convert an n8n workflow, pointed at an n8n .json
7
+ export or directory, or continuing after the import left throwing stubs (`STUB — generated from
8
+ n8n …`, `— implement me`) or a `*.integrations.json` manifest. DO NOT TRIGGER for hand-written
9
+ addon wiring unrelated to n8n (use pikku-addon) or authoring workflows from scratch (use
10
+ pikku-workflow).
12
11
  installGroups: [core]
13
12
  metadata:
14
13
  version: 1.0.0
@@ -1,9 +1,13 @@
1
1
  ---
2
2
  name: pikku-permissions
3
3
  description: >-
4
- Use when deciding WHO may call a function: resource ownership, role gates, admin-only actions, or any "only their own rows" rule. Covers the `permissions` field, `pikkuPermission`, `pikkuAuth`, scopes and `defineScope`, `addGlobalPermission`, the three gates, `permissionsInBody`, and where ownership belongs versus where it does not.
5
- TRIGGER when: writing or reviewing any function that touches a row a user owns, gating an action on a role, building the permissions half of a contract in build PHASE 2, about to write an `if` in a function body that decides whether the caller is allowed, or hitting `MissingScopeError`.
6
- DO NOT TRIGGER when: the question is how a caller proves who they are, such as sign-in, sessions, tokens, API keys or seeding a virtual user (use pikku-auth), or how to shape a paginated list (use pikku-list-query).
4
+ Deciding WHO may call a function: resource ownership, role gates, admin-only actions, "only their
5
+ own rows" rules; the `permissions` field, pikkuPermission, pikkuAuth, scopes/defineScope,
6
+ addGlobalPermission, permissionsInBody. TRIGGER when: writing or reviewing a function that touches
7
+ a user-owned row, gating on a role, building the permissions half of a contract in build PHASE 2,
8
+ about to write an `if` in a body deciding whether the caller is allowed, or hitting
9
+ MissingScopeError. DO NOT TRIGGER for how a caller proves identity: sign-in, sessions, tokens, API
10
+ keys, seeding a virtual user (use pikku-auth), or paginated lists (use pikku-list-query).
7
11
  installGroups: [core]
8
12
  ---
9
13
 
@@ -12,7 +16,7 @@ installGroups: [core]
12
16
  ## First: is the caller a machine with a token?
13
17
 
14
18
  Then this is NOT a permissions problem. Resolve the token in `addHTTPMiddleware('*')`
15
- middleware that calls `setSession`, make the function a `pikkuFunc`, and gate it with
19
+ middleware that calls `setInitialSession`, make the function a `pikkuFunc`, and gate it with
16
20
  `scopes` (pikku-auth, `../pikku-auth/references/machine-auth.md`). A `permissions` check that verifies a
17
21
  bearer token and returns `true` is authentication wearing an authorization hat, and it
18
22
  leaves the function sessionless. The only exception is a bootstrap endpoint whose caller
@@ -54,8 +58,8 @@ user's row by passing its id. Every function that takes a row id needs BOTH: `au
54
58
  for the session, and a `permissions` entry for the ownership.
55
59
 
56
60
  Equally: do NOT write an `isSignedIn` permission that returns `!!session`. That re-checks
57
- authentication, which `auth: true` already did. A permission answers *may this user do
58
- this* — role, ownership, tier — never *is there a session*.
61
+ authentication, which `auth: true` already did. A permission answers _may this user do
62
+ this_ — role, ownership, tier — never _is there a session_.
59
63
 
60
64
  ## Single row vs list — where ownership actually goes
61
65
 
@@ -88,11 +92,13 @@ export const isBookOwner = pikkuPermission(
88
92
  .where('bookId', '=', bookId)
89
93
  .executeTakeFirst()
90
94
  return book?.ownerId === session?.userId
91
- },
95
+ }
92
96
  )
93
97
 
94
98
  // Session-only: no input needed. Use for role and flag gates.
95
- export const isAdmin = pikkuAuth(async (_services, session) => session?.role === 'admin')
99
+ export const isAdmin = pikkuAuth(
100
+ async (_services, session) => session?.role === 'admin'
101
+ )
96
102
  ```
97
103
 
98
104
  ## OR and AND
@@ -1,12 +1,13 @@
1
1
  ---
2
2
  name: pikku-react
3
3
  description: >-
4
- Use when a React frontend talks to a Pikku backend — PikkuProvider and createPikku at the app
5
- root, the generated React Query hooks (usePikkuQuery, usePikkuMutation, usePikkuInfiniteQuery),
6
- direct usePikkuRPC / usePikkuFetch calls, realtime subscriptions, and agent and workflow hooks. TRIGGER when: writing a React component that fetches or mutates backend
7
- data, wiring PikkuProvider, paginating, running or tracking a workflow from the client. DO NOT TRIGGER when: working on the
8
- backend (use pikku-wiring), defining the workflow itself (use pikku-workflow), rendering fetched values such as
9
- dates (use pikku-tailwind), or writing user-facing copy (use pikku-i18n).
4
+ React frontend talking to a Pikku backend: PikkuProvider/createPikku, generated React Query hooks
5
+ (usePikkuQuery, usePikkuMutation, usePikkuInfiniteQuery), usePikkuRPC/usePikkuFetch, realtime
6
+ subscriptions, agent and workflow hooks. TRIGGER when: writing a React component that fetches or
7
+ mutates backend data, wiring PikkuProvider, paginating, or running/tracking a workflow from the
8
+ client. DO NOT TRIGGER for backend work (use pikku-wiring), defining the workflow (use
9
+ pikku-workflow), rendering fetched values such as dates (use pikku-tailwind) or user-facing copy
10
+ (use pikku-i18n).
10
11
  installGroups: [client]
11
12
  ---
12
13
 
@@ -1,15 +1,13 @@
1
1
  ---
2
2
  name: pikku-realtime
3
3
  description: >-
4
- Use when making a view live in a Pikku app — a board, shared list, dashboard, ticker, bidding
5
- room, live count — or when adding two-way chat or presence. Covers the default event-hub SSE path
6
- (`eventHub.publish` on the backend, `PikkuRealtime.subscribeToTopic` on the client) and the
7
- two-way WebSocket channel. TRIGGER when: the user wants live updates, realtime, "update without
8
- refresh", a live board/feed/ticker/room, presence or chat, or data that more than one signed-in
9
- user can change should reflect to others. DO NOT TRIGGER when: a one-shot query/refetch is enough
10
- (data only one user changes, or a manual refresh is acceptable), for background jobs (use
11
- pikku-workflow, or pikku-wiring's queue and scheduler references), or for the full wiring
12
- reference of channels, SSE routes and the `/events` scaffold (use pikku-wiring).
4
+ Making a view live in a Pikku app (board, shared list, dashboard, ticker, bidding room, live
5
+ count) or adding two-way chat/presence: the default event-hub SSE path (`eventHub.publish`,
6
+ `PikkuRealtime.subscribeToTopic`) and the WebSocket channel. TRIGGER when: asked for live updates,
7
+ realtime, "update without refresh", presence or chat, or data several signed-in users can change
8
+ should reflect to others. DO NOT TRIGGER when a one-shot refetch is enough, for background jobs
9
+ (use pikku-workflow, or pikku-wiring's queue/scheduler references), or for the full
10
+ channel/SSE/`/events` wiring reference (use pikku-wiring).
13
11
  installGroups: [core, client]
14
12
  ---
15
13
 
@@ -115,8 +113,15 @@ import { pikkuChannelFunc, wireChannel } from '#pikku/channel'
115
113
 
116
114
  export const onMessage = pikkuChannelFunc<{ text: string }>({
117
115
  func: async ({ eventHub }, input, { channel, session }) => {
118
- const message = { id: crypto.randomUUID(), text: input.text, userId: session!.userId }
119
- await eventHub.publish('room', channel.channelId, { topic: 'room', data: message })
116
+ const message = {
117
+ id: crypto.randomUUID(),
118
+ text: input.text,
119
+ userId: session!.userId,
120
+ }
121
+ await eventHub.publish('room', channel.channelId, {
122
+ topic: 'room',
123
+ data: message,
124
+ })
120
125
  return message
121
126
  },
122
127
  })
@@ -1,15 +1,13 @@
1
1
  ---
2
2
  name: pikku-report
3
3
  description: >-
4
- Use when pikku itself cost you time — wrong generated types, a check that passes when it should
5
- not, output that is quietly wrong, a skill that misled you — or when the user asks you to report
6
- a framework bug or file a finding. Owns `pikku cloud report` (a finding is about pikku, not the
7
- app), the product-vs-harness kinds, the workaround-first ladder, and asking the user at hand-over
8
- whether to send what was filed. TRIGGER when: the framework fought you, codegen produced
9
- something broken, a skill told you to run something that does not exist, the user says "report
10
- this to pikku" / "file a finding", or you are handing over a build. DO NOT TRIGGER when: the bug
11
- is in the app you are building (fix it there), you are debugging a deployed stage,
12
- or you are tempted to patch pikku's source (never do that from an app).
4
+ Reporting Pikku itself when it cost you time (wrong generated types, a check that passes when it
5
+ should not, quietly wrong output, a misleading skill) or when asked to file a framework bug:
6
+ `pikku cloud report`, product-vs-harness kinds, the workaround-first ladder, asking the user at
7
+ hand-over whether to send findings. TRIGGER when: the framework fought you, codegen is broken, a
8
+ skill told you to run something that does not exist, the user says "report this to pikku" / "file
9
+ a finding", or you are handing over a build. DO NOT TRIGGER for bugs in the app you are building,
10
+ debugging a deployed stage, or patching pikku's source (never from an app).
13
11
  installGroups: [core]
14
12
  ---
15
13
 
@@ -62,7 +60,7 @@ baseline noise that was already failing before you started.
62
60
  - `--kind harness` — a skill misled you: it told you to run something that does
63
61
  not exist, described a flag that is spelled differently, or contradicted what
64
62
  the CLI actually did. Pass `--skill <name>` and `--passage "<the line or
65
- section>"`. This is the most useful kind to file, because it is fixable
63
+ section>"`. This is the most useful kind to file, because it is fixable
66
64
  immediately — so file it even when the cost was small.
67
65
 
68
66
  ## The command