@pikku/skills 0.12.58 → 0.12.60
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 +2 -2
- package/package.json +10 -7
- package/skills/pikku-a11y/SKILL.md +5 -3
- package/skills/pikku-addon/SKILL.md +8 -9
- package/skills/pikku-addon/references/addon-package-manifest.md +53 -0
- package/skills/pikku-agent/SKILL.md +16 -13
- package/skills/pikku-agent/references/runner-vercel.md +1 -1
- package/skills/pikku-architect/SKILL.md +7 -10
- package/skills/pikku-auth/SKILL.md +16 -20
- package/skills/pikku-auth/references/better-auth.md +74 -37
- package/skills/pikku-auth/references/sessions.md +8 -0
- package/skills/pikku-build/SKILL.md +36 -68
- package/skills/pikku-build/references/app.md +27 -828
- package/skills/pikku-build/references/feature.md +3 -5
- package/skills/pikku-build/references/phase-1-what-it-is.md +167 -0
- package/skills/pikku-build/references/phase-2-people-and-apps.md +133 -0
- package/skills/pikku-build/references/phase-3-plan.md +62 -0
- package/skills/pikku-build/references/phase-4-build.md +253 -0
- package/skills/pikku-build/references/phase-5-prove-and-ship.md +148 -0
- package/skills/pikku-build/references/platform.md +5 -6
- package/skills/pikku-build/references/quick.md +2 -2
- package/skills/pikku-build/references/ship.md +11 -5
- package/skills/pikku-changes/SKILL.md +13 -7
- package/skills/pikku-concepts/SKILL.md +26 -258
- package/skills/pikku-concepts/references/cli.md +63 -0
- package/skills/pikku-concepts/references/functions.md +117 -0
- package/skills/pikku-concepts/references/language.md +5 -5
- package/skills/pikku-concepts/references/packages.md +6 -4
- package/skills/pikku-concepts/references/testing.md +14 -0
- package/skills/pikku-convert-screen/SKILL.md +7 -1
- package/skills/pikku-deploy/SKILL.md +29 -30
- package/skills/pikku-deploy/references/ws.md +1 -1
- package/skills/pikku-emails/SKILL.md +7 -3
- package/skills/pikku-guide/SKILL.md +7 -10
- package/skills/pikku-i18n/SKILL.md +18 -19
- package/skills/pikku-i18n/references/messages.md +6 -8
- package/skills/pikku-knowledge/SKILL.md +42 -45
- package/skills/pikku-kysely/SKILL.md +50 -13
- package/skills/pikku-list-query/SKILL.md +27 -14
- package/skills/pikku-meta/SKILL.md +13 -16
- package/skills/pikku-meta/references/audit.md +1 -45
- package/skills/pikku-meta/references/versioning.md +1 -1
- package/skills/pikku-middleware/SKILL.md +23 -13
- package/skills/pikku-middleware/references/middleware-patterns.md +2 -2
- package/skills/pikku-n8n-import/SKILL.md +7 -8
- package/skills/pikku-permissions/SKILL.md +14 -8
- package/skills/pikku-react/SKILL.md +7 -6
- package/skills/pikku-realtime/SKILL.md +16 -11
- package/skills/pikku-report/SKILL.md +8 -10
- package/skills/pikku-scenario/SKILL.md +8 -12
- package/skills/pikku-scenario/references/steps.md +3 -4
- package/skills/pikku-scenario/references/virtual-users.md +1 -1
- package/skills/pikku-seo/SKILL.md +16 -8
- package/skills/pikku-service-backends/SKILL.md +26 -28
- package/skills/pikku-services/SKILL.md +8 -9
- package/skills/pikku-services/references/services.md +1 -1
- package/skills/pikku-software-archaeology/README.md +12 -1
- package/skills/pikku-software-archaeology/SKILL.md +9 -2
- package/skills/pikku-software-archaeology/references/pikku-mapping.md +1 -1
- package/skills/pikku-software-archaeology/references/sources/express.md +75 -0
- package/skills/pikku-software-archaeology/references/sources/laravel.md +77 -0
- package/skills/pikku-software-archaeology/references/sources/nestjs.md +73 -0
- package/skills/pikku-software-archaeology/references/sources/rails.md +72 -0
- package/skills/pikku-software-archaeology/references/tests-to-scenarios.md +72 -0
- package/skills/pikku-tailwind/SKILL.md +15 -16
- package/skills/pikku-theme/SKILL.md +15 -17
- package/skills/pikku-ui/SKILL.md +4 -3
- package/skills/pikku-webhook/SKILL.md +10 -11
- package/skills/pikku-wiring/SKILL.md +45 -47
- package/skills/pikku-wiring/references/channel.md +1 -1
- package/skills/pikku-wiring/references/scheduler.md +1 -1
- package/skills/pikku-wiring/references/trigger.md +24 -1
- package/skills/pikku-workflow/SKILL.md +7 -8
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pikku-knowledge
|
|
3
3
|
description: >-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
changeset plan (use pikku-architect), or extracts a `.knowledge/` blueprint from a legacy repo
|
|
13
|
-
(use pikku-software-archaeology).
|
|
4
|
+
Write, read, reorganise or validate a project's knowledge/ directory: Open Knowledge Format notes
|
|
5
|
+
(path-as-identity markdown, only `type` required), sections (entities, decisions, questions,
|
|
6
|
+
wishlist), notes becoming changes (`pikku knowledge gaps`), the `resource:` URI scheme, `pikku
|
|
7
|
+
knowledge validate|index|gaps`. TRIGGER when: asked to record a decision, requirement, entity or
|
|
8
|
+
open question, what the app does or is, a knowledge note/diagram/callout, or a product brief. DO
|
|
9
|
+
NOT TRIGGER for what functions/routes/tables exist (`pikku meta`/`pikku info`), scenarios (use
|
|
10
|
+
pikku-scenario), changeset plans (use pikku-architect) or legacy blueprint extraction (use
|
|
11
|
+
pikku-software-archaeology).
|
|
14
12
|
installGroups: [core]
|
|
15
13
|
agent:
|
|
16
14
|
tools: read, write, edit, bash, grep
|
|
@@ -21,7 +19,6 @@ agent:
|
|
|
21
19
|
verify:
|
|
22
20
|
- id: knowledge-consistent
|
|
23
21
|
command: pikku knowledge validate
|
|
24
|
-
|
|
25
22
|
---
|
|
26
23
|
|
|
27
24
|
# Pikku Knowledge
|
|
@@ -72,14 +69,14 @@ worse than one of them losing access mid-session.
|
|
|
72
69
|
|
|
73
70
|
Frontmatter fields:
|
|
74
71
|
|
|
75
|
-
| Field | Meaning
|
|
76
|
-
| ------------- |
|
|
72
|
+
| Field | Meaning |
|
|
73
|
+
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
77
74
|
| `type` | **The only required field.** One of `entity`, `decision`, `note`, `overview`. Lowercase — every gate compares it literally, so a near-miss is a note no command can see. |
|
|
78
|
-
| `title` | What to call the note in a listing. Falls back to the first heading, then the filename.
|
|
79
|
-
| `description` | One line, used as the note's subtitle in a section index.
|
|
80
|
-
| `resource` | Comma-separated `<kind>:<id>` URIs — the code this note is about. See below.
|
|
81
|
-
| `tags` | Flow list (`[a, b]`) or a `- item` block; both are read.
|
|
82
|
-
| `timestamp` | When it was written, if it matters.
|
|
75
|
+
| `title` | What to call the note in a listing. Falls back to the first heading, then the filename. |
|
|
76
|
+
| `description` | One line, used as the note's subtitle in a section index. |
|
|
77
|
+
| `resource` | Comma-separated `<kind>:<id>` URIs — the code this note is about. See below. |
|
|
78
|
+
| `tags` | Flow list (`[a, b]`) or a `- item` block; both are read. |
|
|
79
|
+
| `timestamp` | When it was written, if it matters. |
|
|
83
80
|
|
|
84
81
|
`index.md` and `log.md` are **reserved**: an `index.md` maps a directory, a `log.md` is an append-only record. Neither is ever listed as a note by an index.
|
|
85
82
|
|
|
@@ -109,13 +106,13 @@ knowledge/
|
|
|
109
106
|
|
|
110
107
|
Each section answers exactly one question, which is what lets a reader find a note without an index of indexes:
|
|
111
108
|
|
|
112
|
-
| Section | The question it answers
|
|
113
|
-
| --------------------- |
|
|
114
|
-
| `entities/` | What is this thing, in the words users use for it?
|
|
115
|
-
| `decisions/` | What was chosen, and what does that rule out?
|
|
116
|
-
| `decisions/security/` | Who may do what?
|
|
117
|
-
| `questions/` | What has been asked and not yet answered?
|
|
118
|
-
| `wishlist/` | What does somebody want that nobody has asked to be built?
|
|
109
|
+
| Section | The question it answers |
|
|
110
|
+
| --------------------- | ---------------------------------------------------------- |
|
|
111
|
+
| `entities/` | What is this thing, in the words users use for it? |
|
|
112
|
+
| `decisions/` | What was chosen, and what does that rule out? |
|
|
113
|
+
| `decisions/security/` | Who may do what? |
|
|
114
|
+
| `questions/` | What has been asked and not yet answered? |
|
|
115
|
+
| `wishlist/` | What does somebody want that nobody has asked to be built? |
|
|
119
116
|
|
|
120
117
|
**Create a section the turn you have a note for it** — never a scaffold of empty directories, and never a section without its own `index.md`. A section index says in one line what belongs in it; that sentence is the reason the file exists, so `pikku knowledge index` writes only the note listing and leaves your prose alone.
|
|
121
118
|
|
|
@@ -193,19 +190,19 @@ It is a **summary, not the note** — the argument continues in prose underneath
|
|
|
193
190
|
|
|
194
191
|
**Every kind resolves.** That is the whole design: a kind that cannot be checked lets notes accumulate references nothing validates, and the graph rots into fiction exactly where it looks most authoritative.
|
|
195
192
|
|
|
196
|
-
| Kind
|
|
197
|
-
|
|
|
198
|
-
| `func:`
|
|
199
|
-
| `workflow:`
|
|
200
|
-
| `schema:`
|
|
201
|
-
| `http:`
|
|
202
|
-
| `queue:`
|
|
203
|
-
| `cron:`
|
|
204
|
-
| `channel:`
|
|
205
|
-
| `table:`
|
|
206
|
-
| `addon:`
|
|
207
|
-
| `scope:`
|
|
208
|
-
| `virtualUser:`
|
|
193
|
+
| Kind | An id is | Where it resolves |
|
|
194
|
+
| -------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------ |
|
|
195
|
+
| `func:` | a function id | generated function meta |
|
|
196
|
+
| `workflow:` | a workflow name | generated workflow meta |
|
|
197
|
+
| `schema:` | a schema name | generated schemas |
|
|
198
|
+
| `http:` | a route, `method:route`, or the function behind it | generated http wirings |
|
|
199
|
+
| `queue:` | a queue name | generated queue wirings |
|
|
200
|
+
| `cron:` | a scheduled task name | generated scheduler wirings |
|
|
201
|
+
| `channel:` | a channel name | generated channel meta |
|
|
202
|
+
| `table:` | a table name | the generated db schema |
|
|
203
|
+
| `addon:` | `@pikku/addon-x` or bare `x` | the manifests that declare the dependency |
|
|
204
|
+
| `scope:` | a scope name | the `scopes:` a function gates itself with, plus the scopes a `defineSystemRole()` confers |
|
|
205
|
+
| `virtualUser:` | a virtual user name | `defineVirtualUsers()` |
|
|
209
206
|
|
|
210
207
|
Ids are case-sensitive: `createEntry` is not `createentry`.
|
|
211
208
|
|
|
@@ -217,14 +214,14 @@ There is no kind for a service, a middleware or a component. Say it in prose ins
|
|
|
217
214
|
|
|
218
215
|
These are all things that exist somewhere better, so a note is always the copy that drifts:
|
|
219
216
|
|
|
220
|
-
| Do not write | Because it lives in
|
|
221
|
-
| ----------------------------------- |
|
|
222
|
-
| a `virtual users/` section
|
|
217
|
+
| Do not write | Because it lives in |
|
|
218
|
+
| ----------------------------------- | ------------------------------------------------------------ |
|
|
219
|
+
| a `virtual users/` section | `defineVirtualUsers()` in the project's own code |
|
|
223
220
|
| a `scenarios/` section | the `scenarios` of the plan for the changeset that builds it |
|
|
224
|
-
| a `permissions/` section | a decision note under `decisions/security/`
|
|
225
|
-
| a list of tables, columns or routes | `pikku meta` — the generated schema _is_ the schema
|
|
226
|
-
| a changelog | `CHANGELOG.md` at the repo root
|
|
227
|
-
| **secrets or credentials** | a secrets service. Never here — `knowledge/` is committed.
|
|
221
|
+
| a `permissions/` section | a decision note under `decisions/security/` |
|
|
222
|
+
| a list of tables, columns or routes | `pikku meta` — the generated schema _is_ the schema |
|
|
223
|
+
| a changelog | `CHANGELOG.md` at the repo root |
|
|
224
|
+
| **secrets or credentials** | a secrets service. Never here — `knowledge/` is committed. |
|
|
228
225
|
|
|
229
226
|
And two shapes that look like a knowledge base but are not:
|
|
230
227
|
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pikku-kysely
|
|
3
3
|
description: >-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
123
|
-
|
|
124
|
-
|
|
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
|
|
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
|
|
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
|
|
187
|
-
|
|
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
|
-
|
|
5
|
-
TRIGGER when:
|
|
6
|
-
|
|
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<
|
|
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(
|
|
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
|
|
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 } =
|
|
96
|
-
|
|
97
|
-
|
|
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<
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
`
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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…
|
|
27
|
-
|
|
|
28
|
-
| Asking what exists, or setting config on a declaration
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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.
|
|
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 `
|
|
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,
|
|
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
|
-
|
|
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 `
|
|
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(
|
|
229
|
-
|
|
230
|
-
|
|
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,
|
|
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)
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|