@pikku/skills 0.12.9 → 0.12.11

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 (75) hide show
  1. package/CHANGELOG.md +768 -0
  2. package/dist/skills.gen.js +1 -1
  3. package/package.json +4 -4
  4. package/skills/pikku-addon/SKILL.md +20 -14
  5. package/skills/pikku-addon/references/addon-package-manifest.md +2 -2
  6. package/skills/{pikku-ai-agent → pikku-agent}/SKILL.md +24 -24
  7. package/skills/pikku-ai-vercel/SKILL.md +18 -18
  8. package/skills/pikku-ai-voice/SKILL.md +15 -15
  9. package/skills/pikku-audit/SKILL.md +28 -13
  10. package/skills/pikku-aws/SKILL.md +2 -2
  11. package/skills/pikku-better-auth/SKILL.md +97 -17
  12. package/skills/pikku-build-app/SKILL.md +621 -0
  13. package/skills/pikku-build-app/references/multi-app.md +117 -0
  14. package/skills/pikku-build-app/references/ship.md +98 -0
  15. package/skills/pikku-build-app/references/theming.md +70 -0
  16. package/skills/pikku-build-platform/SKILL.md +239 -0
  17. package/skills/pikku-build-quick/SKILL.md +238 -0
  18. package/skills/pikku-cli/SKILL.md +7 -7
  19. package/skills/pikku-cli/references/complete-example.md +1 -1
  20. package/skills/pikku-concepts/SKILL.md +10 -7
  21. package/skills/pikku-concepts/references/concept-mapping.md +1 -1
  22. package/skills/pikku-config/SKILL.md +5 -3
  23. package/skills/pikku-deploy-azure/SKILL.md +5 -4
  24. package/skills/pikku-deploy-cloudflare/SKILL.md +9 -9
  25. package/skills/pikku-deploy-uws/SKILL.md +5 -2
  26. package/skills/pikku-deps/SKILL.md +42 -3
  27. package/skills/pikku-emails/SKILL.md +5 -5
  28. package/skills/pikku-fabric/SKILL.md +27 -3
  29. package/skills/pikku-fabric-debug/SKILL.md +1 -1
  30. package/skills/pikku-feature/SKILL.md +5 -4
  31. package/skills/pikku-http/SKILL.md +4 -4
  32. package/skills/pikku-http/references/http-options.md +13 -13
  33. package/skills/pikku-i18n/SKILL.md +2 -1
  34. package/skills/pikku-info/SKILL.md +1 -1
  35. package/skills/pikku-knowledge/SKILL.md +13 -13
  36. package/skills/pikku-kysely/SKILL.md +68 -41
  37. package/skills/pikku-machine-auth/SKILL.md +10 -10
  38. package/skills/pikku-mcp/SKILL.md +23 -20
  39. package/skills/pikku-middleware/SKILL.md +19 -12
  40. package/skills/pikku-middleware/references/middleware-patterns.md +14 -8
  41. package/skills/pikku-mongodb/SKILL.md +11 -11
  42. package/skills/pikku-n8n-import/SKILL.md +12 -12
  43. package/skills/pikku-n8n-import/SPEC.md +3 -0
  44. package/skills/pikku-n8n-import/references/addon-mapping.md +14 -8
  45. package/skills/pikku-n8n-import/references/code-translation.md +26 -22
  46. package/skills/pikku-n8n-import/references/loops-and-control.md +7 -7
  47. package/skills/pikku-paraglide/SKILL.md +11 -6
  48. package/skills/pikku-permissions/SKILL.md +19 -15
  49. package/skills/pikku-product-second-opinion/README.md +3 -3
  50. package/skills/pikku-product-second-opinion/SKILL.md +83 -73
  51. package/skills/pikku-product-second-opinion/example/sample-report.md +15 -12
  52. package/skills/pikku-product-second-opinion/references/report-template.md +10 -7
  53. package/skills/pikku-queue/SKILL.md +1 -1
  54. package/skills/pikku-react/SKILL.md +53 -13
  55. package/skills/pikku-realtime/SKILL.md +51 -19
  56. package/skills/pikku-rpc/SKILL.md +1 -1
  57. package/skills/pikku-rtl/SKILL.md +1 -1
  58. package/skills/pikku-scenario/SKILL.md +164 -44
  59. package/skills/pikku-schedule/SKILL.md +6 -1
  60. package/skills/pikku-schema-ajv/SKILL.md +2 -2
  61. package/skills/pikku-schema-cfworker/SKILL.md +1 -1
  62. package/skills/pikku-security/SKILL.md +9 -5
  63. package/skills/pikku-services/SKILL.md +27 -18
  64. package/skills/pikku-services/references/audit-wire-service.md +14 -8
  65. package/skills/pikku-software-archaeology/SKILL.md +27 -23
  66. package/skills/pikku-software-archaeology/references/blueprint.schema.json +580 -102
  67. package/skills/pikku-software-archaeology/scripts/validate.mjs +146 -69
  68. package/skills/pikku-tag-middleware/SKILL.md +1 -0
  69. package/skills/pikku-template-clone/SKILL.md +2 -1
  70. package/skills/pikku-trigger/SKILL.md +3 -3
  71. package/skills/pikku-versioning/SKILL.md +87 -3
  72. package/skills/pikku-websocket/SKILL.md +4 -3
  73. package/skills/pikku-workflow/SKILL.md +2 -2
  74. package/skills/pikku-workflow/references/workflow-reference.md +24 -10
  75. package/skills/pikku-ws/SKILL.md +5 -2
@@ -38,7 +38,7 @@ Follow existing patterns you find (naming, tag usage, file organization). See `p
38
38
 
39
39
  ## API Reference
40
40
 
41
- All three come from `#pikku` (the generated `.pikku/pikku-types.gen.js`), which
41
+ All three come from `#pikku/http` (the generated `.pikku/http/index.ts`), which
42
42
  binds them to your project's service, session and middleware types. The
43
43
  `@pikku/core/http` versions are the unbound generics — they compile, but you
44
44
  lose the typing that makes the wiring worth having.
@@ -59,7 +59,7 @@ addHTTPMiddleware('*', [authBearer()]) // All routes
59
59
  addHTTPMiddleware('/api/*', [rateLimit()]) // Pattern match
60
60
  ```
61
61
 
62
- > HTTP-route-level permissions (`addHTTPPermission`, a `permissions` field on the wiring) were removed in #972. Declare authorization on the function definition (`pikkuFunc({ permissions })`, see `pikku-permissions`), or app-wide via `addGlobalPermission`. Tags/patterns are for *middleware* only now.
62
+ > HTTP-route-level permissions (`addHTTPPermission`, a `permissions` field on the wiring) were removed in #972. Declare authorization on the function definition (`pikkuFunc({ permissions })`, see `pikku-permissions`), or app-wide via `addGlobalPermission`. Tags/patterns are for _middleware_ only now.
63
63
 
64
64
  ## Data Flow
65
65
 
@@ -211,7 +211,7 @@ Functions live in their own files (one per file) and supply behavior + `permissi
211
211
 
212
212
  ```typescript
213
213
  // functions/books.functions.ts
214
- import { pikkuFunc, pikkuSessionlessFunc } from '#pikku'
214
+ import { pikkuFunc, pikkuSessionlessFunc } from '#pikku/function'
215
215
 
216
216
  export const listBooks = pikkuSessionlessFunc({
217
217
  title: 'List Books',
@@ -226,7 +226,7 @@ export const getBook = pikkuFunc({
226
226
  })
227
227
 
228
228
  // wirings/books.http.ts — same defineHTTPRoutes/wireHTTPRoutes shape as the Route Groups example above
229
- import { addHTTPMiddleware } from '#pikku'
229
+ import { addHTTPMiddleware } from '#pikku/http'
230
230
  import { cors, authBearer } from '@pikku/core/middleware'
231
231
 
232
232
  addHTTPMiddleware('*', [cors(), authBearer()])
@@ -4,19 +4,19 @@
4
4
 
5
5
  Wire a single function to an HTTP endpoint. Import from `#pikku`.
6
6
 
7
- | Option | Type | Notes |
8
- | --- | --- | --- |
9
- | `method` | `'get' \| 'post' \| 'put' \| 'patch' \| 'delete' \| 'head' \| 'options'` | HTTP verb |
10
- | `route` | `string` | e.g. `/books/:bookId` — `:params` become `data` fields |
11
- | `func` | `PikkuFunc` | The function to call |
12
- | `auth?` | `boolean` | Override default auth (`true` = require session) |
13
- | `tags?` | `string[]` | For grouping, middleware targeting |
14
- | `middleware?` | `PikkuMiddleware[]` | Per-route middleware |
15
- | `sse?` | `boolean` | Enable Server-Sent Events — **`method: 'get'` only** |
16
- | `query?` | `Array<keyof In>` | **`method: 'post'` only** — input fields also read from the query string |
17
- | `contentType?` | `'xml' \| 'json'` | Response content type |
18
- | `timeout?` | `number` | Request timeout in ms |
19
- | `headers?` | `HTTPHeadersSchema` | Expected headers schema |
7
+ | Option | Type | Notes |
8
+ | -------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------ |
9
+ | `method` | `'get' \| 'post' \| 'put' \| 'patch' \| 'delete' \| 'head' \| 'options'` | HTTP verb |
10
+ | `route` | `string` | e.g. `/books/:bookId` — `:params` become `data` fields |
11
+ | `func` | `PikkuFunc` | The function to call |
12
+ | `auth?` | `boolean` | Override default auth (`true` = require session) |
13
+ | `tags?` | `string[]` | For grouping, middleware targeting |
14
+ | `middleware?` | `PikkuMiddleware[]` | Per-route middleware |
15
+ | `sse?` | `boolean` | Enable Server-Sent Events — **`method: 'get'` only** |
16
+ | `query?` | `Array<keyof In>` | **`method: 'post'` only** — input fields also read from the query string |
17
+ | `contentType?` | `'xml' \| 'json'` | Response content type |
18
+ | `timeout?` | `number` | Request timeout in ms |
19
+ | `headers?` | `HTTPHeadersSchema` | Expected headers schema |
20
20
 
21
21
  `sse` and `query` are constrained by the config union rather than by a runtime
22
22
  check, so a `sse: true` on a `post` fails to typecheck rather than silently
@@ -142,7 +142,8 @@ The wrapper alternative — a module that walks the namespace and pipes each mes
142
142
 
143
143
  `packages/console` is the one place in this repo that still wraps it, in `src/i18n/messages.ts`, to keep the debug mask (`█`) it carried over from i18next. That wrapper is a leftover, not a pattern — the generated-locale approach above is how a new app gets the same masking without touching every export. Don't copy it.
144
144
 
145
- The `mKey`/`mList` runtime resolvers that used to live beside it are **gone**, and must not come back. `mList` resolved indexed `prefix.0` keys that no longer exist; `mKey` took a computed string, which is exactly the type safety Paraglide exists to provide. Where a key really is dynamic, map the discriminant to a message *function* and call it — the map is type-checked, a string is not.
145
+ The `mKey`/`mList` runtime resolvers that used to live beside it are **gone**, and must not come back. `mList` resolved indexed `prefix.0` keys that no longer exist; `mKey` took a computed string, which is exactly the type safety Paraglide exists to provide. Where a key really is dynamic, map the discriminant to a message _function_ and call it — the map is type-checked, a string is not.
146
+
146
147
  - Don't re-resolve messages by string key or re-implement `{param}` interpolation. A key-string resolver turns a missing key back into silent runtime text, surrendering the type safety that is the entire reason to use Paraglide.
147
148
  - Don't reach for i18next/react-i18next or a runtime-fetch translation loader — Paraglide's compiled functions are the whole delivery mechanism.
148
149
  - Don't tokenize backend error messages or logs here — those are not frontend display strings.
@@ -29,7 +29,7 @@ Use the `pikku info` CLI commands to inspect this Pikku project. Run the command
29
29
 
30
30
  There are exactly four subcommands — `functions`, `tags`, `middleware`,
31
31
  `permissions`. Routes, channels, schedulers and queues are not separate
32
- subcommands; they show up as the *transport* column of `info functions --verbose`.
32
+ subcommands; they show up as the _transport_ column of `info functions --verbose`.
33
33
 
34
34
  ## Available Commands
35
35
 
@@ -66,7 +66,7 @@ Frontmatter fields:
66
66
 
67
67
  | Field | Meaning |
68
68
  | ------------- | ----------------------------------------------------------------------------------------------------------------------- |
69
- | `type` | **The only required field.** `slice`, `entity`, `decision`, `note`, `overview`. Lowercase — gates compare it literally. |
69
+ | `type` | **The only required field.** `slice` — or `milestone`, when the project's own `knowledge/index.md` names the section that way; `validate` accepts both, so follow the scaffold rather than this list. Then `entity`, `decision`, `note`, `overview`. Lowercase — gates compare it literally. |
70
70
  | `title` | What to call the note in a listing. Falls back to the first heading, then the filename. |
71
71
  | `description` | One line, used as the note's subtitle in a section index. |
72
72
  | `resource` | Comma-separated `<kind>:<id>` URIs — the code this note is about. See below. |
@@ -193,19 +193,19 @@ It is a **summary, not the note** — the argument continues in prose underneath
193
193
 
194
194
  **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
195
 
196
- | Kind | An id is | Where it resolves |
197
- | ----------- | -------------------------------------------------- | --------------------------------------------------------------------------------- |
198
- | `func:` | a function id | generated function meta |
199
- | `workflow:` | a workflow name | generated workflow meta |
200
- | `schema:` | a schema name | generated schemas |
201
- | `http:` | a route, `method:route`, or the function behind it | generated http wirings |
202
- | `queue:` | a queue name | generated queue wirings |
203
- | `cron:` | a scheduled task name | generated scheduler wirings |
204
- | `channel:` | a channel name | generated channel meta |
205
- | `table:` | a table name | the generated db schema |
206
- | `addon:` | `@pikku/addon-x` or bare `x` | the manifests that declare the dependency |
196
+ | Kind | An id is | Where it resolves |
197
+ | ----------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------ |
198
+ | `func:` | a function id | generated function meta |
199
+ | `workflow:` | a workflow name | generated workflow meta |
200
+ | `schema:` | a schema name | generated schemas |
201
+ | `http:` | a route, `method:route`, or the function behind it | generated http wirings |
202
+ | `queue:` | a queue name | generated queue wirings |
203
+ | `cron:` | a scheduled task name | generated scheduler wirings |
204
+ | `channel:` | a channel name | generated channel meta |
205
+ | `table:` | a table name | the generated db schema |
206
+ | `addon:` | `@pikku/addon-x` or bare `x` | the manifests that declare the dependency |
207
207
  | `scope:` | a scope name | the `scopes:` a function gates itself with, plus the scopes a `defineSystemRole()` confers |
208
- | `persona:` | a persona name | `definePersonas()` |
208
+ | `persona:` | a persona name | `definePersonas()` |
209
209
 
210
210
  Ids are case-sensitive: `createEntry` is not `createentry`.
211
211
 
@@ -28,25 +28,29 @@ Use this skill as an execution checklist, not reference material.
28
28
 
29
29
  ## Writing Queries — the Kysely query builder
30
30
 
31
- In a Pikku function body the injected `kysely` IS the `Kysely<DB>` instance — query it directly. Every connection factory below wires the **CamelCasePlugin** by default, so you write **camelCase everywhere in TS** (columns, aliases) and raw **snake_case ONLY inside a `` sql`` `` literal**. If a project opted out (`createNodeSqliteKysely({ camelCase: false })`), that inverts — check how the instance was built before assuming. Kysely is a query builder, NOT an ORM — there are no relations; shape nested data with the JSON helpers below. Never hand-roll SQL strings; never annotate the return type (in Pikku the output zod schema IS the type).
31
+ In a Pikku function body the injected `kysely` IS the `Kysely<DB>` instance — query it directly. Every connection factory below wires the **CamelCasePlugin** by default, so you write **camelCase everywhere in TS** (columns, aliases) and raw **snake_case ONLY inside a ` sql` `` literal**. If a project opted out (`createNodeSqliteKysely({ camelCase: false })`), that inverts — check how the instance was built before assuming. Kysely is a query builder, NOT an ORM — there are no relations; shape nested data with the JSON helpers below. Never hand-roll SQL strings; never annotate the return type (in Pikku the output zod schema IS the type).
32
32
 
33
33
  ```typescript
34
34
  import { sql } from 'kysely'
35
35
  // Relation helpers are ENGINE-SPECIFIC — import the matching path:
36
- import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/sqlite' // SQLite / libSQL
36
+ import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/sqlite' // SQLite / libSQL
37
37
  // import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres' // Postgres
38
38
  ```
39
39
 
40
40
  ```typescript
41
41
  // SELECT + where/orderBy/limit/offset. Terminals: .execute() | .executeTakeFirst()
42
42
  // | .executeTakeFirstOrThrow(() => new NotFoundError()) — pass an error factory.
43
- const rows = await kysely.selectFrom('item')
43
+ const rows = await kysely
44
+ .selectFrom('item')
44
45
  .select(['id', 'name', 'quantity'])
45
46
  .where('warehouseId', '=', warehouseId)
46
- .orderBy('name').limit(50).execute()
47
+ .orderBy('name')
48
+ .limit(50)
49
+ .execute()
47
50
 
48
51
  // JOINS + aliased selects (qualify columns once a join exists)
49
- await kysely.selectFrom('stock')
52
+ await kysely
53
+ .selectFrom('stock')
50
54
  .innerJoin('item', 'item.id', 'stock.itemId')
51
55
  .leftJoin('bin as b', 'b.id', 'stock.binId')
52
56
  .select(['stock.id', 'item.name as itemName', 'b.code as binCode'])
@@ -54,41 +58,64 @@ await kysely.selectFrom('stock')
54
58
 
55
59
  // AGGREGATES via the fn helper + groupBy/having. eb.fn.count returns string|number —
56
60
  // cast if you need a JS number (SQLite: CAST(... AS INTEGER)).
57
- await kysely.selectFrom('stock')
61
+ await kysely
62
+ .selectFrom('stock')
58
63
  .select((eb) => ['itemId', eb.fn.sum<number>('quantity').as('onHand')])
59
64
  .groupBy('itemId')
60
- .having((eb) => eb.fn.sum('quantity'), '<', 10) // low-stock
65
+ .having((eb) => eb.fn.sum('quantity'), '<', 10) // low-stock
61
66
  .execute()
62
67
 
63
68
  // INSERT + RETURNING (one round-trip; works on SQLite & Postgres)
64
- const created = await kysely.insertInto('item')
69
+ const created = await kysely
70
+ .insertInto('item')
65
71
  .values({ name: input.name, warehouseId })
66
- .returning(['id', 'name']).executeTakeFirstOrThrow()
72
+ .returning(['id', 'name'])
73
+ .executeTakeFirstOrThrow()
67
74
 
68
75
  // UPDATE + RETURNING, DELETE
69
- await kysely.updateTable('item').set({ quantity: input.quantity })
70
- .where('id', '=', input.id).returning(['id', 'quantity']).executeTakeFirstOrThrow()
76
+ await kysely
77
+ .updateTable('item')
78
+ .set({ quantity: input.quantity })
79
+ .where('id', '=', input.id)
80
+ .returning(['id', 'quantity'])
81
+ .executeTakeFirstOrThrow()
71
82
  await kysely.deleteFrom('item').where('id', '=', input.id).execute()
72
83
 
73
84
  // EXPRESSION BUILDER for and/or; $if for conditional building; sql for raw fragments
74
- await kysely.selectFrom('item')
85
+ await kysely
86
+ .selectFrom('item')
75
87
  .selectAll()
76
88
  .where((eb) => eb.or([eb('quantity', '=', 0), eb('discontinued', '=', true)]))
77
89
  .$if(!!input.search, (qb) => qb.where('name', 'like', `%${input.search}%`))
78
- .select(sql<number>`quantity * unit_cost`.as('value')) // snake_case ok inside sql``
90
+ .select(sql<number>`quantity * unit_cost`.as('value')) // snake_case ok inside sql``
79
91
  .execute()
80
92
 
81
93
  // NESTED DATA (no relations) — jsonObjectFrom (one) / jsonArrayFrom (many)
82
- await kysely.selectFrom('warehouse')
83
- .select((eb) => ['warehouse.id', 'warehouse.name',
84
- jsonArrayFrom(eb.selectFrom('bin').select(['bin.id', 'bin.code'])
85
- .whereRef('bin.warehouseId', '=', 'warehouse.id')).as('bins')])
94
+ await kysely
95
+ .selectFrom('warehouse')
96
+ .select((eb) => [
97
+ 'warehouse.id',
98
+ 'warehouse.name',
99
+ jsonArrayFrom(
100
+ eb
101
+ .selectFrom('bin')
102
+ .select(['bin.id', 'bin.code'])
103
+ .whereRef('bin.warehouseId', '=', 'warehouse.id')
104
+ ).as('bins'),
105
+ ])
86
106
  .execute()
87
107
 
88
108
  // TRANSACTION — multi-write atomicity. Use trx (not kysely) inside.
89
109
  await kysely.transaction().execute(async (trx) => {
90
- await trx.updateTable('stock').set({ quantity: 0 }).where('itemId', '=', id).execute()
91
- await trx.insertInto('stockMove').values({ itemId: id, delta: -qty }).execute()
110
+ await trx
111
+ .updateTable('stock')
112
+ .set({ quantity: 0 })
113
+ .where('itemId', '=', id)
114
+ .execute()
115
+ await trx
116
+ .insertInto('stockMove')
117
+ .values({ itemId: id, delta: -qty })
118
+ .execute()
92
119
  })
93
120
  ```
94
121
 
@@ -151,10 +178,10 @@ import { createNodeSqliteKysely } from '@pikku/kysely-node-sqlite'
151
178
 
152
179
  // Your application DB — CamelCasePlugin on by default
153
180
  const kysely = createNodeSqliteKysely<DB>({
154
- filename: 'app.db', // or ':memory:'
181
+ filename: 'app.db', // or ':memory:'
155
182
  camelCase: true,
156
- plugins: [], // layered on top
157
- functions: {}, // scalar UDFs, registered as deterministic (Node only)
183
+ plugins: [], // layered on top
184
+ functions: {}, // scalar UDFs, registered as deterministic (Node only)
158
185
  })
159
186
  ```
160
187
 
@@ -175,29 +202,29 @@ functions query.
175
202
 
176
203
  Each database variant exports these services with a prefix (`Pg`, `MySQL`, `SQLite`, or base `Kysely`):
177
204
 
178
- | Service | Interface | Purpose |
179
- | --------------------- | ------------------------------------- | ---------------------------------------------- |
180
- | `*ChannelStore` | `ChannelStore` | WebSocket channel state persistence |
181
- | `*EventHubStore` | `EventHubStore` | Event hub state persistence |
182
- | `*WorkflowService` | `PikkuWorkflowService` | Workflow definition storage |
183
- | `*WorkflowRunService` | `WorkflowRunService` | Workflow execution tracking |
184
- | `*DeploymentService` | `DeploymentService` | Deployment state management |
185
- | `*AIStorageService` | `AIStorageService, AIRunStateService` | AI conversation/run storage |
186
- | `*AgentRunService` | `AgentRunService` | Agent execution tracking |
187
- | `*SecretService` | `SecretService` | Encrypted secret storage (envelope encryption) |
205
+ | Service | Interface | Purpose |
206
+ | ---------------------- | ------------------------------------------- | ---------------------------------------------- |
207
+ | `*ChannelStore` | `ChannelStore` | WebSocket channel state persistence |
208
+ | `*EventHubStore` | `EventHubStore` | Event hub state persistence |
209
+ | `*WorkflowService` | `PikkuWorkflowService` | Workflow definition storage |
210
+ | `*WorkflowRunService` | `WorkflowRunService` | Workflow execution tracking |
211
+ | `*DeploymentService` | `DeploymentService` | Deployment state management |
212
+ | `*AgentStorageService` | `AgentStorageService, AgentRunStateService` | AI conversation/run storage |
213
+ | `*AgentRunService` | `AgentRunService` | Agent execution tracking |
214
+ | `*SecretService` | `SecretService` | Encrypted secret storage (envelope encryption) |
188
215
 
189
216
  A handful more live only on the base package — there is no `Pg`/`MySQL`/`SQLite`
190
217
  variant to reach for, you import them from `@pikku/kysely` whatever the engine:
191
218
 
192
- | Service | Purpose |
193
- | -------------------------- | --------------------------------------------- |
194
- | `KyselySessionStore` | Persisted user sessions |
195
- | `KyselyScopeService` | Scope and role storage |
196
- | `KyselyWebhookService` | Webhook registrations and deliveries |
197
- | `KyselyCredentialService` | Encrypted third-party credentials |
198
- | `KyselyAIRunStateService` | AI run state (also implemented by AIStorage) |
199
- | `KyselyWorkflowMirror` | Mirrors workflow runs into queryable tables |
200
- | `KyselyAuditService` | Durable audit sink (see `pikku-audit`) |
219
+ | Service | Purpose |
220
+ | ---------------------------- | -------------------------------------------- |
221
+ | `KyselySessionStore` | Persisted user sessions |
222
+ | `KyselyScopeService` | Scope and role storage |
223
+ | `KyselyWebhookService` | Webhook registrations and deliveries |
224
+ | `KyselyCredentialService` | Encrypted third-party credentials |
225
+ | `KyselyAgentRunStateService` | AI run state (also implemented by AIStorage) |
226
+ | `KyselyWorkflowMirror` | Mirrors workflow runs into queryable tables |
227
+ | `KyselyAuditService` | Durable audit sink (see `pikku-audit`) |
201
228
 
202
229
  All services take a `Kysely<KyselyPikkuDB>` instance in their constructor and have an `init()` method that creates tables if needed.
203
230
 
@@ -16,10 +16,10 @@ description: >-
16
16
  Unified authentication for humans **and** machines against a Pikku + better-auth
17
17
  server. Two paths, two headers, one resolver:
18
18
 
19
- | Caller | Credential | Header | Obtained by |
20
- |---|---|---|---|
21
- | **Human** (CLI, dev) | better-auth session token | `Authorization: Bearer <token>` | `pikku login` (device flow) → `~/.pikku/session.json` |
22
- | **Machine** (agent, sandbox, worker) | scoped API key | `x-api-key: <key>` | `createApiKey` (server-side, at provision/spawn) |
19
+ | Caller | Credential | Header | Obtained by |
20
+ | ------------------------------------ | ------------------------- | ------------------------------- | ----------------------------------------------------- |
21
+ | **Human** (CLI, dev) | better-auth session token | `Authorization: Bearer <token>` | `pikku login` (device flow) → `~/.pikku/session.json` |
22
+ | **Machine** (agent, sandbox, worker) | scoped API key | `x-api-key: <key>` | `createApiKey` (server-side, at provision/spawn) |
23
23
 
24
24
  Both resolve to a Pikku `UserSession` through one middleware:
25
25
  `betterAuthSession({ mapSession, apiKey: { mapKey } })`.
@@ -83,7 +83,7 @@ import { apiKey } from '@better-auth/api-key'
83
83
  betterAuth({
84
84
  plugins: [
85
85
  apiKey({
86
- enableMetadata: true, // REQUIRED to store scope on the key
86
+ enableMetadata: true, // REQUIRED to store scope on the key
87
87
  enableSessionForAPIKeys: true, // lets a key resolve via getSession too
88
88
  }),
89
89
  ],
@@ -104,10 +104,10 @@ one for a non-existent `userId` is created but will not resolve.
104
104
  // `auth` is the better-auth instance (injected service)
105
105
  const { key } = await auth.api.createApiKey({
106
106
  body: {
107
- userId: sandboxRuntimeUserId, // a stable service user
107
+ userId: sandboxRuntimeUserId, // a stable service user
108
108
  name: `sandbox:${sandboxId}`,
109
- expiresIn: 60 * 60, // seconds
110
- metadata: { sandboxId }, // keep only STABLE ids here
109
+ expiresIn: 60 * 60, // seconds
110
+ metadata: { sandboxId }, // keep only STABLE ids here
111
111
  permissions: { sandbox: ['read', 'write'] },
112
112
  },
113
113
  })
@@ -159,7 +159,7 @@ 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
160
  When it is absent, the human `getSession` path runs as normal. Either way the
161
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
162
+ _live_ session rather than the wire's construction-time snapshot, so it can't
163
163
  clobber one an earlier middleware resolved.
164
164
 
165
165
  ### Restricting a key below its owner
@@ -182,7 +182,7 @@ power — the restriction lives on the key, not on a proliferation of identities
182
182
  ### Failure handling is deliberately split
183
183
 
184
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*
185
+ authenticated" — an unusable credential is not an outage. A failure _inside_
186
186
  `mapKey` (your scope store is down) propagates as a real error instead. That
187
187
  asymmetry is on purpose: a scope lookup that silently failed would serve the
188
188
  request anonymously, which is exactly the wrong direction to fail in.
@@ -6,7 +6,7 @@ description: >-
6
6
  wireMCPPrompt, the MCP wire object and PikkuMCPServer. TRIGGER when: code uses mcp: true or any
7
7
  pikkuMCP*Func/wireMCP* helper, user asks about MCP, Model Context Protocol, AI tool integration,
8
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).
9
+ pikku-agent) or general function definitions (use pikku-concepts).
10
10
  installGroups: [core]
11
11
  ---
12
12
 
@@ -37,11 +37,11 @@ See `pikku-concepts` for the core mental model.
37
37
 
38
38
  MCP has three surfaces, and Pikku wires them differently:
39
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>` |
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
45
 
46
46
  Tools are the odd one out — there is no `wireMCPTool`. Resources and prompts
47
47
  carry protocol metadata (a URI template, a prompt name) that belongs to the
@@ -58,8 +58,8 @@ Add `mcp: true` to any existing function:
58
58
 
59
59
  ```typescript
60
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
61
+ description: 'Create a new todo item', // becomes the MCP tool description
62
+ input: CreateTodoInput, // becomes the MCP tool input schema
63
63
  output: CreateTodoOutput,
64
64
  mcp: true,
65
65
  func: async ({ db }, { text, priority }) => db.createTodo({ text, priority }),
@@ -74,7 +74,7 @@ returns MCP content blocks (`{ type: 'text', text }` or `{ type: 'image', data }
74
74
  with base64), so the assistant reads prose rather than raw JSON:
75
75
 
76
76
  ```typescript
77
- import { pikkuMCPToolFunc } from '#pikku'
77
+ import { pikkuMCPToolFunc } from '#pikku/mcp'
78
78
 
79
79
  export const createTodoTool = pikkuMCPToolFunc({
80
80
  description: 'Create a todo item with title, priority, due date and tags',
@@ -96,7 +96,7 @@ presentation layer over logic that is already tested and reachable over HTTP.
96
96
  ### Resources
97
97
 
98
98
  ```typescript
99
- import { pikkuMCPResourceFunc } from '#pikku'
99
+ import { pikkuMCPResourceFunc } from '#pikku/mcp'
100
100
 
101
101
  export const getTodoResource = pikkuMCPResourceFunc<{ id: string }>(
102
102
  async (_services, { id }, { rpc, mcp }) => {
@@ -117,7 +117,7 @@ it is text only, with no blob variant. `mcp.uri` is the concrete URI the client
117
117
  asked for, which is why each entry echoes it back.
118
118
 
119
119
  ```typescript
120
- import { wireMCPResource } from '#pikku'
120
+ import { wireMCPResource } from '#pikku/mcp'
121
121
 
122
122
  wireMCPResource({
123
123
  uri: 'todos/{id}', // URI template
@@ -136,12 +136,15 @@ than handing the function an `undefined`.
136
136
  ### Prompts
137
137
 
138
138
  ```typescript
139
- import { pikkuMCPPromptFunc, wireMCPPrompt } from '#pikku'
139
+ import { pikkuMCPPromptFunc, wireMCPPrompt } from '#pikku/mcp'
140
140
 
141
141
  export const planDayPrompt = pikkuMCPPromptFunc({
142
142
  input: UserIdInputSchema,
143
143
  func: async (_services, { userId }, { rpc }) => {
144
- const { todos } = await rpc.invoke('listTodos', { userId, completed: false })
144
+ const { todos } = await rpc.invoke('listTodos', {
145
+ userId,
146
+ completed: false,
147
+ })
145
148
  return [
146
149
  {
147
150
  role: 'user' as const,
@@ -242,10 +245,10 @@ anything logs.
242
245
 
243
246
  ## Red flags
244
247
 
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()` |
248
+ | Symptom | Cause |
249
+ | ------------------------------------------------ | --------------------------------------------------------------- |
250
+ | `wireMCPTool` is not exported | There is no tool wiring — use `mcp: true` or `pikkuMCPToolFunc` |
251
+ | `uri`/`title` rejected on `pikkuMCPResourceFunc` | Those belong on `wireMCPResource` |
252
+ | Resource returning `{ uri, blob, mimeType }` | Resources are text only: `{ uri, text }` |
253
+ | Client sees a tool with no description | `mcp: true` without a `description` — check the codegen warning |
254
+ | stdio client disconnects on the first log line | Logger still writing to stdout; use `createMCPLogger()` |
@@ -23,7 +23,7 @@ installGroups: [core]
23
23
  ## The `pikkuMiddleware` Factory
24
24
 
25
25
  ```typescript
26
- import { pikkuMiddleware } from '#pikku'
26
+ import { pikkuMiddleware } from '#pikku/function'
27
27
 
28
28
  // Simple: just a function
29
29
  const myMiddleware = pikkuMiddleware(async (services, wire, next) => {
@@ -48,12 +48,13 @@ const telemetryMiddleware = pikkuMiddleware({
48
48
  ```
49
49
 
50
50
  The `wire` object gives you:
51
+
51
52
  - `wire.http` — inbound HTTP context (headers, URL, cookies)
52
53
  - `wire.setSession(session)` — set the session for this request
53
54
  - `wire.getSession()` — read the current session
54
55
  - `wire.session` — the session set so far (may be undefined)
55
56
 
56
- Throw a typed error to abort: `UnauthorizedError`, `ForbiddenError`, etc. from `@pikku/core/errors`.
57
+ Throw a typed error to abort: `UnauthorizedError`, `ForbiddenError`, etc. from `#pikku/error`.
57
58
 
58
59
  ## Scoping: Five Levels
59
60
 
@@ -70,7 +71,7 @@ addHTTPMiddleware('*', [cors(), authBearer()])
70
71
  addHTTPMiddleware('/admin/*', [auditLog])
71
72
 
72
73
  // 4. Tag-based: any wiring with matching tag
73
- addTagMiddleware('machine-agent', [bearerAuth]) // tag on function or wire
74
+ addTagMiddleware('machine-agent', [bearerAuth]) // tag on function or wire
74
75
 
75
76
  // 5. Inline: per-wiring
76
77
  wireHTTP({
@@ -88,8 +89,8 @@ Runs before everything else, across every wire type: HTTP, Queue, Channel, Trigg
88
89
  import { addGlobalMiddleware } from '@pikku/core'
89
90
  import { telemetryOuter, telemetryInner } from '@pikku/core/middleware'
90
91
 
91
- addGlobalMiddleware([telemetryOuter({ environmentId: env.STAGE_ID })]) // wraps the full call
92
- addGlobalMiddleware([telemetryInner({ environmentId: env.STAGE_ID })]) // closest to the function body
92
+ addGlobalMiddleware([telemetryOuter({ environmentId: env.STAGE_ID })]) // wraps the full call
93
+ addGlobalMiddleware([telemetryInner({ environmentId: env.STAGE_ID })]) // closest to the function body
93
94
  ```
94
95
 
95
96
  `telemetryOuter` ships with `priority: 'highest'`, `telemetryInner` with `priority: 'lowest'` — so priority sorting places outer first regardless of array/call order.
@@ -101,7 +102,9 @@ import { addHTTPMiddleware } from '@pikku/core/http'
101
102
  import { cors, authBearer } from '@pikku/core/middleware'
102
103
 
103
104
  // All routes
104
- addHTTPMiddleware('*', [cors({ origin: 'https://app.example.com', credentials: true })])
105
+ addHTTPMiddleware('*', [
106
+ cors({ origin: 'https://app.example.com', credentials: true }),
107
+ ])
105
108
 
106
109
  // Scoped to /api/* prefix
107
110
  addHTTPMiddleware('/api/*', [rateLimit({ maxRequests: 100, windowMs: 60_000 })])
@@ -136,7 +139,7 @@ Tags from the function definition and the wire object are merged — middleware
136
139
  ### Registering Tag Middleware
137
140
 
138
141
  ```typescript
139
- import { addTagMiddleware } from '#pikku'
142
+ import { addTagMiddleware } from '#pikku/function'
140
143
 
141
144
  addTagMiddleware('machine-agent', [machineAgentBearerAuth])
142
145
  ```
@@ -161,7 +164,7 @@ highest → high → medium (default) → low → lowest
161
164
 
162
165
  **Priority is the primary key across every scope, not within one.** The collected
163
166
  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
167
+ middleware runs _after_ an inline per-route middleware of default priority — the
165
168
  narrower scope does not win. Scope order survives only as the tiebreaker between
166
169
  middleware of equal priority, because the sort is stable.
167
170
 
@@ -190,19 +193,23 @@ A server that exposes RPCs only to a trusted caller (e.g. an API calling a machi
190
193
  ```typescript
191
194
  // lib/host-token.ts
192
195
  let _token: string | null = null
193
- export const setToken = (t: string) => { _token = t }
196
+ export const setToken = (t: string) => {
197
+ _token = t
198
+ }
194
199
  export const getToken = () => _token
195
200
  ```
196
201
 
197
202
  ```typescript
198
203
  // wirings/http.wiring.ts
199
204
  import { timingSafeEqual } from 'node:crypto'
200
- import { addTagMiddleware, pikkuMiddleware } from '#pikku'
201
- import { UnauthorizedError } from '@pikku/core/errors'
205
+ import { addTagMiddleware, pikkuMiddleware } from '#pikku/function'
206
+ import { UnauthorizedError } from '#pikku/error'
202
207
  import { getToken } from '../lib/host-token.js'
203
208
 
204
209
  const bearerAuth = pikkuMiddleware(async (_services, { http }, next) => {
205
- const authHeader = http?.request?.header?.('authorization') || http?.request?.header?.('Authorization')
210
+ const authHeader =
211
+ http?.request?.header?.('authorization') ||
212
+ http?.request?.header?.('Authorization')
206
213
  const token = getToken()
207
214
  const expected = token ? `Bearer ${token}` : null
208
215
  if (
@@ -31,17 +31,23 @@ export function getServiceRPC(baseUrl: string, token: string): RPCInvoke {
31
31
  ## Session-Setting Middleware
32
32
 
33
33
  ```typescript
34
- const apiKeyAuth = pikkuMiddleware(async ({ kysely }, { http, setSession, session }, next) => {
35
- if (session) return next() // already authenticated
34
+ const apiKeyAuth = pikkuMiddleware(
35
+ async ({ kysely }, { http, setSession, session }, next) => {
36
+ if (session) return next() // already authenticated
36
37
 
37
- const header = http?.request?.header?.('x-api-key')
38
- if (!header) return next()
38
+ const header = http?.request?.header?.('x-api-key')
39
+ if (!header) return next()
39
40
 
40
- const row = await kysely.selectFrom('apiKey').select('userId').where('key', '=', header).executeTakeFirst()
41
- if (row) setSession?.({ userId: row.userId })
41
+ const row = await kysely
42
+ .selectFrom('apiKey')
43
+ .select('userId')
44
+ .where('key', '=', header)
45
+ .executeTakeFirst()
46
+ if (row) setSession?.({ userId: row.userId })
42
47
 
43
- return next()
44
- })
48
+ return next()
49
+ }
50
+ )
45
51
 
46
52
  addTagMiddleware('api-key-auth', [apiKeyAuth])
47
53
  ```