@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.
- package/CHANGELOG.md +768 -0
- package/dist/skills.gen.js +1 -1
- package/package.json +4 -4
- package/skills/pikku-addon/SKILL.md +20 -14
- package/skills/pikku-addon/references/addon-package-manifest.md +2 -2
- package/skills/{pikku-ai-agent → pikku-agent}/SKILL.md +24 -24
- package/skills/pikku-ai-vercel/SKILL.md +18 -18
- package/skills/pikku-ai-voice/SKILL.md +15 -15
- package/skills/pikku-audit/SKILL.md +28 -13
- package/skills/pikku-aws/SKILL.md +2 -2
- package/skills/pikku-better-auth/SKILL.md +97 -17
- package/skills/pikku-build-app/SKILL.md +621 -0
- package/skills/pikku-build-app/references/multi-app.md +117 -0
- package/skills/pikku-build-app/references/ship.md +98 -0
- package/skills/pikku-build-app/references/theming.md +70 -0
- package/skills/pikku-build-platform/SKILL.md +239 -0
- package/skills/pikku-build-quick/SKILL.md +238 -0
- package/skills/pikku-cli/SKILL.md +7 -7
- package/skills/pikku-cli/references/complete-example.md +1 -1
- package/skills/pikku-concepts/SKILL.md +10 -7
- package/skills/pikku-concepts/references/concept-mapping.md +1 -1
- package/skills/pikku-config/SKILL.md +5 -3
- package/skills/pikku-deploy-azure/SKILL.md +5 -4
- package/skills/pikku-deploy-cloudflare/SKILL.md +9 -9
- package/skills/pikku-deploy-uws/SKILL.md +5 -2
- package/skills/pikku-deps/SKILL.md +42 -3
- package/skills/pikku-emails/SKILL.md +5 -5
- package/skills/pikku-fabric/SKILL.md +27 -3
- package/skills/pikku-fabric-debug/SKILL.md +1 -1
- package/skills/pikku-feature/SKILL.md +5 -4
- package/skills/pikku-http/SKILL.md +4 -4
- package/skills/pikku-http/references/http-options.md +13 -13
- package/skills/pikku-i18n/SKILL.md +2 -1
- package/skills/pikku-info/SKILL.md +1 -1
- package/skills/pikku-knowledge/SKILL.md +13 -13
- package/skills/pikku-kysely/SKILL.md +68 -41
- package/skills/pikku-machine-auth/SKILL.md +10 -10
- package/skills/pikku-mcp/SKILL.md +23 -20
- package/skills/pikku-middleware/SKILL.md +19 -12
- package/skills/pikku-middleware/references/middleware-patterns.md +14 -8
- package/skills/pikku-mongodb/SKILL.md +11 -11
- package/skills/pikku-n8n-import/SKILL.md +12 -12
- package/skills/pikku-n8n-import/SPEC.md +3 -0
- package/skills/pikku-n8n-import/references/addon-mapping.md +14 -8
- package/skills/pikku-n8n-import/references/code-translation.md +26 -22
- package/skills/pikku-n8n-import/references/loops-and-control.md +7 -7
- package/skills/pikku-paraglide/SKILL.md +11 -6
- package/skills/pikku-permissions/SKILL.md +19 -15
- package/skills/pikku-product-second-opinion/README.md +3 -3
- package/skills/pikku-product-second-opinion/SKILL.md +83 -73
- package/skills/pikku-product-second-opinion/example/sample-report.md +15 -12
- package/skills/pikku-product-second-opinion/references/report-template.md +10 -7
- package/skills/pikku-queue/SKILL.md +1 -1
- package/skills/pikku-react/SKILL.md +53 -13
- package/skills/pikku-realtime/SKILL.md +51 -19
- package/skills/pikku-rpc/SKILL.md +1 -1
- package/skills/pikku-rtl/SKILL.md +1 -1
- package/skills/pikku-scenario/SKILL.md +164 -44
- package/skills/pikku-schedule/SKILL.md +6 -1
- package/skills/pikku-schema-ajv/SKILL.md +2 -2
- package/skills/pikku-schema-cfworker/SKILL.md +1 -1
- package/skills/pikku-security/SKILL.md +9 -5
- package/skills/pikku-services/SKILL.md +27 -18
- package/skills/pikku-services/references/audit-wire-service.md +14 -8
- package/skills/pikku-software-archaeology/SKILL.md +27 -23
- package/skills/pikku-software-archaeology/references/blueprint.schema.json +580 -102
- package/skills/pikku-software-archaeology/scripts/validate.mjs +146 -69
- package/skills/pikku-tag-middleware/SKILL.md +1 -0
- package/skills/pikku-template-clone/SKILL.md +2 -1
- package/skills/pikku-trigger/SKILL.md +3 -3
- package/skills/pikku-versioning/SKILL.md +87 -3
- package/skills/pikku-websocket/SKILL.md +4 -3
- package/skills/pikku-workflow/SKILL.md +2 -2
- package/skills/pikku-workflow/references/workflow-reference.md +24 -10
- 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/
|
|
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
|
|
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
|
|
8
|
-
|
|
|
9
|
-
| `method`
|
|
10
|
-
| `route`
|
|
11
|
-
| `func`
|
|
12
|
-
| `auth?`
|
|
13
|
-
| `tags?`
|
|
14
|
-
| `middleware?`
|
|
15
|
-
| `sse?`
|
|
16
|
-
| `query?`
|
|
17
|
-
| `contentType?` | `'xml' \| 'json'`
|
|
18
|
-
| `timeout?`
|
|
19
|
-
| `headers?`
|
|
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
|
|
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
|
|
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
|
|
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'
|
|
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
|
|
43
|
+
const rows = await kysely
|
|
44
|
+
.selectFrom('item')
|
|
44
45
|
.select(['id', 'name', 'quantity'])
|
|
45
46
|
.where('warehouseId', '=', warehouseId)
|
|
46
|
-
.orderBy('name')
|
|
47
|
+
.orderBy('name')
|
|
48
|
+
.limit(50)
|
|
49
|
+
.execute()
|
|
47
50
|
|
|
48
51
|
// JOINS + aliased selects (qualify columns once a join exists)
|
|
49
|
-
await kysely
|
|
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
|
|
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)
|
|
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
|
|
69
|
+
const created = await kysely
|
|
70
|
+
.insertInto('item')
|
|
65
71
|
.values({ name: input.name, warehouseId })
|
|
66
|
-
.returning(['id', 'name'])
|
|
72
|
+
.returning(['id', 'name'])
|
|
73
|
+
.executeTakeFirstOrThrow()
|
|
67
74
|
|
|
68
75
|
// UPDATE + RETURNING, DELETE
|
|
69
|
-
await kysely
|
|
70
|
-
.
|
|
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
|
|
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'))
|
|
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
|
|
83
|
-
.
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
91
|
-
|
|
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',
|
|
181
|
+
filename: 'app.db', // or ':memory:'
|
|
155
182
|
camelCase: true,
|
|
156
|
-
plugins: [],
|
|
157
|
-
functions: {},
|
|
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
|
|
179
|
-
|
|
|
180
|
-
| `*ChannelStore`
|
|
181
|
-
| `*EventHubStore`
|
|
182
|
-
| `*WorkflowService`
|
|
183
|
-
| `*WorkflowRunService`
|
|
184
|
-
| `*DeploymentService`
|
|
185
|
-
| `*
|
|
186
|
-
| `*AgentRunService`
|
|
187
|
-
| `*SecretService`
|
|
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
|
|
193
|
-
|
|
|
194
|
-
| `KyselySessionStore`
|
|
195
|
-
| `KyselyScopeService`
|
|
196
|
-
| `KyselyWebhookService`
|
|
197
|
-
| `KyselyCredentialService`
|
|
198
|
-
| `
|
|
199
|
-
| `KyselyWorkflowMirror`
|
|
200
|
-
| `KyselyAuditService`
|
|
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
|
|
20
|
-
|
|
21
|
-
| **Human** (CLI, dev)
|
|
22
|
-
| **Machine** (agent, sandbox, worker) | scoped API key
|
|
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,
|
|
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,
|
|
107
|
+
userId: sandboxRuntimeUserId, // a stable service user
|
|
108
108
|
name: `sandbox:${sandboxId}`,
|
|
109
|
-
expiresIn: 60 * 60,
|
|
110
|
-
metadata: { sandboxId },
|
|
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
|
-
|
|
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
|
|
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-
|
|
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
|
|
41
|
-
|
|
|
42
|
-
| **Tool**
|
|
43
|
-
| **Resource** | `pikkuMCPResourceFunc`
|
|
44
|
-
| **Prompt**
|
|
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',
|
|
62
|
-
input: CreateTodoInput,
|
|
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', {
|
|
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
|
|
246
|
-
|
|
|
247
|
-
| `wireMCPTool` is not exported
|
|
248
|
-
| `uri`/`title` rejected on `pikkuMCPResourceFunc` | Those belong on `wireMCPResource`
|
|
249
|
-
| Resource returning `{ uri, blob, mimeType }`
|
|
250
|
-
| Client sees a tool with no description
|
|
251
|
-
| stdio client disconnects on the first log line
|
|
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
|
|
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])
|
|
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 })])
|
|
92
|
-
addGlobalMiddleware([telemetryInner({ environmentId: env.STAGE_ID })])
|
|
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('*', [
|
|
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
|
|
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) => {
|
|
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 '
|
|
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 =
|
|
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(
|
|
35
|
-
|
|
34
|
+
const apiKeyAuth = pikkuMiddleware(
|
|
35
|
+
async ({ kysely }, { http, setSession, session }, next) => {
|
|
36
|
+
if (session) return next() // already authenticated
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
const header = http?.request?.header?.('x-api-key')
|
|
39
|
+
if (!header) return next()
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
44
|
-
}
|
|
48
|
+
return next()
|
|
49
|
+
}
|
|
50
|
+
)
|
|
45
51
|
|
|
46
52
|
addTagMiddleware('api-key-auth', [apiKeyAuth])
|
|
47
53
|
```
|