@pikku/skills 0.12.22 → 0.12.25

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 (101) hide show
  1. package/CHANGELOG.md +101 -0
  2. package/dist/skills.gen.js +1 -1
  3. package/package.json +1 -1
  4. package/skills/pikku-addon/SKILL.md +2 -2
  5. package/skills/pikku-agent/SKILL.md +67 -316
  6. package/skills/pikku-agent/references/agents.md +299 -0
  7. package/skills/{pikku-ai-vercel/SKILL.md → pikku-agent/references/runner-vercel.md} +2 -24
  8. package/skills/{pikku-ai-voice/SKILL.md → pikku-agent/references/voice.md} +1 -22
  9. package/skills/pikku-architect/SKILL.md +264 -0
  10. package/skills/pikku-auth/SKILL.md +89 -0
  11. package/skills/{pikku-better-auth/SKILL.md → pikku-auth/references/better-auth.md} +1 -27
  12. package/skills/{pikku-jose/SKILL.md → pikku-auth/references/jose.md} +1 -23
  13. package/skills/{pikku-machine-auth/SKILL.md → pikku-auth/references/machine-auth.md} +0 -23
  14. package/skills/{pikku-permissions/SKILL.md → pikku-auth/references/permissions.md} +1 -20
  15. package/skills/{pikku-security/SKILL.md → pikku-auth/references/sessions.md} +0 -20
  16. package/skills/pikku-build/SKILL.md +87 -0
  17. package/skills/{pikku-build-app/SKILL.md → pikku-build/references/app.md} +75 -23
  18. package/skills/{pikku-feature/SKILL.md → pikku-build/references/feature.md} +1 -8
  19. package/skills/{pikku-build-app → pikku-build}/references/multi-app.md +1 -1
  20. package/skills/{pikku-build-platform/SKILL.md → pikku-build/references/platform.md} +22 -37
  21. package/skills/{pikku-template-clone/SKILL.md → pikku-build/references/post-clone.md} +0 -6
  22. package/skills/{pikku-build-quick/SKILL.md → pikku-build/references/quick.md} +4 -20
  23. package/skills/{pikku-build-app → pikku-build}/references/ship.md +7 -1
  24. package/skills/pikku-concepts/SKILL.md +72 -7
  25. package/skills/pikku-concepts/references/concept-mapping.md +8 -8
  26. package/skills/pikku-deploy/SKILL.md +158 -0
  27. package/skills/{pikku-deploy-azure/SKILL.md → pikku-deploy/references/azure.md} +18 -50
  28. package/skills/pikku-deploy/references/cloudflare.md +104 -0
  29. package/skills/pikku-deploy/references/express.md +92 -0
  30. package/skills/{pikku-deploy-fastify/SKILL.md → pikku-deploy/references/fastify.md} +8 -32
  31. package/skills/{pikku-deploy-lambda/SKILL.md → pikku-deploy/references/lambda.md} +6 -27
  32. package/skills/{pikku-deploy-nextjs/SKILL.md → pikku-deploy/references/nextjs.md} +9 -33
  33. package/skills/pikku-deploy/references/uws.md +72 -0
  34. package/skills/pikku-deploy/references/ws.md +75 -0
  35. package/skills/pikku-emails/SKILL.md +3 -2
  36. package/skills/pikku-fabric/SKILL.md +12 -2
  37. package/skills/{pikku-fabric-debug/SKILL.md → pikku-fabric/references/debugging.md} +0 -6
  38. package/skills/pikku-i18n/SKILL.md +60 -207
  39. package/skills/{pikku-paraglide/SKILL.md → pikku-i18n/references/enum-labels.md} +0 -6
  40. package/skills/pikku-i18n/references/messages.md +218 -0
  41. package/skills/{pikku-rtl/SKILL.md → pikku-i18n/references/rtl.md} +3 -9
  42. package/skills/pikku-knowledge/SKILL.md +14 -0
  43. package/skills/pikku-kysely/SKILL.md +13 -13
  44. package/skills/pikku-meta/SKILL.md +58 -130
  45. package/skills/{pikku-deps/SKILL.md → pikku-meta/references/audit.md} +1 -17
  46. package/skills/pikku-meta/references/meta.md +114 -0
  47. package/skills/{pikku-versioning/SKILL.md → pikku-meta/references/versioning.md} +0 -26
  48. package/skills/pikku-middleware/SKILL.md +5 -5
  49. package/skills/pikku-n8n-import/SKILL.md +0 -1
  50. package/skills/pikku-react/SKILL.md +50 -298
  51. package/skills/pikku-react/references/client.md +293 -0
  52. package/skills/{pikku-react-query/SKILL.md → pikku-react/references/react-query.md} +2 -22
  53. package/skills/{pikku-workflows-client/SKILL.md → pikku-react/references/workflows.md} +1 -22
  54. package/skills/pikku-scenario/SKILL.md +60 -45
  55. package/skills/pikku-scenario/references/persona-run.md +148 -0
  56. package/skills/pikku-service-backends/SKILL.md +154 -0
  57. package/skills/pikku-service-backends/references/aws.md +106 -0
  58. package/skills/pikku-service-backends/references/backblaze.md +57 -0
  59. package/skills/pikku-service-backends/references/mongodb.md +90 -0
  60. package/skills/pikku-service-backends/references/redis.md +75 -0
  61. package/skills/pikku-service-backends/references/schema.md +63 -0
  62. package/skills/pikku-services/SKILL.md +68 -291
  63. package/skills/{pikku-audit/SKILL.md → pikku-services/references/audit.md} +0 -22
  64. package/skills/{pikku-config/SKILL.md → pikku-services/references/config.md} +1 -25
  65. package/skills/{pikku-pino/SKILL.md → pikku-services/references/pino.md} +0 -20
  66. package/skills/pikku-services/references/services.md +272 -0
  67. package/skills/pikku-software-archaeology/README.md +5 -1
  68. package/skills/pikku-software-archaeology/SKILL.md +15 -2
  69. package/skills/{pikku-product-second-opinion/example/sample-report.md → pikku-software-archaeology/example/second-opinion-sample-report.md} +1 -1
  70. package/skills/pikku-software-archaeology/references/blueprint.schema.json +1 -1
  71. package/skills/pikku-software-archaeology/references/pikku-mapping.md +3 -3
  72. package/skills/{pikku-product-second-opinion/SKILL.md → pikku-software-archaeology/references/second-opinion.md} +4 -9
  73. package/skills/pikku-webhook/SKILL.md +199 -0
  74. package/skills/pikku-wiring/SKILL.md +180 -0
  75. package/skills/{pikku-websocket/SKILL.md → pikku-wiring/references/channel.md} +2 -35
  76. package/skills/{pikku-cli/SKILL.md → pikku-wiring/references/cli.md} +1 -33
  77. package/skills/{pikku-gateway-slack/SKILL.md → pikku-wiring/references/gateway-slack.md} +0 -23
  78. package/skills/{pikku-http/SKILL.md → pikku-wiring/references/http.md} +3 -39
  79. package/skills/{pikku-mcp/SKILL.md → pikku-wiring/references/mcp.md} +0 -33
  80. package/skills/{pikku-queue/SKILL.md → pikku-wiring/references/queue.md} +1 -33
  81. package/skills/{pikku-realtime/SKILL.md → pikku-wiring/references/realtime.md} +2 -25
  82. package/skills/{pikku-rpc/SKILL.md → pikku-wiring/references/rpc.md} +0 -32
  83. package/skills/{pikku-schedule/SKILL.md → pikku-wiring/references/scheduler.md} +1 -35
  84. package/skills/{pikku-trigger/SKILL.md → pikku-wiring/references/trigger.md} +0 -43
  85. package/skills/pikku-workflow/SKILL.md +2 -2
  86. package/skills/pikku-aws/SKILL.md +0 -161
  87. package/skills/pikku-backblaze/SKILL.md +0 -104
  88. package/skills/pikku-deploy-cloudflare/SKILL.md +0 -123
  89. package/skills/pikku-deploy-express/SKILL.md +0 -122
  90. package/skills/pikku-deploy-uws/SKILL.md +0 -144
  91. package/skills/pikku-mongodb/SKILL.md +0 -113
  92. package/skills/pikku-product-second-opinion/README.md +0 -43
  93. package/skills/pikku-redis/SKILL.md +0 -99
  94. package/skills/pikku-schema-ajv/SKILL.md +0 -83
  95. package/skills/pikku-schema-cfworker/SKILL.md +0 -82
  96. package/skills/pikku-ws/SKILL.md +0 -87
  97. /package/skills/{pikku-build-app → pikku-build}/references/theming.md +0 -0
  98. /package/skills/{pikku-product-second-opinion/references/report-template.md → pikku-software-archaeology/references/second-opinion-report-template.md} +0 -0
  99. /package/skills/{pikku-cli/references/complete-example.md → pikku-wiring/references/cli-complete-example.md} +0 -0
  100. /package/skills/{pikku-http → pikku-wiring}/references/http-options.md +0 -0
  101. /package/skills/{pikku-realtime/references/other-routes.md → pikku-wiring/references/realtime-other-routes.md} +0 -0
@@ -1,313 +1,65 @@
1
1
  ---
2
2
  name: pikku-react
3
- description: 'Set up @pikku/react in a React app: PikkuProvider context, createPikku factory, and the usePikkuRPC / usePikkuFetch hooks for direct (non-React-Query) calls. TRIGGER when: the user is bootstrapping a React frontend that talks to a Pikku backend, asks how to wire `PikkuProvider`, or needs to make one-off RPC calls outside of useQuery/useMutation. TRIGGER when: user asks about the dev actor switcher, "sign in as" / quick-login UI, useDevActors, VITE_DEV_ACTORS, or the app-missing-actor-quick-login validate finding. DO NOT TRIGGER when: the user is asking about useQuery/useMutation hooks (use pikku-react-query) or about workflows (use pikku-workflows-client).'
3
+ description: >-
4
+ Use when a React frontend talks to a Pikku backend — PikkuProvider and createPikku at the app
5
+ root, the generated React Query hooks (usePikkuQuery, usePikkuMutation, usePikkuInfiniteQuery),
6
+ direct usePikkuRPC / usePikkuFetch calls, realtime subscriptions, agent and workflow hooks, and
7
+ the dev actor switcher. TRIGGER when: writing a React component that fetches or mutates backend
8
+ data, wiring PikkuProvider, paginating, running or tracking a workflow from the client, or
9
+ asking about useDevActors / VITE_DEV_ACTORS / quick login. DO NOT TRIGGER when: working on the
10
+ backend (use pikku-wiring), defining the workflow itself (use pikku-workflow), or writing
11
+ user-facing copy (use pikku-i18n).
4
12
  installGroups: [client]
5
13
  ---
6
14
 
7
15
  # Pikku React
8
16
 
9
- ## Agent Operating Procedure
17
+ The hook names and their argument types come from your generated `api.gen.ts` —
18
+ read it for what this app actually exposes. This skill is the part it cannot
19
+ tell you: which hook a given need calls for, and where the generated client
20
+ stops.
10
21
 
11
- Use this skill as an execution checklist, not reference material.
22
+ ## Pick the reference
12
23
 
13
- 1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.
14
- 2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.
15
- 3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.
16
- 4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.
17
- 5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.
24
+ | You are… | Read |
25
+ | --- | --- |
26
+ | Wiring the app root, resolving the server URL, authenticating, or subscribing to realtime | `references/client.md` |
27
+ | Fetching, mutating or paginating data | `references/react-query.md` |
28
+ | Starting a workflow and showing its progress | `references/workflows.md` |
18
29
 
19
- `@pikku/react` is the smallest possible binding: a Context provider plus
20
- two hooks. It does **not** depend on React Query — that's a separate
21
- opt-in via the generated `api.gen.ts`. Use this skill when setting up the
22
- provider or making direct RPC calls.
30
+ ## Reach for what
23
31
 
24
- ## What ships
32
+ | Need | Use |
33
+ | --- | --- |
34
+ | Render data, dedupe and cache | `usePikkuQuery` |
35
+ | Trigger a write and wait for the result | `usePikkuMutation` |
36
+ | Paginate | `usePikkuInfiniteQuery` |
37
+ | One-off call from an event handler | `usePikkuRPC()` |
38
+ | Hit a REST endpoint rather than an RPC | `usePikkuFetch()` |
39
+ | Talk to one named AI agent | `usePikkuAgent(name)` → `.run` / `.stream` / `.approve` |
40
+ | Run one named workflow | `usePikkuWorkflow(name)` → `.start` / `.run` / `.status` |
41
+ | A workflow long enough to need progress UI | `references/workflows.md` |
42
+ | Subscribe to events, SSE or a channel | `usePikkuRealtime()` |
25
43
 
26
- ```tsx
27
- import {
28
- PikkuProvider,
29
- createPikku,
30
- usePikkuFetch,
31
- usePikkuRPC,
32
- usePikkuRealtime,
33
- usePikkuAgent,
34
- usePikkuWorkflow,
35
- asI18n,
36
- } from '@pikku/react'
37
- ```
38
-
39
- `usePikkuRealtime` is only valid when you wired a `PikkuRealtime` class via
40
- `createPikku` — see the setup section. `usePikkuAgent` and `usePikkuWorkflow`
41
- are thin bindings over the RPC client that pin one agent/workflow name, so a
42
- component never repeats it. `asI18n` is the i18n brand (see **pikku-i18n**).
43
-
44
- ## Resolving the server URL
45
-
46
- Every client (`createPikku`, realtime, the auth client) resolves its base
47
- through one shared helper in `src/lib/env.ts`. Write this once:
48
-
49
- ```ts
50
- // Endpoints come from env, never hardcoded.
51
- export function apiUrl(): string {
52
- // SSR: the client hooks only run in the browser, so a placeholder is fine.
53
- if (import.meta.env.SSR) {
54
- return import.meta.env.VITE_API_URL ?? '/__api'
55
- }
56
- return import.meta.env.VITE_API_URL ?? `${window.location.origin}/api`
57
- }
58
- ```
59
-
60
- **Never fall back to `http://localhost:3000`.** `import.meta.env.VITE_API_URL`
61
- is substituted by Vite at _build_ time, so any deploy that supplies the URL as
62
- a _runtime_ env var or platform binding leaves it `undefined` in the shipped
63
- bundle — the fallback is then the only branch that ever runs in the browser. A
64
- localhost fallback means every request from a deployed app goes to the user's
65
- own machine. `origin + '/api'` is same-origin, needs no build-time knowledge of
66
- the domain, and is correct wherever the app is served from.
67
-
68
- For local dev, set `VITE_API_URL`, or proxy `/api` → your backend in
69
- `vite.config.ts` under `server.proxy`. One `/api` entry also covers
70
- `/api/auth/*`; only add more entries for root-level routes outside `/api`.
71
-
72
- ## Setup at the app root
73
-
74
- ```tsx
75
- import { createPikku, PikkuProvider } from '@pikku/react'
76
- import { PikkuFetch } from './pikku/pikku-fetch.gen'
77
- import { PikkuRPC } from './pikku/pikku-rpc.gen'
78
- import { apiUrl } from './lib/env'
79
-
80
- const pikku = createPikku(PikkuFetch, PikkuRPC, {
81
- serverUrl: apiUrl(),
82
- })
83
-
84
- createRoot(document.getElementById('root')!).render(
85
- <PikkuProvider pikku={pikku}>
86
- <App />
87
- </PikkuProvider>
88
- )
89
- ```
90
-
91
- If the project also exposes realtime events (see **pikku-realtime**), pass
92
- the `PikkuRealtime` class as the third argument and the instance gets a
93
- `realtime` field too:
94
-
95
- ```tsx
96
- import { PikkuRealtime } from './pikku/realtime.gen'
97
-
98
- const pikku = createPikku(PikkuFetch, PikkuRPC, PikkuRealtime, {
99
- serverUrl: apiUrl(),
100
- })
101
- // pikku.fetch / pikku.rpc / pikku.realtime — all share the same fetch
102
- // (server URL + auth configured once).
103
- ```
104
-
105
- The generated classes come from your `pikku.config.json`:
106
-
107
- | config field | generated file |
108
- | ---------------------------- | ----------------------------------------------------- |
109
- | `clientFiles.fetchFile` | typed HTTP client (`PikkuFetch` class) |
110
- | `clientFiles.rpcWiringsFile` | RPC client (`PikkuRPC` class) calling all exposed fns |
111
- | `clientFiles.realtimeFile` | `PikkuRealtime` (websocket events + SSE + channels) |
112
-
113
- If a file isn't being generated, that field is missing from the config —
114
- add it and re-run `pikku all`.
115
-
116
- `createPikku(...)` accepts the same `CorePikkuFetchOptions` as `PikkuFetch`
117
- plus `serverUrl`. Auth headers, request interceptors, etc. are configured
118
- on the fetch instance — RPC and realtime inherit them automatically.
119
-
120
- ## Calling an RPC directly (no React Query)
121
-
122
- Inside a component:
123
-
124
- ```tsx
125
- import { usePikkuRPC } from '@pikku/react'
126
-
127
- function Logout() {
128
- const rpc = usePikkuRPC()
129
- return <button onClick={() => rpc.invoke('logoutUser', {})}>Sign out</button>
130
- }
131
- ```
132
-
133
- `rpc.invoke(name, data)` is typed against `FlattenedRPCMap` — `name` must
134
- be an exposed function id, `data` matches the input schema, return value
135
- matches the output schema.
136
-
137
- You also have `rpc.<funcName>(data)` if the generated RPC client builds
138
- direct methods (project-dependent).
139
-
140
- ## Calling fetch directly
141
-
142
- ```tsx
143
- const fetch = usePikkuFetch()
144
- const data = await fetch.get('/some-rest-route', { searchParams: {...} })
145
- ```
146
-
147
- Use this only when the function is wired via HTTP (REST shape) and you
148
- need a path-style call. For RPC calls, `usePikkuRPC()` is cleaner.
149
-
150
- ## Realtime subscriptions
151
-
152
- If you wired a `PikkuRealtime` class into `createPikku`, use
153
- `usePikkuRealtime()` to grab the shared instance:
154
-
155
- ```tsx
156
- import { usePikkuRealtime } from '@pikku/react'
157
- import type { PikkuRealtime } from './pikku/realtime.gen'
158
-
159
- function TodoList() {
160
- const realtime = usePikkuRealtime<PikkuRealtime>()
161
- useEffect(() => {
162
- return realtime.subscribe('todo-created', ({ todo }) => {
163
- /* ... */
164
- })
165
- }, [realtime])
166
- // ...
167
- }
168
- ```
169
-
170
- The hook throws if no `PikkuRealtime` was wired — that's how you know to
171
- add it to `createPikku(...)`. Full event-hub setup, publishing, and SSE
172
- helpers live in **pikku-realtime**.
173
-
174
- ## When to reach for what
175
-
176
- | Need | Use |
177
- | ----------------------------------- | -------------------------------------------------- |
178
- | Render data, dedupe + cache | **usePikkuQuery** (react-query) |
179
- | Trigger a write, wait for result | **usePikkuMutation** (react-query) |
180
- | Paginate | **usePikkuInfiniteQuery** (react-query) |
181
- | One-off call from an event handler | `usePikkuRPC()` direct |
182
- | Hit a REST endpoint (not RPC) | `usePikkuFetch()` |
183
- | Run one named workflow | `usePikkuWorkflow('name')` → `.start/.run/.status` |
184
- | Talk to one named AI agent | `usePikkuAgent('name')` → `.run/.stream/.approve` |
185
- | Longer-running workflow UX | **pikku-workflows-client** |
186
- | Subscribe to events / SSE / channel | `usePikkuRealtime()` (see **pikku-realtime**) |
187
-
188
- The first three live in your generated `api.gen.ts` (see the
189
- **pikku-react-query** skill). This skill covers the rest.
190
-
191
- `usePikkuAgent` and `usePikkuWorkflow` bind the name once and hand back the
192
- call methods with it already applied:
193
-
194
- ```tsx
195
- const agent = usePikkuAgent('todo-agent')
196
- const { text } = await agent.run({ message, threadId })
197
-
198
- const workflow = usePikkuWorkflow('onboardUser')
199
- const { runId } = await workflow.start({ email })
200
- const state = await workflow.status(runId)
201
- ```
202
-
203
- ## Authentication
204
-
205
- Auth is handled at the `PikkuFetch` layer, and `createPikku`'s options object
206
- _is_ `CorePikkuFetchOptions` plus `serverUrl` — flat, not nested under a
207
- `fetchOptions` key:
208
-
209
- ```tsx
210
- const pikku = createPikku(PikkuFetch, PikkuRPC, {
211
- serverUrl: apiUrl(),
212
- credentials: 'include', // cookie sessions
213
- authHeaders: { jwt: token }, // or { apiKey }
214
- transformDate: true,
215
- })
216
- ```
217
-
218
- There is no request-interceptor hook. For a token that changes after startup,
219
- call the setter on the shared instance — RPC and realtime pick it up because
220
- they hold the same fetch:
221
-
222
- ```tsx
223
- pikku.fetch.setAuthorizationJWT(token) // null clears it
224
- pikku.fetch.setAPIKey(key)
225
- pikku.fetch.setHeader('x-tenant', tenantId)
226
- ```
227
-
228
- `authHeaders.jwt` becomes `Authorization: Bearer …` and `authHeaders.apiKey`
229
- becomes `X-API-KEY`; setting a JWT takes precedence over an API key.
230
-
231
- ### Dev actor sign-in (`useDevActors`)
232
-
233
- The dev-only "Sign in as …" control: one click signs in as a declared scenario
234
- persona with no password, so the app can be reviewed as each kind of user.
235
- `pikku fabric validate` **requires** any frontend with a login screen to ship one
236
- (`app-missing-actor-quick-login-<app>`) — without it a reviewer is locked out of
237
- their own sandbox.
238
-
239
- ```tsx
240
- import { useDevActors } from '@pikku/react'
241
-
242
- const { actors, signInAs, pendingEmail, isPending, error } = useDevActors({
243
- // Gate both reads on the bundler's dev flag so no credential can reach a
244
- // production bundle. The sandbox dev server bakes them from your personas.
245
- actors: import.meta.env.DEV ? import.meta.env.VITE_DEV_ACTORS : undefined,
246
- secrets: import.meta.env.DEV
247
- ? import.meta.env.VITE_DEV_ACTOR_SECRETS
248
- : undefined,
249
- apiUrl: apiUrl(),
250
- onSignedIn: () => navigate({ to: '/' }),
251
- })
252
- ```
253
-
254
- - **It is UI-free**, so render it however you like. For the default rendering use
255
- `<DevActorSwitcher />` from `@pikku/mantine/dev` — a separate entry point from
256
- `@pikku/mantine/core`, whose contract is "drop-in alias for `@mantine/core`"
257
- and so must not export components Mantine has no counterpart for.
258
- - **`secrets` is `{ address: credential }`, not one shared value** — a
259
- credential opens the one persona it was minted for (see
260
- **pikku-better-auth**). `actors` is empty unless the host supplied both a list
261
- and the credentials for it, and an actor with no credential is not offered, so
262
- a production build renders nothing without you testing for it.
263
- - **It takes `onSignedIn` rather than a router**, and takes the env values rather
264
- than reading them, because how env is spelled is a bundler fact
265
- (`import.meta.env.VITE_*` vs `process.env.NEXT_PUBLIC_*`).
266
- - The underlying `signInAsActor()` and `parseDevActors()` are exported too, for a
267
- non-React caller. The endpoint only accepts rows flagged `actor: true`, so it
268
- can never impersonate a real user — see **pikku-better-auth**.
269
-
270
- Do not hand-write the `devActors()` / `signInAsActor()` pair per app; that
271
- copy-paste, including the `import.meta.env.DEV` gate, is exactly what this
272
- replaced.
273
-
274
- ### Linking from a Mantine element: `renderRoot`, not `component`
275
-
276
- Handing TanStack's `Link` to a Mantine element as `component={Link}` compiles,
277
- renders, and navigates — and silently unties the type. Mantine's polymorphic
278
- `component` prop widens the router generic to `AnyRouter`, so `to` and
279
- `params` stop being checked against your actual routes. Renaming a route then
280
- breaks the running app instead of the build, which is the one thing the typed
281
- router exists to prevent.
282
-
283
- Wrap the typed `Link` once and reach it through `renderRoot`, which passes the
284
- props through without re-typing the element:
285
-
286
- ```tsx
287
- // components/links.tsx — one wrapper the whole app links through
288
- import { Link } from '@tanstack/react-router'
289
-
290
- export const AssessmentLink = (props: { assessmentId: string; children: React.ReactNode }) => (
291
- <Link to="/assessments/$assessmentId" params={{ assessmentId: props.assessmentId }}>
292
- {props.children}
293
- </Link>
294
- )
295
- ```
296
-
297
- ```tsx
298
- <Button renderRoot={(p) => <AssessmentLink assessmentId={id} {...p} />}>
299
- Open
300
- </Button>
301
- ```
302
-
303
- The wrapper is where `to` and `params` are checked, and it is checked once.
44
+ A workflow that finishes in a moment can be awaited; one that does not needs
45
+ start-plus-observe, or the component holds a pending state with nothing to show.
304
46
 
305
47
  ## What NOT to do
306
48
 
307
- - Don't instantiate `PikkuFetch`/`PikkuRPC` inside a component — `createPikku`
308
- goes once at the app root, the instance flows through Context.
309
- - Don't call `usePikkuRPC()` outside a `<PikkuProvider>` — it throws.
310
- - Don't write a custom RPC client. The generated one already covers every
311
- exposed function with full types.
312
- - Don't hardcode user-facing strings. Every display string goes through an
313
- i18n token — see **pikku-i18n** for the setup (it's English-only by default).
49
+ - **Do not write a client.** The generated one covers every exposed function
50
+ with full types; a hand-rolled RPC client or a hand-written
51
+ `useQuery({ queryKey, queryFn })` reimplements it worse.
52
+ - **Do not instantiate `PikkuFetch`/`PikkuRPC` in a component.** `createPikku`
53
+ runs once at the app root and the instance flows through context — and
54
+ `usePikkuRPC()` outside `<PikkuProvider>` throws.
55
+ - **Do not call the RPC client inside a `useEffect`.** The hooks handle
56
+ deduplication, caching and unmounting; a manual effect handles none of them.
57
+ - **Do not construct a hook name at runtime.** Hook names are the RPC names known
58
+ at generation time, and a computed one is not type-checked.
59
+ - **Do not poll a workflow with `setInterval`.** `useWorkflowStatus` with a
60
+ `refetchInterval` callback dedupes across components and stops on a terminal
61
+ state in one place.
62
+ - **Do not reach for `as any` when a hook's types disagree with you.** The
63
+ mismatch is the backend's input/output schema; fix it there.
64
+ - **Do not hardcode a user-facing string.** Every display string goes through an
65
+ i18n message — see `pikku-i18n`.
@@ -0,0 +1,293 @@
1
+ # Pikku React
2
+
3
+
4
+ ## What ships
5
+
6
+ ```tsx
7
+ import {
8
+ PikkuProvider,
9
+ createPikku,
10
+ usePikkuFetch,
11
+ usePikkuRPC,
12
+ usePikkuRealtime,
13
+ usePikkuAgent,
14
+ usePikkuWorkflow,
15
+ asI18n,
16
+ } from '@pikku/react'
17
+ ```
18
+
19
+ `usePikkuRealtime` is only valid when you wired a `PikkuRealtime` class via
20
+ `createPikku` — see the setup section. `usePikkuAgent` and `usePikkuWorkflow`
21
+ are thin bindings over the RPC client that pin one agent/workflow name, so a
22
+ component never repeats it. `asI18n` is the i18n brand (see **pikku-i18n**).
23
+
24
+ ## Resolving the server URL
25
+
26
+ Every client (`createPikku`, realtime, the auth client) resolves its base
27
+ through one shared helper in `src/lib/env.ts`. Write this once:
28
+
29
+ ```ts
30
+ // Endpoints come from env, never hardcoded.
31
+ export function apiUrl(): string {
32
+ // SSR: the client hooks only run in the browser, so a placeholder is fine.
33
+ if (import.meta.env.SSR) {
34
+ return import.meta.env.VITE_API_URL ?? '/__api'
35
+ }
36
+ return import.meta.env.VITE_API_URL ?? `${window.location.origin}/api`
37
+ }
38
+ ```
39
+
40
+ **Never fall back to `http://localhost:3000`.** `import.meta.env.VITE_API_URL`
41
+ is substituted by Vite at _build_ time, so any deploy that supplies the URL as
42
+ a _runtime_ env var or platform binding leaves it `undefined` in the shipped
43
+ bundle — the fallback is then the only branch that ever runs in the browser. A
44
+ localhost fallback means every request from a deployed app goes to the user's
45
+ own machine. `origin + '/api'` is same-origin, needs no build-time knowledge of
46
+ the domain, and is correct wherever the app is served from.
47
+
48
+ For local dev, set `VITE_API_URL`, or proxy `/api` → your backend in
49
+ `vite.config.ts` under `server.proxy`. One `/api` entry also covers
50
+ `/api/auth/*`; only add more entries for root-level routes outside `/api`.
51
+
52
+ ## Setup at the app root
53
+
54
+ ```tsx
55
+ import { createPikku, PikkuProvider } from '@pikku/react'
56
+ import { PikkuFetch } from './pikku/pikku-fetch.gen'
57
+ import { PikkuRPC } from './pikku/pikku-rpc.gen'
58
+ import { apiUrl } from './lib/env'
59
+
60
+ const pikku = createPikku(PikkuFetch, PikkuRPC, {
61
+ serverUrl: apiUrl(),
62
+ })
63
+
64
+ createRoot(document.getElementById('root')!).render(
65
+ <PikkuProvider pikku={pikku}>
66
+ <App />
67
+ </PikkuProvider>
68
+ )
69
+ ```
70
+
71
+ If the project also exposes realtime events (see **pikku-wiring**), pass
72
+ the `PikkuRealtime` class as the third argument and the instance gets a
73
+ `realtime` field too:
74
+
75
+ ```tsx
76
+ import { PikkuRealtime } from './pikku/realtime.gen'
77
+
78
+ const pikku = createPikku(PikkuFetch, PikkuRPC, PikkuRealtime, {
79
+ serverUrl: apiUrl(),
80
+ })
81
+ // pikku.fetch / pikku.rpc / pikku.realtime — all share the same fetch
82
+ // (server URL + auth configured once).
83
+ ```
84
+
85
+ The generated classes come from your `pikku.config.json`:
86
+
87
+ | config field | generated file |
88
+ | ---------------------------- | ----------------------------------------------------- |
89
+ | `clientFiles.fetchFile` | typed HTTP client (`PikkuFetch` class) |
90
+ | `clientFiles.rpcWiringsFile` | RPC client (`PikkuRPC` class) calling all exposed fns |
91
+ | `clientFiles.realtimeFile` | `PikkuRealtime` (websocket events + SSE + channels) |
92
+
93
+ If a file isn't being generated, that field is missing from the config —
94
+ add it and re-run `pikku all`.
95
+
96
+ `createPikku(...)` accepts the same `CorePikkuFetchOptions` as `PikkuFetch`
97
+ plus `serverUrl`. Auth headers, request interceptors, etc. are configured
98
+ on the fetch instance — RPC and realtime inherit them automatically.
99
+
100
+ ## Calling an RPC directly (no React Query)
101
+
102
+ Inside a component:
103
+
104
+ ```tsx
105
+ import { usePikkuRPC } from '@pikku/react'
106
+
107
+ function Logout() {
108
+ const rpc = usePikkuRPC()
109
+ return <button onClick={() => rpc.invoke('logoutUser', {})}>Sign out</button>
110
+ }
111
+ ```
112
+
113
+ `rpc.invoke(name, data)` is typed against `FlattenedRPCMap` — `name` must
114
+ be an exposed function id, `data` matches the input schema, return value
115
+ matches the output schema.
116
+
117
+ You also have `rpc.<funcName>(data)` if the generated RPC client builds
118
+ direct methods (project-dependent).
119
+
120
+ ## Calling fetch directly
121
+
122
+ ```tsx
123
+ const fetch = usePikkuFetch()
124
+ const data = await fetch.get('/some-rest-route', { searchParams: {...} })
125
+ ```
126
+
127
+ Use this only when the function is wired via HTTP (REST shape) and you
128
+ need a path-style call. For RPC calls, `usePikkuRPC()` is cleaner.
129
+
130
+ ## Realtime subscriptions
131
+
132
+ If you wired a `PikkuRealtime` class into `createPikku`, use
133
+ `usePikkuRealtime()` to grab the shared instance:
134
+
135
+ ```tsx
136
+ import { usePikkuRealtime } from '@pikku/react'
137
+ import type { PikkuRealtime } from './pikku/realtime.gen'
138
+
139
+ function TodoList() {
140
+ const realtime = usePikkuRealtime<PikkuRealtime>()
141
+ useEffect(() => {
142
+ return realtime.subscribe('todo-created', ({ todo }) => {
143
+ /* ... */
144
+ })
145
+ }, [realtime])
146
+ // ...
147
+ }
148
+ ```
149
+
150
+ The hook throws if no `PikkuRealtime` was wired — that's how you know to
151
+ add it to `createPikku(...)`. Full event-hub setup, publishing, and SSE
152
+ helpers live in **pikku-wiring**.
153
+
154
+ ## When to reach for what
155
+
156
+ | Need | Use |
157
+ | ----------------------------------- | -------------------------------------------------- |
158
+ | Render data, dedupe + cache | **usePikkuQuery** (react-query) |
159
+ | Trigger a write, wait for result | **usePikkuMutation** (react-query) |
160
+ | Paginate | **usePikkuInfiniteQuery** (react-query) |
161
+ | One-off call from an event handler | `usePikkuRPC()` direct |
162
+ | Hit a REST endpoint (not RPC) | `usePikkuFetch()` |
163
+ | Run one named workflow | `usePikkuWorkflow('name')` → `.start/.run/.status` |
164
+ | Talk to one named AI agent | `usePikkuAgent('name')` → `.run/.stream/.approve` |
165
+ | Longer-running workflow UX | `references/workflows.md` |
166
+ | Subscribe to events / SSE / channel | `usePikkuRealtime()` (see **pikku-wiring**) |
167
+
168
+ The first three live in your generated `api.gen.ts` (see the
169
+ `references/react-query.md`). This reference covers the rest.
170
+
171
+ `usePikkuAgent` and `usePikkuWorkflow` bind the name once and hand back the
172
+ call methods with it already applied:
173
+
174
+ ```tsx
175
+ const agent = usePikkuAgent('todo-agent')
176
+ const { text } = await agent.run({ message, threadId })
177
+
178
+ const workflow = usePikkuWorkflow('onboardUser')
179
+ const { runId } = await workflow.start({ email })
180
+ const state = await workflow.status(runId)
181
+ ```
182
+
183
+ ## Authentication
184
+
185
+ Auth is handled at the `PikkuFetch` layer, and `createPikku`'s options object
186
+ _is_ `CorePikkuFetchOptions` plus `serverUrl` — flat, not nested under a
187
+ `fetchOptions` key:
188
+
189
+ ```tsx
190
+ const pikku = createPikku(PikkuFetch, PikkuRPC, {
191
+ serverUrl: apiUrl(),
192
+ credentials: 'include', // cookie sessions
193
+ authHeaders: { jwt: token }, // or { apiKey }
194
+ transformDate: true,
195
+ })
196
+ ```
197
+
198
+ There is no request-interceptor hook. For a token that changes after startup,
199
+ call the setter on the shared instance — RPC and realtime pick it up because
200
+ they hold the same fetch:
201
+
202
+ ```tsx
203
+ pikku.fetch.setAuthorizationJWT(token) // null clears it
204
+ pikku.fetch.setAPIKey(key)
205
+ pikku.fetch.setHeader('x-tenant', tenantId)
206
+ ```
207
+
208
+ `authHeaders.jwt` becomes `Authorization: Bearer …` and `authHeaders.apiKey`
209
+ becomes `X-API-KEY`; setting a JWT takes precedence over an API key.
210
+
211
+ ### Dev actor sign-in (`useDevActors`)
212
+
213
+ The dev-only "Sign in as …" control: one click signs in as a declared scenario
214
+ persona with no password, so the app can be reviewed as each kind of user.
215
+ `pikku fabric validate` **requires** any frontend with a login screen to ship one
216
+ (`app-missing-actor-quick-login-<app>`) — without it a reviewer is locked out of
217
+ their own sandbox.
218
+
219
+ ```tsx
220
+ import { useDevActors } from '@pikku/react'
221
+
222
+ const { actors, signInAs, pendingEmail, isPending, error } = useDevActors({
223
+ // Gate both reads on the bundler's dev flag so no credential can reach a
224
+ // production bundle. The sandbox dev server bakes them from your personas.
225
+ actors: import.meta.env.DEV ? import.meta.env.VITE_DEV_ACTORS : undefined,
226
+ secrets: import.meta.env.DEV
227
+ ? import.meta.env.VITE_DEV_ACTOR_SECRETS
228
+ : undefined,
229
+ apiUrl: apiUrl(),
230
+ onSignedIn: () => navigate({ to: '/' }),
231
+ })
232
+ ```
233
+
234
+ - **It is UI-free**, so render it however you like. For the default rendering use
235
+ `<DevActorSwitcher />` from `@pikku/mantine/dev` — a separate entry point from
236
+ `@pikku/mantine/core`, whose contract is "drop-in alias for `@mantine/core`"
237
+ and so must not export components Mantine has no counterpart for.
238
+ - **`secrets` is `{ address: credential }`, not one shared value** — a
239
+ credential opens the one persona it was minted for (see
240
+ **pikku-auth**). `actors` is empty unless the host supplied both a list
241
+ and the credentials for it, and an actor with no credential is not offered, so
242
+ a production build renders nothing without you testing for it.
243
+ - **It takes `onSignedIn` rather than a router**, and takes the env values rather
244
+ than reading them, because how env is spelled is a bundler fact
245
+ (`import.meta.env.VITE_*` vs `process.env.NEXT_PUBLIC_*`).
246
+ - The underlying `signInAsActor()` and `parseDevActors()` are exported too, for a
247
+ non-React caller. The endpoint only accepts rows flagged `actor: true`, so it
248
+ can never impersonate a real user — see **pikku-auth**.
249
+
250
+ Do not hand-write the `devActors()` / `signInAsActor()` pair per app; that
251
+ copy-paste, including the `import.meta.env.DEV` gate, is exactly what this
252
+ replaced.
253
+
254
+ ### Linking from a Mantine element: `renderRoot`, not `component`
255
+
256
+ Handing TanStack's `Link` to a Mantine element as `component={Link}` compiles,
257
+ renders, and navigates — and silently unties the type. Mantine's polymorphic
258
+ `component` prop widens the router generic to `AnyRouter`, so `to` and
259
+ `params` stop being checked against your actual routes. Renaming a route then
260
+ breaks the running app instead of the build, which is the one thing the typed
261
+ router exists to prevent.
262
+
263
+ Wrap the typed `Link` once and reach it through `renderRoot`, which passes the
264
+ props through without re-typing the element:
265
+
266
+ ```tsx
267
+ // components/links.tsx — one wrapper the whole app links through
268
+ import { Link } from '@tanstack/react-router'
269
+
270
+ export const AssessmentLink = (props: { assessmentId: string; children: React.ReactNode }) => (
271
+ <Link to="/assessments/$assessmentId" params={{ assessmentId: props.assessmentId }}>
272
+ {props.children}
273
+ </Link>
274
+ )
275
+ ```
276
+
277
+ ```tsx
278
+ <Button renderRoot={(p) => <AssessmentLink assessmentId={id} {...p} />}>
279
+ Open
280
+ </Button>
281
+ ```
282
+
283
+ The wrapper is where `to` and `params` are checked, and it is checked once.
284
+
285
+ ## What NOT to do
286
+
287
+ - Don't instantiate `PikkuFetch`/`PikkuRPC` inside a component — `createPikku`
288
+ goes once at the app root, the instance flows through Context.
289
+ - Don't call `usePikkuRPC()` outside a `<PikkuProvider>` — it throws.
290
+ - Don't write a custom RPC client. The generated one already covers every
291
+ exposed function with full types.
292
+ - Don't hardcode user-facing strings. Every display string goes through an
293
+ i18n token — see **pikku-i18n** for the setup (it's English-only by default).