@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/skills",
3
- "version": "0.12.34",
3
+ "version": "0.12.35",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/pikkujs/pikku.git",
@@ -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 |