@pikku/skills 0.12.2 → 0.12.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/skills.gen.js +1 -1
  2. package/package.json +1 -1
  3. package/skills/pikku-addon/SKILL.md +56 -29
  4. package/skills/pikku-ai-agent/SKILL.md +197 -105
  5. package/skills/pikku-ai-vercel/SKILL.md +57 -18
  6. package/skills/pikku-ai-voice/SKILL.md +126 -52
  7. package/skills/pikku-audit/SKILL.md +35 -13
  8. package/skills/pikku-aws/SKILL.md +66 -16
  9. package/skills/pikku-backblaze/SKILL.md +44 -11
  10. package/skills/pikku-better-auth/SKILL.md +80 -34
  11. package/skills/pikku-cli/SKILL.md +67 -18
  12. package/skills/pikku-cli/references/complete-example.md +2 -0
  13. package/skills/pikku-concepts/SKILL.md +82 -10
  14. package/skills/pikku-concepts/references/concept-mapping.md +2 -2
  15. package/skills/pikku-config/SKILL.md +134 -52
  16. package/skills/pikku-cron/SKILL.md +13 -6
  17. package/skills/pikku-deploy-azure/SKILL.md +83 -28
  18. package/skills/pikku-deploy-cloudflare/SKILL.md +79 -37
  19. package/skills/pikku-deploy-express/SKILL.md +40 -4
  20. package/skills/pikku-deploy-fastify/SKILL.md +22 -1
  21. package/skills/pikku-deploy-lambda/SKILL.md +99 -19
  22. package/skills/pikku-deploy-nextjs/SKILL.md +49 -5
  23. package/skills/pikku-deploy-uws/SKILL.md +54 -1
  24. package/skills/pikku-deps/SKILL.md +29 -8
  25. package/skills/pikku-emails/SKILL.md +36 -5
  26. package/skills/pikku-fabric/SKILL.md +30 -5
  27. package/skills/pikku-fabric-debug/SKILL.md +5 -1
  28. package/skills/pikku-feature/SKILL.md +12 -7
  29. package/skills/pikku-gateway-slack/SKILL.md +72 -11
  30. package/skills/pikku-http/SKILL.md +18 -5
  31. package/skills/pikku-http/references/http-options.md +10 -5
  32. package/skills/pikku-i18n/SKILL.md +18 -7
  33. package/skills/pikku-info/SKILL.md +18 -8
  34. package/skills/pikku-jose/SKILL.md +35 -6
  35. package/skills/pikku-knowledge/SKILL.md +3 -3
  36. package/skills/pikku-kysely/SKILL.md +78 -15
  37. package/skills/pikku-machine-auth/SKILL.md +36 -1
  38. package/skills/pikku-mcp/SKILL.md +159 -149
  39. package/skills/pikku-middleware/SKILL.md +17 -5
  40. package/skills/pikku-mongodb/SKILL.md +10 -2
  41. package/skills/pikku-n8n-import/SKILL.md +14 -6
  42. package/skills/pikku-permissions/SKILL.md +102 -22
  43. package/skills/pikku-pino/SKILL.md +12 -4
  44. package/skills/pikku-product-second-opinion/SKILL.md +3 -3
  45. package/skills/pikku-queue/SKILL.md +45 -16
  46. package/skills/pikku-react/SKILL.md +41 -14
  47. package/skills/pikku-react-query/SKILL.md +14 -10
  48. package/skills/pikku-realtime/SKILL.md +44 -22
  49. package/skills/pikku-redis/SKILL.md +12 -3
  50. package/skills/pikku-rpc/SKILL.md +23 -12
  51. package/skills/pikku-rtl/SKILL.md +21 -17
  52. package/skills/pikku-scenario/SKILL.md +285 -50
  53. package/skills/pikku-schedule/SKILL.md +39 -6
  54. package/skills/pikku-schema-ajv/SKILL.md +24 -2
  55. package/skills/pikku-schema-cfworker/SKILL.md +22 -2
  56. package/skills/pikku-security/SKILL.md +54 -9
  57. package/skills/pikku-services/SKILL.md +49 -9
  58. package/skills/pikku-services/references/audit-wire-service.md +2 -1
  59. package/skills/pikku-software-archaeology/README.md +16 -6
  60. package/skills/pikku-software-archaeology/references/pikku-mapping.md +30 -30
  61. package/skills/pikku-template-clone/SKILL.md +10 -5
  62. package/skills/pikku-trigger/SKILL.md +50 -6
  63. package/skills/pikku-versioning/SKILL.md +46 -17
  64. package/skills/pikku-websocket/SKILL.md +72 -44
  65. package/skills/pikku-workflow/SKILL.md +35 -1
  66. package/skills/pikku-workflow/references/workflow-reference.md +13 -8
  67. package/skills/pikku-workflows-client/SKILL.md +13 -6
  68. package/skills/pikku-ws/SKILL.md +44 -8
@@ -157,7 +157,42 @@ addHTTPMiddleware([
157
157
 
158
158
  When the api-key header is present it is authoritative — the middleware never
159
159
  falls through to `getSession` (a bare mock session would shadow the scoped one).
160
- When it is absent, the human `getSession` path runs as normal.
160
+ When it is absent, the human `getSession` path runs as normal. Either way the
161
+ middleware bails out entirely if a session is already set, and it checks the
162
+ *live* session rather than the wire's construction-time snapshot, so it can't
163
+ clobber one an earlier middleware resolved.
164
+
165
+ ### Restricting a key below its owner
166
+
167
+ Set `scopes` on the session `mapKey` returns and that set is **authoritative** —
168
+ including an empty one. It is never widened back out to everything the owning
169
+ service user holds:
170
+
171
+ ```typescript
172
+ mapKey: async (key) => ({
173
+ userId: 'sandbox-runtime',
174
+ scopes: ['sandbox:read'], // this key can do only this
175
+ })
176
+ ```
177
+
178
+ Leave `scopes` unset for a key that acts with its owner's full rights. This is
179
+ what makes one stable service user safely able to own keys of very different
180
+ power — the restriction lives on the key, not on a proliferation of identities.
181
+
182
+ ### Failure handling is deliberately split
183
+
184
+ A key that fails to verify is logged and treated as an ordinary "not
185
+ authenticated" — an unusable credential is not an outage. A failure *inside*
186
+ `mapKey` (your scope store is down) propagates as a real error instead. That
187
+ asymmetry is on purpose: a scope lookup that silently failed would serve the
188
+ request anonymously, which is exactly the wrong direction to fail in.
189
+
190
+ ### `betterAuthStatelessSession` has no machine path
191
+
192
+ The lean cookie-cache middleware (`betterAuthStatelessSession` — no
193
+ `services.auth()`, no DB) handles only the human path. Machine auth needs
194
+ `betterAuthSession`, because `verifyApiKey` is a server call there is no
195
+ stateless equivalent of. Both accept an `impersonation` option.
161
196
 
162
197
  ### WebSocket channels authenticate on the upgrade handshake
163
198
 
@@ -2,11 +2,11 @@
2
2
  name: pikku-mcp
3
3
  description: >-
4
4
  Use when exposing Pikku functions as MCP tools, resources, or prompts for AI assistants. Covers
5
- mcp: true flag, pikkuMCPResourceFunc, pikkuMCPPromptFunc, and MCP wire object. TRIGGER when:
6
- code uses mcp: true or pikkuMCPResourceFunc/pikkuMCPPromptFunc, user asks about MCP, Model
7
- Context Protocol, AI tool integration, or exposing functions to Claude/ChatGPT. DO NOT TRIGGER
8
- when: user asks about AI agents (use pikku-ai-agent) or general function definitions (use
9
- pikku-concepts).
5
+ mcp: true, pikkuMCPToolFunc, pikkuMCPResourceFunc, pikkuMCPPromptFunc, wireMCPResource,
6
+ wireMCPPrompt, the MCP wire object and PikkuMCPServer. TRIGGER when: code uses mcp: true or any
7
+ pikkuMCP*Func/wireMCP* helper, user asks about MCP, Model Context Protocol, AI tool integration,
8
+ or exposing functions to Claude/ChatGPT. DO NOT TRIGGER when: user asks about AI agents (use
9
+ pikku-ai-agent) or general function definitions (use pikku-concepts).
10
10
  installGroups: [core]
11
11
  ---
12
12
 
@@ -33,209 +33,219 @@ pikku info tags --verbose # Understand project organization
33
33
 
34
34
  See `pikku-concepts` for the core mental model.
35
35
 
36
+ ## The shape of MCP in Pikku
37
+
38
+ MCP has three surfaces, and Pikku wires them differently:
39
+
40
+ | Surface | Function factory | Wiring | Return type |
41
+ | --- | --- | --- | --- |
42
+ | **Tool** | `mcp: true` on a `pikkuFunc`, or `pikkuMCPToolFunc` | none — the function *is* the registration | the func's own output, or MCP content blocks |
43
+ | **Resource** | `pikkuMCPResourceFunc` | `wireMCPResource({ uri, title, … })` | `Array<{ uri, text }>` |
44
+ | **Prompt** | `pikkuMCPPromptFunc` | `wireMCPPrompt({ name, description, … })` | `Array<MCPPromptMessage>` |
45
+
46
+ Tools are the odd one out — there is no `wireMCPTool`. Resources and prompts
47
+ carry protocol metadata (a URI template, a prompt name) that belongs to the
48
+ endpoint rather than the implementation, so that metadata lives on the wiring and
49
+ the `pikkuMCP*Func` factory stays a plain function.
50
+
51
+ Import every factory and wiring from `#pikku`.
52
+
36
53
  ## API Reference
37
54
 
38
- ### MCP Tools (simplest approach)
55
+ ### Tools
39
56
 
40
- Add `mcp: true` to any existing `pikkuFunc` to expose it as an MCP tool:
57
+ Add `mcp: true` to any existing function:
41
58
 
42
59
  ```typescript
43
- const myFunc = pikkuFunc({
44
- description: string, // Used as MCP tool description
45
- input: ZodSchema, // Becomes MCP tool input schema
46
- output: ZodSchema, // Return type
47
- mcp: true, // ← Expose as MCP tool
48
- func: async (services, data) => { ... },
60
+ export const createTodo = pikkuFunc({
61
+ description: 'Create a new todo item', // becomes the MCP tool description
62
+ input: CreateTodoInput, // becomes the MCP tool input schema
63
+ output: CreateTodoOutput,
64
+ mcp: true,
65
+ func: async ({ db }, { text, priority }) => db.createTodo({ text, priority }),
49
66
  })
50
67
  ```
51
68
 
52
- ### MCP Resources (`pikkuMCPResourceFunc`)
69
+ A missing `description` is all an assistant has to go on, so codegen warns about
70
+ it rather than failing — treat the warning as a bug.
71
+
72
+ Use `pikkuMCPToolFunc` when the tool should control its own presentation. It
73
+ returns MCP content blocks (`{ type: 'text', text }` or `{ type: 'image', data }`
74
+ with base64), so the assistant reads prose rather than raw JSON:
53
75
 
54
76
  ```typescript
55
- import { pikkuMCPResourceFunc } from '#pikku'
77
+ import { pikkuMCPToolFunc } from '#pikku'
56
78
 
57
- const resource = pikkuMCPResourceFunc({
58
- uri: string, // URI template, e.g. 'todos/{id}'
59
- title: string, // Human-readable title
60
- description?: string,
61
- func: async (services, data, { mcp }) => {
62
- // Must return array of { uri, text } or { uri, blob, mimeType }
63
- return [{ uri: mcp.uri!, text: JSON.stringify(result) }]
79
+ export const createTodoTool = pikkuMCPToolFunc({
80
+ description: 'Create a todo item with title, priority, due date and tags',
81
+ input: CreateTodoWithUserInputSchema,
82
+ func: async (_services, input, { rpc }) => {
83
+ const { todo } = await rpc.invoke('createTodo', input)
84
+ return [
85
+ { type: 'text' as const, text: `Created "${todo.title}" (${todo.id})` },
86
+ ]
64
87
  },
65
88
  })
66
89
  ```
67
90
 
68
- ### MCP Prompts (`pikkuMCPPromptFunc`)
91
+ It also accepts `name`, `title`, `summary`, `tags`, `middleware` and
92
+ `permissions`. The function is sessionless and gets `mcp` and `rpc` on its wire —
93
+ calling existing business functions through `rpc.invoke` keeps the tool a thin
94
+ presentation layer over logic that is already tested and reachable over HTTP.
95
+
96
+ ### Resources
69
97
 
70
98
  ```typescript
71
- import { pikkuMCPPromptFunc } from '#pikku'
99
+ import { pikkuMCPResourceFunc } from '#pikku'
72
100
 
73
- const prompt = pikkuMCPPromptFunc({
74
- name: string,
75
- description: string,
76
- func: async (services, data) => {
77
- // Must return array of MCP messages
101
+ export const getTodoResource = pikkuMCPResourceFunc<{ id: string }>(
102
+ async (_services, { id }, { rpc, mcp }) => {
103
+ const { todo } = await rpc.invoke('getTodo', { id })
78
104
  return [
79
105
  {
80
- role: 'user',
81
- content: { type: 'text', text: '...' },
106
+ uri: mcp.uri!,
107
+ text: todo ? formatTodo(todo) : `Todo "${id}" not found.`,
82
108
  },
83
109
  ]
84
- },
85
- })
86
- ```
87
-
88
- ### MCP Wire Object
89
-
90
- Inside MCP-enabled functions, `wire.mcp` provides:
91
-
92
- ```typescript
93
- mcp.uri // Current resource URI (for resources)
94
- mcp.sendResourceUpdated(uri) // Notify clients a resource changed
95
- mcp.enableTools({ toolName: true }) // Dynamically enable/disable tools
110
+ }
111
+ )
96
112
  ```
97
113
 
98
- ## Usage Patterns
99
-
100
- ### Expose Existing Functions as MCP Tools
101
-
102
- The simplest path — add `mcp: true` to any function:
114
+ The factory takes either a bare function (as above) or a config object — `{ func, name }`,
115
+ or `{ func, input }` with a schema. A resource returns `Array<{ uri, text }>`;
116
+ it is text only, with no blob variant. `mcp.uri` is the concrete URI the client
117
+ asked for, which is why each entry echoes it back.
103
118
 
104
119
  ```typescript
105
- export const createTodo = pikkuFunc({
106
- description: 'Create a new todo item',
107
- input: CreateTodoInput,
108
- output: CreateTodoOutput,
109
- mcp: true,
110
- func: async ({ db }, { text, priority }) => {
111
- return await db.createTodo({ text, priority })
112
- },
113
- })
114
- ```
120
+ import { wireMCPResource } from '#pikku'
115
121
 
116
- ### MCP Resources with URI Templates
117
-
118
- ```typescript
119
- export const getTodo = pikkuMCPResourceFunc({
120
- uri: 'todos/{id}',
122
+ wireMCPResource({
123
+ uri: 'todos/{id}', // URI template
121
124
  title: 'Todo Details',
122
- description: 'Get a todo by ID',
123
- func: async ({ db }, { id }, { mcp }) => {
124
- const todo = await db.getTodo(id)
125
- return [{ uri: mcp.uri!, text: JSON.stringify(todo) }]
126
- },
125
+ description: 'Get details of a specific todo by ID',
126
+ func: getTodoResource,
127
+ tags: ['todos'],
128
+ // also: summary?, mimeType?, size?, streaming?, errors?, middleware?
127
129
  })
128
130
  ```
129
131
 
130
- ### MCP Prompts
132
+ Every `{param}` in `uri` is checked against the function's input at compile time,
133
+ so `todos/{id}` wired to a function whose input has no `id` fails to build rather
134
+ than handing the function an `undefined`.
135
+
136
+ ### Prompts
131
137
 
132
138
  ```typescript
133
- export const codeReview = pikkuMCPPromptFunc({
134
- name: 'codeReview',
135
- description: 'Generate a code review prompt',
136
- func: async ({}, { filePath, context }) => {
139
+ import { pikkuMCPPromptFunc, wireMCPPrompt } from '#pikku'
140
+
141
+ export const planDayPrompt = pikkuMCPPromptFunc({
142
+ input: UserIdInputSchema,
143
+ func: async (_services, { userId }, { rpc }) => {
144
+ const { todos } = await rpc.invoke('listTodos', { userId, completed: false })
137
145
  return [
138
146
  {
139
- role: 'user',
147
+ role: 'user' as const,
140
148
  content: {
141
- type: 'text',
142
- text: `Review ${filePath}. Context: ${context}`,
149
+ type: 'text' as const,
150
+ text: `Plan my day:\n${todos.map(formatTodo).join('\n')}`,
143
151
  },
144
152
  },
145
153
  ]
146
154
  },
147
155
  })
156
+
157
+ wireMCPPrompt({
158
+ name: 'planDay',
159
+ description: 'Generate a daily plan based on pending todos',
160
+ func: planDayPrompt,
161
+ tags: ['productivity'],
162
+ })
148
163
  ```
149
164
 
150
- ### Dynamic Tool Control
165
+ A message's `role` is `'user' | 'assistant' | 'system'` and its `content.type` is
166
+ `'text' | 'image'`. The prompt arguments the client sees are derived from the
167
+ input schema at codegen time: each property becomes a named argument, and
168
+ schema-required properties become required arguments.
169
+
170
+ ### MCP Wire Object
171
+
172
+ Available as `wire.mcp` inside any MCP function:
173
+
174
+ ```typescript
175
+ mcp.uri // the resolved resource URI (resources only)
176
+ mcp.sendResourceUpdated(uri) // notify clients a resource changed
177
+ await mcp.enableTools({ archiveTodos: true })
178
+ await mcp.enableResources({ todoDetails: false })
179
+ await mcp.enablePrompts({ planDay: true })
180
+ ```
181
+
182
+ The `enable*` calls are how a server presents a changing surface — hiding tools
183
+ that are meaningless in the current state beats letting the assistant call them
184
+ and fail. Each returns a boolean, and each name is typechecked against your
185
+ generated endpoint names.
151
186
 
152
187
  ```typescript
153
- export const manageTodos = pikkuFunc({
154
- description: 'Manage todo items',
155
- input: ManageTodosInput,
156
- output: ManageTodosOutput,
188
+ export const deleteTodo = pikkuFunc({
189
+ description: 'Delete a todo item',
157
190
  mcp: true,
158
- func: async ({ db }, { action, id }, { mcp }) => {
159
- if (action === 'delete') {
160
- await db.deleteTodo(id)
161
- mcp.sendResourceUpdated(`todos/${id}`)
162
- await mcp.enableTools({ archiveTodos: true })
163
- return { deleted: true }
164
- }
191
+ func: async ({ db }, { id }, { mcp }) => {
192
+ await db.deleteTodo(id)
193
+ mcp.sendResourceUpdated(`todos/${id}`)
194
+ return { deleted: true }
165
195
  },
166
196
  })
167
197
  ```
168
198
 
169
- ### MCP Server Setup
199
+ ## MCP Server Setup
200
+
201
+ `PikkuMCPServer` takes the server config and a logger — not your services. It
202
+ loads the generated `mcp.gen.json`, and the bootstrap import is what registers
203
+ your functions.
170
204
 
171
205
  ```typescript
172
206
  // start.ts
173
207
  import { PikkuMCPServer } from '@pikku/modelcontextprotocol'
208
+ import { createConfig, createSingletonServices } from './services.js'
209
+ import mcpJSON from '../.pikku/mcp/mcp.gen.json' with { type: 'json' }
210
+ import '../.pikku/pikku-bootstrap.gen.js'
211
+
212
+ const config = await createConfig()
213
+ const singletonServices = await createSingletonServices(config)
214
+
215
+ const server = new PikkuMCPServer(
216
+ {
217
+ name: 'pikku-mcp-server',
218
+ version: '1.0.0',
219
+ mcpJSON,
220
+ capabilities: { logging: {}, tools: {}, resources: {}, prompts: {} },
221
+ },
222
+ singletonServices.logger
223
+ )
174
224
 
175
- const server = new PikkuMCPServer(config, singletonServices, createWireServices)
176
225
  await server.init()
177
- await server.start()
178
- ```
179
226
 
180
- ## Complete Example
227
+ // stdio — the transport desktop MCP clients spawn
228
+ await server.connectStdio()
229
+ singletonServices.logger = server.createMCPLogger()
181
230
 
182
- ```typescript
183
- // functions/todos.functions.ts
184
- export const listTodos = pikkuSessionlessFunc({
185
- description: 'List all todo items',
186
- input: ListTodosInput,
187
- output: ListTodosOutput,
188
- mcp: true,
189
- func: async ({ db }, { status }) => {
190
- return { todos: await db.listTodos(status) }
191
- },
192
- })
231
+ // …or streamable HTTP, for a hosted server
232
+ const { close } = await server.connectHTTP({ port: 3000, host: '127.0.0.1' })
233
+ ```
193
234
 
194
- export const createTodo = pikkuFunc({
195
- description: 'Create a new todo item',
196
- input: CreateTodoInput,
197
- output: CreateTodoOutput,
198
- mcp: true,
199
- func: async ({ db }, { text, priority }) => {
200
- return await db.createTodo({ text, priority })
201
- },
202
- })
235
+ `capabilities` is a filter, not documentation: a surface you leave out is not
236
+ advertised and its endpoints are never loaded, which is how you ship a tools-only
237
+ server.
203
238
 
204
- export const completeTodo = pikkuFunc({
205
- description: 'Mark a todo as complete',
206
- input: CompleteTodoInput,
207
- output: CompleteTodoOutput,
208
- mcp: true,
209
- func: async ({ db }, { todoId }) => {
210
- return await db.completeTodo(todoId)
211
- },
212
- })
239
+ Over stdio the protocol owns stdout, so an ordinary console logger corrupts the
240
+ frames — that is what `createMCPLogger()` is for. Swap the logger before
241
+ anything logs.
213
242
 
214
- // functions/todos.mcp.ts
215
- export const getTodoResource = pikkuMCPResourceFunc({
216
- uri: 'todos/{id}',
217
- title: 'Todo Details',
218
- description: 'Get details of a specific todo',
219
- func: async ({ db }, { id }, { mcp }) => {
220
- const todo = await db.getTodo(id)
221
- return [{ uri: mcp.uri!, text: JSON.stringify(todo) }]
222
- },
223
- })
243
+ ## Red flags
224
244
 
225
- export const planDayPrompt = pikkuMCPPromptFunc({
226
- name: 'planDay',
227
- description: 'Create a daily plan based on pending todos',
228
- func: async ({ db }, {}) => {
229
- const { todos } = await db.listTodos('pending')
230
- return [
231
- {
232
- role: 'user',
233
- content: {
234
- type: 'text',
235
- text: `Plan my day. Here are my pending todos:\n${todos.map((t) => `- ${t.text} (${t.priority})`).join('\n')}`,
236
- },
237
- },
238
- ]
239
- },
240
- })
241
- ```
245
+ | Symptom | Cause |
246
+ | --- | --- |
247
+ | `wireMCPTool` is not exported | There is no tool wiring — use `mcp: true` or `pikkuMCPToolFunc` |
248
+ | `uri`/`title` rejected on `pikkuMCPResourceFunc` | Those belong on `wireMCPResource` |
249
+ | Resource returning `{ uri, blob, mimeType }` | Resources are text only: `{ uri, text }` |
250
+ | Client sees a tool with no description | `mcp: true` without a `description` — check the codegen warning |
251
+ | stdio client disconnects on the first log line | Logger still writing to stdout; use `createMCPLogger()` |
@@ -136,7 +136,7 @@ Tags from the function definition and the wire object are merged — middleware
136
136
  ### Registering Tag Middleware
137
137
 
138
138
  ```typescript
139
- import { addTagMiddleware } from '.pikku/pikku-types.gen.js'
139
+ import { addTagMiddleware } from '#pikku'
140
140
 
141
141
  addTagMiddleware('machine-agent', [machineAgentBearerAuth])
142
142
  ```
@@ -145,18 +145,30 @@ Call at module load time — typically in the same `wirings/*.ts` file as the `w
145
145
 
146
146
  ## Middleware Execution Order
147
147
 
148
- **Scope resolution order (broadest → narrowest):**
148
+ Resolution happens in two steps, and the order matters more than it looks.
149
+
150
+ **Step 1 — collect, broadest → narrowest:**
149
151
 
150
152
  ```text
151
153
  global → httpGroup/* → httpGroup/prefix → wiringTags → wiringMiddleware → funcTags → funcMiddleware → function body
152
154
  ```
153
155
 
154
- **Within each scope, sorted by priority:**
156
+ **Step 2 — sort that whole flat list by priority:**
155
157
 
156
158
  ```text
157
159
  highest → high → medium (default) → low → lowest
158
160
  ```
159
161
 
162
+ **Priority is the primary key across every scope, not within one.** The collected
163
+ list is flattened first and sorted once, so a `priority: 'lowest'` global
164
+ middleware runs *after* an inline per-route middleware of default priority — the
165
+ narrower scope does not win. Scope order survives only as the tiebreaker between
166
+ middleware of equal priority, because the sort is stable.
167
+
168
+ This is what makes `telemetryOuter`/`telemetryInner` work: they pin themselves to
169
+ `highest`/`lowest` so they bracket every other middleware no matter where those
170
+ were registered.
171
+
160
172
  Set priority using the config-object form of `pikkuMiddleware`:
161
173
 
162
174
  ```typescript
@@ -167,7 +179,7 @@ const earlyMiddleware = pikkuMiddleware({
167
179
  })
168
180
  ```
169
181
 
170
- Priority is the primary sort key; within the same level, registration order is preserved. Use priority when a middleware must run before/after others regardless of registration order (e.g. telemetry wrapping everything, session extraction before auth checks).
182
+ Within the same priority level, the collection order above is preserved. Use priority when a middleware must run before/after others regardless of where it was registered (e.g. telemetry wrapping everything, session extraction before auth checks).
171
183
 
172
184
  ## Service-to-Service Bearer Auth (canonical pattern)
173
185
 
@@ -185,7 +197,7 @@ export const getToken = () => _token
185
197
  ```typescript
186
198
  // wirings/http.wiring.ts
187
199
  import { timingSafeEqual } from 'node:crypto'
188
- import { addTagMiddleware, pikkuMiddleware } from '../../.pikku/pikku-types.gen.js'
200
+ import { addTagMiddleware, pikkuMiddleware } from '#pikku'
189
201
  import { UnauthorizedError } from '@pikku/core/errors'
190
202
  import { getToken } from '../lib/host-token.js'
191
203
 
@@ -59,17 +59,25 @@ await mongo.close()
59
59
  | `MongoDBAIStorageService` | `AIStorageService, AIRunStateService` | AI conversation/run storage |
60
60
  | `MongoDBAgentRunService` | `AgentRunService` | Agent execution tracking |
61
61
  | `MongoDBSecretService` | `SecretService` | Encrypted secret storage (envelope encryption) |
62
+ | `MongoDBSessionStore` | `SessionStore` | Persisted user sessions |
62
63
 
63
64
  All services take a `Db` instance in their constructor and have an `init()` method that creates collections/indexes.
64
65
 
65
66
  ### Secret Service
66
67
 
68
+ Envelope encryption: `key` derives the KEK that wraps each secret's own DEK.
69
+ Keeping `previousKey` set is what makes `rotateKEK()` possible — it re-wraps
70
+ every secret onto the current key and returns the new version.
71
+
67
72
  ```typescript
68
73
  import { MongoDBSecretService } from '@pikku/mongodb'
69
74
 
70
75
  const secrets = new MongoDBSecretService(mongo.db, {
71
- kekSecret: 'your-key-encryption-key',
72
- salt: 'your-salt',
76
+ key: 'your-key-encryption-passphrase',
77
+ keyVersion: 2, // defaults to 1
78
+ previousKey: 'the-passphrase-you-are-rotating-away-from',
79
+ audit: true, // log write/delete/rotate through the audit sink
80
+ auditReads: false, // reads too — noisy, off by default
73
81
  })
74
82
  await secrets.init()
75
83
 
@@ -35,16 +35,24 @@ missing dependency).
35
35
  ### 1 — Run the importer (do as much as possible, cheaply)
36
36
 
37
37
  ```bash
38
- pikku import n8n <export.json> [outDir]
38
+ pikku import n8n <file> [--out <dir>] # -o for short
39
39
  ```
40
40
 
41
+ The output directory is an **option**, not a positional argument; omitted, it
42
+ falls back to `scaffold.functionDir` from `pikku.config.json`, then cwd.
43
+
44
+ `<file>` is one export, **or a directory** — the command reads every `.json` in
45
+ it — and either form may hold a single workflow object, a bare array (`n8n
46
+ export:workflow --all`), or a `{ workflows: [...] }` wrapper. All of those are
47
+ flattened into one import per workflow, so there is no need to loop yourself.
48
+
41
49
  It writes `<slug>.graph.ts` (+ `.agent.ts` for AI workflows), `<slug>.addons.gen.ts`,
42
50
  a `<slug>.integrations.json` manifest, and one stub function per node it could not
43
- map. It **exits 1** on an un-importable input (a cross-workflow sub-workflow
44
- reference, a dynamic workflow target, a mid-flow `respondToWebhook`) with a
45
- `[reason] message` — relay that to the user; do not fake a partial scaffold.
46
-
47
- For a directory of exports, run it per file.
51
+ map. An un-importable workflow (a cross-workflow sub-workflow reference, a dynamic
52
+ workflow target, a mid-flow `respondToWebhook`) is reported as `[reason] message`
53
+ and **skipped** — nothing partial is written for it. Across a batch the others
54
+ still import; the command exits 1 at the end if any failed, so read the log rather
55
+ than the exit code to know what landed. Relay every skipped workflow to the user.
48
56
 
49
57
  ### 2 — Triage what it left
50
58