@pikku/skills 0.12.34 → 0.12.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/skills.gen.js +1 -1
- package/package.json +1 -1
- package/skills/pikku-addon/SKILL.md +1 -6
- package/skills/pikku-architect/SKILL.md +12 -0
- package/skills/pikku-build/SKILL.md +12 -0
- package/skills/pikku-fabric/SKILL.md +13 -0
- package/skills/pikku-knowledge/SKILL.md +10 -0
- package/skills/pikku-mantine/SKILL.md +80 -0
- package/skills/pikku-wiring/references/http.md +8 -0
- package/skills/pikku-wiring/references/mcp.md +59 -0
package/package.json
CHANGED
|
@@ -47,7 +47,7 @@ wireAddon({
|
|
|
47
47
|
package: string, // NPM package name (e.g. '@pikku/addon-todos')
|
|
48
48
|
rpcEndpoint?: string, // Optional remote RPC endpoint for distributed execution
|
|
49
49
|
auth?: boolean, // Require a session for every function in the addon
|
|
50
|
-
mcp?: boolean,
|
|
50
|
+
mcp?: boolean | string[], // true: every function the addon declared mcp: true; a list: the tools this app offers, typed against the addon's function names
|
|
51
51
|
tags?: string[], // Tags applied to all addon functions
|
|
52
52
|
scopes?: string[], // Required of every function, on top of its own
|
|
53
53
|
secretOverrides?: Record<string, string>, // Remap secret names (and grant them)
|
|
@@ -324,11 +324,6 @@ wireAddon({ name: 'todos', package: '@my-org/addon-todos' })
|
|
|
324
324
|
|
|
325
325
|
After registration, run `yarn pikku all` to generate types for the addon's functions.
|
|
326
326
|
|
|
327
|
-
Give each addon its own wiring file. A deployment unit imports a `wireAddon`
|
|
328
|
-
file only while at least one addon that file wires survives the unit's filter,
|
|
329
|
-
so wiring two addons from one file means a unit needing either one registers
|
|
330
|
-
both and bundles both packages' dependencies.
|
|
331
|
-
|
|
332
327
|
If the addon ships tables, `pikku db generate` then writes one migration per
|
|
333
328
|
addon — named after the package, carrying the addon's own SQL — after Better
|
|
334
329
|
Auth's and the runtime's, so an addon table may reference `user` or a runtime
|
|
@@ -12,6 +12,18 @@ description: >-
|
|
|
12
12
|
the plan already exists and the job is to build it (use pikku-build), or the ask is a one-off
|
|
13
13
|
edit to a working app.
|
|
14
14
|
installGroups: [core]
|
|
15
|
+
agent:
|
|
16
|
+
tools: read, write, edit, bash, grep
|
|
17
|
+
timeoutMs: 1800000
|
|
18
|
+
acceptance:
|
|
19
|
+
level: verified
|
|
20
|
+
evidence: [changed-files, validation-output]
|
|
21
|
+
verify:
|
|
22
|
+
- id: knowledge-consistent
|
|
23
|
+
command: pikku knowledge validate
|
|
24
|
+
- id: plan-accepted
|
|
25
|
+
command: pikku knowledge next --require dispatch,idle
|
|
26
|
+
|
|
15
27
|
---
|
|
16
28
|
|
|
17
29
|
# Plan one milestone
|
|
@@ -13,6 +13,18 @@ description: >-
|
|
|
13
13
|
allowed-tools: Bash(yarn pikku meta *), Bash(yarn pikku all *), Bash(yarn tsc), Bash(git status *), Bash(git diff *), Bash(git switch *), Bash(git checkout *), Bash(git checkout -b *), Bash(git add *), Bash(git commit *), Bash(git rm *), Bash(git mv *), Bash(git log *), Bash(git branch *), Bash(yarn pikku fabric report *), Bash(npx --no pikku fabric report *)
|
|
14
14
|
argument-hint: '[feature description]'
|
|
15
15
|
installGroups: [core]
|
|
16
|
+
agent:
|
|
17
|
+
tools: read, write, edit, bash, grep
|
|
18
|
+
timeoutMs: 5400000
|
|
19
|
+
acceptance:
|
|
20
|
+
level: verified
|
|
21
|
+
evidence: [changed-files, tests-added, commands-run, validation-output]
|
|
22
|
+
verify:
|
|
23
|
+
- id: knowledge-consistent
|
|
24
|
+
command: pikku knowledge validate
|
|
25
|
+
- id: typechecks
|
|
26
|
+
command: pikku all --tsc-summary
|
|
27
|
+
|
|
16
28
|
---
|
|
17
29
|
|
|
18
30
|
# Build on Pikku
|
|
@@ -327,6 +327,19 @@ pikku fabric validate # must pass clean
|
|
|
327
327
|
pikku fabric deploy apply --production -y
|
|
328
328
|
```
|
|
329
329
|
|
|
330
|
+
`init` and `link` import into whichever organization your session is in. When
|
|
331
|
+
you belong to several — a personal one and a company one, say — name the target
|
|
332
|
+
with `--organization`, taking a slug, a display name or an id:
|
|
333
|
+
|
|
334
|
+
```bash
|
|
335
|
+
pikku fabric link --organization vlandor
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
You have to be a member of the organization you name, and its GitHub account
|
|
339
|
+
has to be connected already: importing a `github.com/<owner>/<repo>` repo needs
|
|
340
|
+
the Fabric GitHub App installed on `<owner>` _and_ linked to that organization,
|
|
341
|
+
or the import refuses by name.
|
|
342
|
+
|
|
330
343
|
The branch is positional and defaults to the checked-out one, and `-y` is the
|
|
331
344
|
short form of `--auto-approve`, so a one-shot deploy is:
|
|
332
345
|
|
|
@@ -14,6 +14,16 @@ description: >-
|
|
|
14
14
|
functions, routes, tables or permissions exist (that is `pikku meta` / `pikku info`, never a
|
|
15
15
|
note), or to write a scenario test (use pikku-scenario).
|
|
16
16
|
installGroups: [core]
|
|
17
|
+
agent:
|
|
18
|
+
tools: read, write, edit, bash, grep
|
|
19
|
+
timeoutMs: 1800000
|
|
20
|
+
acceptance:
|
|
21
|
+
level: verified
|
|
22
|
+
evidence: [changed-files, validation-output]
|
|
23
|
+
verify:
|
|
24
|
+
- id: knowledge-consistent
|
|
25
|
+
command: pikku knowledge validate
|
|
26
|
+
|
|
17
27
|
---
|
|
18
28
|
|
|
19
29
|
# Pikku Knowledge
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pikku-mantine
|
|
3
|
+
description: >-
|
|
4
|
+
Use when building a Mantine UI on top of a Pikku backend — rendering dates that came back from a
|
|
5
|
+
generated client, keeping layout flow-relative so the app survives an RTL locale, and branching on
|
|
6
|
+
colour scheme without hardcoding a shade. TRIGGER when: putting a value from usePikkuQuery or an
|
|
7
|
+
RPC response on screen, writing margins/padding/alignment in Mantine props or CSS, choosing a date
|
|
8
|
+
input, or handling light/dark. DO NOT TRIGGER when: the data does not come from a Pikku client
|
|
9
|
+
(this is only about what the generated clients hand you), or for user-facing copy (use pikku-i18n).
|
|
10
|
+
installGroups: [client]
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Mantine on a Pikku client
|
|
14
|
+
|
|
15
|
+
## Dates — always format before rendering
|
|
16
|
+
|
|
17
|
+
The generated clients run `transformDates`, which revives **fully-zoned ISO-8601 instants** —
|
|
18
|
+
`2026-03-14T08:12:00Z`, `2026-03-14T08:12:00.000+01:00` — into `Date` objects and touches nothing
|
|
19
|
+
else. A bare `2026-03-14`, a zoneless `2026-03-14T08:12:00` and an impossible `2026-02-31T00:00:00Z`
|
|
20
|
+
all stay the strings the server sent. So a field's runtime type follows the VALUE, not the schema:
|
|
21
|
+
one column can arrive as a `Date` from one row and a string from the next.
|
|
22
|
+
|
|
23
|
+
Two consequences, and both compile:
|
|
24
|
+
|
|
25
|
+
- **A string method on one white-screens the page.** `row.createdAt.split('T')[0]` type-checks
|
|
26
|
+
against nothing useful and blows up at runtime. There is no string to slice.
|
|
27
|
+
- **A raw `Date` dropped into JSX crashes the route.** `<Text>{row.createdAt}</Text>`, a table cell,
|
|
28
|
+
a `<Badge>` — React throws `Objects are not valid as a React child (found: [object Date])` and the
|
|
29
|
+
page falls into its error boundary. Nothing catches it before the screen is white, which makes it
|
|
30
|
+
the most common broken page in a build.
|
|
31
|
+
|
|
32
|
+
**Format with dayjs.** It is Mantine's own date library, already shipped alongside `@mantine/dates`,
|
|
33
|
+
and it takes either a `Date` or a string. Never `toLocaleDateString`, `date-fns` or `luxon`.
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
import dayjs from 'dayjs'
|
|
37
|
+
|
|
38
|
+
<Text>{dayjs(row.dueOn).format('D MMM YYYY')}</Text> // 15 Jun 2026
|
|
39
|
+
<Text>{dayjs(row.createdAt).format('D MMM YYYY, HH:mm')}</Text>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
A relative "2 days ago" via dayjs `relativeTime` is fine. Coercing instead of formatting
|
|
43
|
+
(`` `${d}` ``, `String(d)`, `d + ''`) does not crash but prints
|
|
44
|
+
`Mon Jun 15 2026 02:00:00 GMT+0200` — a different bug, equally wrong.
|
|
45
|
+
|
|
46
|
+
Date **inputs** are `@mantine/dates` — `DatePickerInput`, `DatePicker`, `Calendar` — never a raw
|
|
47
|
+
`<TextInput type="date">`. Those are pickers and they are not a schedule: Mantine ships no
|
|
48
|
+
week/time-grid component, so a diary, rota, timetable or booking week is a grid you build, not a
|
|
49
|
+
`Calendar` with the time-of-day left out. `Calendar` with `renderDay` IS right for "a few things on
|
|
50
|
+
each day of a month", like a content calendar or a holiday planner.
|
|
51
|
+
|
|
52
|
+
All of this applies to stub and fixture dates exactly as it does to real data.
|
|
53
|
+
|
|
54
|
+
## RTL-safe styles
|
|
55
|
+
|
|
56
|
+
Write layout styles flow-relative so the UI works in both LTR and RTL languages. Pikku's i18n ships
|
|
57
|
+
Arabic, Hebrew, Farsi and Urdu support, and a physical margin is what breaks under it.
|
|
58
|
+
|
|
59
|
+
| Avoid | Use instead |
|
|
60
|
+
| ----------------------------- | ------------------------------------------ |
|
|
61
|
+
| `ml`, `mr`, `pl`, `pr` | `ms`, `me`, `ps`, `pe` |
|
|
62
|
+
| `text-align: left/right` | `text-align: start/end` |
|
|
63
|
+
| `margin-left`, `margin-right` | `margin-inline-start`, `margin-inline-end` |
|
|
64
|
+
| `flex-direction: row-reverse` | `dir` attribute or logical properties |
|
|
65
|
+
|
|
66
|
+
Mantine shorthand: `ms` = margin-inline-start, `me` = margin-inline-end, `ps` = padding-inline-start,
|
|
67
|
+
`pe` = padding-inline-end.
|
|
68
|
+
|
|
69
|
+
## Dark mode
|
|
70
|
+
|
|
71
|
+
Use Mantine's `light-dark()` utility or `useMantineColorScheme`, and only with colours that already
|
|
72
|
+
come from the theme — never introduce a literal colour or a shade string for one scheme.
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
// Correct
|
|
76
|
+
<Box bg="var(--mantine-color-body)">
|
|
77
|
+
|
|
78
|
+
// Wrong — scheme branch with hardcoded Mantine shades
|
|
79
|
+
<Box bg={theme.colorScheme === 'dark' ? 'dark.6' : 'gray.0'}>
|
|
80
|
+
```
|
|
@@ -147,6 +147,14 @@ it is optional because the same function can be reached over plain HTTP or RPC,
|
|
|
147
147
|
where there is no stream to send on. The `if (channel)` guard is what lets one
|
|
148
148
|
function serve both; the return value is the non-streaming answer.
|
|
149
149
|
|
|
150
|
+
A function that throws once the stream is open cannot answer with a status code,
|
|
151
|
+
so the runner ends the stream with `{ type: 'error', errorText }` then
|
|
152
|
+
`{ type: 'done' }`. A route whose client parses a different event protocol says
|
|
153
|
+
so with `streamProtocol`, and the failure is written in that one instead —
|
|
154
|
+
`streamProtocol: 'agui'` ends the stream with a single AG-UI `RUN_ERROR` and
|
|
155
|
+
nothing after it. The default is `'pikku'`; the generated agent stream routes
|
|
156
|
+
set `'agui'`.
|
|
157
|
+
|
|
150
158
|
### Generated Fetch Client
|
|
151
159
|
|
|
152
160
|
After `npx pikku all`, a type-safe client is generated:
|
|
@@ -186,6 +186,63 @@ When you add a tool, tell whoever asked for it the URL. An assistant that cannot
|
|
|
186
186
|
be pointed at an endpoint has not been connected to anything, and `/mcp` is the
|
|
187
187
|
whole answer.
|
|
188
188
|
|
|
189
|
+
## Authentication
|
|
190
|
+
|
|
191
|
+
An MCP endpoint is not gated as a whole. Pikku already knows, tool by tool, which
|
|
192
|
+
calls need a session, and the endpoint answers accordingly:
|
|
193
|
+
|
|
194
|
+
| Declaration | Anonymous call |
|
|
195
|
+
| ---------------------------------------- | ------------------- |
|
|
196
|
+
| `pikkuSessionlessFunc` with `mcp: true` | runs |
|
|
197
|
+
| the same, plus `auth: true` | `401` + a challenge |
|
|
198
|
+
| `pikkuFunc` with `mcp: true` | `401` + a challenge |
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
// public: anyone connecting to /mcp can call this
|
|
202
|
+
export const searchCatalog = pikkuSessionlessFunc<Query, Results>({
|
|
203
|
+
mcp: true,
|
|
204
|
+
func: async (services, data) => services.catalog.search(data),
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
// private: an anonymous caller is challenged, never dispatched
|
|
208
|
+
export const myOrders = pikkuFunc<void, Order[]>({
|
|
209
|
+
mcp: true,
|
|
210
|
+
func: async (services, _data, session) =>
|
|
211
|
+
services.orders.forUser(session.userId),
|
|
212
|
+
})
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
The `401` carries a `WWW-Authenticate` header naming the endpoint's RFC 9728
|
|
216
|
+
Protected Resource Metadata document, which the server also serves — `/mcp` is
|
|
217
|
+
described at `/.well-known/oauth-protected-resource/mcp`. That pair is what an
|
|
218
|
+
MCP client needs to discover an authorization server and start an OAuth flow;
|
|
219
|
+
a refusal delivered as a JSON-RPC result instead reads to a client as a tool that
|
|
220
|
+
failed, and no discovery happens.
|
|
221
|
+
|
|
222
|
+
`tools/list` is never gated, so a client can still see what exists before it has
|
|
223
|
+
a token.
|
|
224
|
+
|
|
225
|
+
Nothing needs configuring: the metadata document defaults to advertising the
|
|
226
|
+
origin the request arrived on, which is right whenever the app is its own
|
|
227
|
+
authorization server. To point elsewhere, pass `mcpAuth` to the runtime:
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
new PikkuNodeHTTPServer(config, logger, {
|
|
231
|
+
mcpJson,
|
|
232
|
+
mcpAuth: {
|
|
233
|
+
authorizationServers: ['https://auth.example.com'],
|
|
234
|
+
scopesSupported: ['mcp'],
|
|
235
|
+
resourceName: 'Example API',
|
|
236
|
+
},
|
|
237
|
+
})
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
The transport never verifies a token itself — session resolution stays with the
|
|
241
|
+
app's own middleware, exactly as it works over HTTP. One consequence: only a
|
|
242
|
+
request carrying *no* credentials is challenged. A token that is present but
|
|
243
|
+
expired is dispatched, and the runner's refusal reaches the client as a tool
|
|
244
|
+
error.
|
|
245
|
+
|
|
189
246
|
## Red flags
|
|
190
247
|
|
|
191
248
|
| Symptom | Cause |
|
|
@@ -195,3 +252,5 @@ whole answer.
|
|
|
195
252
|
| Resource returning `{ uri, blob, mimeType }` | Resources are text only: `{ uri, text }` |
|
|
196
253
|
| Client sees a tool with no description | `mcp: true` without a `description` — check the codegen warning |
|
|
197
254
|
| `/mcp` 404s | Nothing to serve yet — the mount is skipped until one tool, resource or prompt exists |
|
|
255
|
+
| A tool an assistant should be able to call returns `401` | It is a `pikkuFunc`, or declares `auth: true` — make it a `pikkuSessionlessFunc` if it is genuinely public |
|
|
256
|
+
| A private tool returns a result rather than a challenge | The request carried a credential, so it was dispatched; only a call with none is refused at the door |
|