@pikku/skills 0.12.35 → 0.12.38

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 (41) hide show
  1. package/README.md +9 -4
  2. package/dist/index.d.ts +7 -4
  3. package/dist/index.js +9 -5
  4. package/dist/skills.gen.d.ts +1 -0
  5. package/dist/skills.gen.js +5 -3
  6. package/dist/snippets.d.ts +26 -0
  7. package/dist/snippets.js +148 -0
  8. package/package.json +2 -2
  9. package/skills/pikku-addon/SKILL.md +70 -24
  10. package/skills/pikku-addon/references/addon-package-manifest.md +9 -4
  11. package/skills/pikku-addon/references/openapi.md +130 -0
  12. package/skills/pikku-agent/references/agents.md +3 -1
  13. package/skills/pikku-auth/references/better-auth.md +33 -2
  14. package/skills/pikku-build/SKILL.md +94 -6
  15. package/skills/pikku-build/references/app.md +82 -12
  16. package/skills/pikku-build/references/design.md +16 -5
  17. package/skills/pikku-build/references/feature.md +23 -96
  18. package/skills/pikku-build/references/openapi.md +119 -0
  19. package/skills/pikku-build/references/platform.md +4 -0
  20. package/skills/pikku-build/references/quick.md +16 -6
  21. package/skills/pikku-changes/SKILL.md +172 -0
  22. package/skills/pikku-concepts/SKILL.md +33 -138
  23. package/skills/pikku-concepts/references/bootstrap.md +58 -0
  24. package/skills/pikku-concepts/references/concept-mapping.md +16 -0
  25. package/skills/pikku-concepts/references/language.md +87 -0
  26. package/skills/pikku-deploy/SKILL.md +1 -1
  27. package/skills/pikku-fabric/SKILL.md +13 -13
  28. package/skills/pikku-guide/SKILL.md +264 -0
  29. package/skills/pikku-kysely/SKILL.md +1 -1
  30. package/skills/pikku-n8n-import/SKILL.md +4 -3
  31. package/skills/pikku-react/references/client.md +12 -0
  32. package/skills/pikku-realtime/SKILL.md +6 -6
  33. package/skills/pikku-report/SKILL.md +143 -0
  34. package/skills/pikku-scenario/SKILL.md +71 -562
  35. package/skills/pikku-scenario/references/browser.md +59 -0
  36. package/skills/pikku-scenario/references/coverage.md +70 -0
  37. package/skills/pikku-scenario/references/personas.md +149 -0
  38. package/skills/pikku-scenario/references/steps.md +366 -0
  39. package/skills/pikku-service-backends/SKILL.md +1 -1
  40. package/skills/pikku-wiring/SKILL.md +1 -1
  41. package/skills/pikku-workflow/SKILL.md +7 -8
@@ -0,0 +1,26 @@
1
+ export declare class UnclosedSnippetError extends Error {
2
+ constructor(name: string, file: string);
3
+ }
4
+ export declare class DuplicateSnippetError extends Error {
5
+ constructor(name: string, file: string, other: string);
6
+ }
7
+ export declare class MissingSnippetError extends Error {
8
+ constructor(name: string, file: string);
9
+ }
10
+ /**
11
+ * The `// @snippet start <name>` regions in a project's source, so an example
12
+ * in a doc comment can name one instead of restating it. The same mechanism the
13
+ * website uses to pull its code blocks out of the shop template: the code that
14
+ * reaches the reader is the code that compiles, and it cannot drift.
15
+ */
16
+ export declare const collectSnippets: (projectDir: string, into?: Map<string, string>, origins?: Map<string, string>) => Promise<Map<string, string>>;
17
+ export type Snippets = Record<string, string> | Map<string, string>;
18
+ /** Every region a markdown document shows, in order. */
19
+ export declare const snippetRegionsIn: (markdown: string) => string[];
20
+ /**
21
+ * A skill's text with every marked fence replaced by its region's code. Used
22
+ * twice, deliberately: `scripts/embed.ts` bakes the result into the manifest
23
+ * that ships inside the CLI binary, and `readSkillFile` applies it to a
24
+ * filesystem copy so an installed package shows the same code the binary does.
25
+ */
26
+ export declare const expandSkillMarkdown: (markdown: string, snippets: Snippets, file?: string) => string;
@@ -0,0 +1,148 @@
1
+ import { readdir, readFile } from 'node:fs/promises';
2
+ import { join, relative } from 'node:path';
3
+ const START = /^\s*(?:\/\/|--)\s*@snippet start (\S+)\s*$/;
4
+ const END = /^\s*(?:\/\/|--)\s*@snippet end (\S+)\s*$/;
5
+ const SOURCE_EXTENSIONS = ['.ts', '.tsx', '.sql'];
6
+ const SKIPPED_DIRECTORIES = new Set(['node_modules', '.pikku', 'dist', '.git']);
7
+ export class UnclosedSnippetError extends Error {
8
+ constructor(name, file) {
9
+ super(`The snippet "${name}" opens in ${file} and never closes. Add "// @snippet end ${name}".`);
10
+ this.name = 'UnclosedSnippetError';
11
+ }
12
+ }
13
+ export class DuplicateSnippetError extends Error {
14
+ constructor(name, file, other) {
15
+ super(`The snippet "${name}" is defined twice, in ${other} and ${file}. Snippet names are the reference an example uses, so they have to be unique.`);
16
+ this.name = 'DuplicateSnippetError';
17
+ }
18
+ }
19
+ export class MissingSnippetError extends Error {
20
+ constructor(name, file) {
21
+ super(`${file} shows the snippet "${name}", which no @snippet region defines. ` +
22
+ `Add "// @snippet start ${name}" to a compiled example project, or fix the fence.`);
23
+ this.name = 'MissingSnippetError';
24
+ }
25
+ }
26
+ const dedent = (lines) => {
27
+ const indents = lines
28
+ .filter((line) => line.trim().length > 0)
29
+ .map((line) => line.length - line.trimStart().length);
30
+ const shortest = indents.length > 0 ? Math.min(...indents) : 0;
31
+ return lines
32
+ .map((line) => line.slice(shortest))
33
+ .join('\n')
34
+ .trim();
35
+ };
36
+ const sourceFilesIn = async (directory, found = []) => {
37
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
38
+ const full = join(directory, entry.name);
39
+ if (entry.isDirectory()) {
40
+ if (SKIPPED_DIRECTORIES.has(entry.name) || entry.name.startsWith('.')) {
41
+ continue;
42
+ }
43
+ await sourceFilesIn(full, found);
44
+ }
45
+ else if (SOURCE_EXTENSIONS.some((ext) => entry.name.endsWith(ext))) {
46
+ found.push(full);
47
+ }
48
+ }
49
+ return found;
50
+ };
51
+ /**
52
+ * The `// @snippet start <name>` regions in a project's source, so an example
53
+ * in a doc comment can name one instead of restating it. The same mechanism the
54
+ * website uses to pull its code blocks out of the shop template: the code that
55
+ * reaches the reader is the code that compiles, and it cannot drift.
56
+ */
57
+ export const collectSnippets = async (projectDir, into = new Map(), origins = new Map()) => {
58
+ for (const file of await sourceFilesIn(projectDir)) {
59
+ const where = relative(projectDir, file);
60
+ const lines = (await readFile(file, 'utf8')).split('\n');
61
+ const open = new Map();
62
+ for (const line of lines) {
63
+ const end = line.match(END);
64
+ if (end?.[1] && open.has(end[1])) {
65
+ const name = end[1];
66
+ const existing = origins.get(name);
67
+ if (existing)
68
+ throw new DuplicateSnippetError(name, where, existing);
69
+ into.set(name, dedent(open.get(name)));
70
+ origins.set(name, where);
71
+ open.delete(name);
72
+ continue;
73
+ }
74
+ const start = line.match(START);
75
+ if (start?.[1]) {
76
+ if (!open.has(start[1]))
77
+ open.set(start[1], []);
78
+ continue;
79
+ }
80
+ if (end)
81
+ continue;
82
+ for (const body of open.values())
83
+ body.push(line);
84
+ }
85
+ const [unclosed] = open.keys();
86
+ if (unclosed)
87
+ throw new UnclosedSnippetError(unclosed, where);
88
+ }
89
+ return into;
90
+ };
91
+ /**
92
+ * A fence that names a region instead of carrying code:
93
+ *
94
+ * ```ts snippet:definePersonas
95
+ * ```
96
+ *
97
+ * The language is optional and kept. Marked fences are replaced whole — both
98
+ * the placeholder form and a filled-in block, so a skill can be migrated
99
+ * without hand-deleting the old copy.
100
+ */
101
+ const SNIPPET_FENCE = /^(\s*)```(\S*)\s+snippet:([A-Za-z0-9_.-]+)\s*$/;
102
+ const CLOSING_FENCE = /^\s*```\s*$/;
103
+ const replace = (markdown, lookup) => {
104
+ const lines = markdown.split('\n');
105
+ const out = [];
106
+ for (let i = 0; i < lines.length; i++) {
107
+ const line = lines[i];
108
+ const match = line.match(SNIPPET_FENCE);
109
+ if (!match) {
110
+ out.push(line);
111
+ continue;
112
+ }
113
+ const indent = match[1] ?? '';
114
+ const lang = match[2] ?? '';
115
+ const region = match[3];
116
+ const body = lookup(region);
117
+ out.push(`${indent}\`\`\`${lang}`);
118
+ for (const bodyLine of body.split('\n')) {
119
+ out.push(bodyLine.length > 0 ? indent + bodyLine : '');
120
+ }
121
+ out.push(`${indent}\`\`\``);
122
+ while (i + 1 < lines.length && !CLOSING_FENCE.test(lines[i + 1]))
123
+ i++;
124
+ i++;
125
+ }
126
+ return out.join('\n');
127
+ };
128
+ /** Every region a markdown document shows, in order. */
129
+ export const snippetRegionsIn = (markdown) => markdown
130
+ .split('\n')
131
+ .map((line) => line.match(SNIPPET_FENCE))
132
+ .filter((match) => match !== null)
133
+ .map((match) => match[3]);
134
+ /**
135
+ * A skill's text with every marked fence replaced by its region's code. Used
136
+ * twice, deliberately: `scripts/embed.ts` bakes the result into the manifest
137
+ * that ships inside the CLI binary, and `readSkillFile` applies it to a
138
+ * filesystem copy so an installed package shows the same code the binary does.
139
+ */
140
+ export const expandSkillMarkdown = (markdown, snippets, file = 'skill') => {
141
+ const lookup = (name) => {
142
+ const body = snippets instanceof Map ? snippets.get(name) : snippets[name];
143
+ if (body === undefined)
144
+ throw new MissingSnippetError(name, file);
145
+ return body;
146
+ };
147
+ return replace(markdown, lookup);
148
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/skills",
3
- "version": "0.12.35",
3
+ "version": "0.12.38",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/pikkujs/pikku.git",
@@ -14,7 +14,7 @@
14
14
  "types": "dist/index.d.ts",
15
15
  "type": "module",
16
16
  "scripts": {
17
- "embed": "node scripts/embed.mjs",
17
+ "embed": "bun run scripts/embed.ts",
18
18
  "tsc": "bun run embed && tsc",
19
19
  "build": "bun run embed && tsc -b",
20
20
  "ncu": "npx npm-check-updates",
@@ -3,8 +3,10 @@ name: pikku-addon
3
3
  description: >-
4
4
  Use when creating or consuming reusable function packages (addons) in Pikku. Covers wireAddon,
5
5
  ref(), pikkuAddonServices, pikkuAddonWireServices, addon package structure, addons that ship
6
- database tables (pikku db export), and cross-project function sharing. TRIGGER when: code uses wireAddon/ref()/pikkuAddonServices, user asks about
7
- addons, reusable function packages, cross-project sharing, or addon package structure. DO NOT
6
+ database tables (pikku db export), generating an addon from an OpenAPI/Swagger spec, and
7
+ cross-project function sharing. TRIGGER when: code uses wireAddon/ref()/pikkuAddonServices, user
8
+ asks about addons, reusable function packages, cross-project sharing, or addon package structure,
9
+ or the user hands over an OpenAPI/Swagger spec (file or URL) to build on. DO NOT
8
10
  TRIGGER when: user asks about internal function composition (use pikku-wiring) or general function
9
11
  definitions (use pikku-concepts).
10
12
  installGroups: [core]
@@ -19,7 +21,7 @@ Use this skill as an execution checklist, not reference material.
19
21
  1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.
20
22
  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.
21
23
  3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.
22
- 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.
24
+ 4. Validate with the narrowest relevant command first, then run `pikku all` when functions, wirings, schemas, or generated clients may have changed.
23
25
  5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.
24
26
 
25
27
  Addons are reusable Pikku function packages that can be shared across projects. They bundle functions, services, secrets, and variables into a self-contained NPM package.
@@ -48,6 +50,7 @@ wireAddon({
48
50
  rpcEndpoint?: string, // Optional remote RPC endpoint for distributed execution
49
51
  auth?: boolean, // Require a session for every function in the addon
50
52
  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
53
+ expose?: boolean | string[], // what rpc.exposed / POST /rpc may reach: unset/true = the addon's own expose: true, false = none, a list = exactly those (typed; PKU343 on unknown names)
51
54
  tags?: string[], // Tags applied to all addon functions
52
55
  scopes?: string[], // Required of every function, on top of its own
53
56
  secretOverrides?: Record<string, string>, // Remap secret names (and grant them)
@@ -146,7 +149,7 @@ second argument is always present — an addon never falls back to its own logge
146
149
  variables or secrets; the consuming app supplies them:
147
150
 
148
151
  ```typescript
149
- import { pikkuAddonServices } from '#pikku/setup'
152
+ import { pikkuAddonServices } from '#pikku/addon/setup'
150
153
 
151
154
  export const createSingletonServices = pikkuAddonServices(
152
155
  async (config, { secrets, logger }) => {
@@ -167,7 +170,7 @@ config object.
167
170
  Define per-request services for an addon package (created fresh per HTTP request, queue job, etc.):
168
171
 
169
172
  ```typescript
170
- import { pikkuAddonWireServices } from '#pikku/setup'
173
+ import { pikkuAddonWireServices } from '#pikku/addon/setup'
171
174
 
172
175
  export const createWireServices = pikkuAddonWireServices(
173
176
  async (singletonServices, wire) => {
@@ -189,13 +192,17 @@ npx pikku new addon <name> # name is a required positional
189
192
  npx pikku new addon stripe --display-name Stripe --category Payments --dir addons
190
193
  ```
191
194
 
195
+ **Handed an OpenAPI or Swagger spec?** Don't hand-write the functions —
196
+ `pikku new addon <name> --openapi <spec>` generates one function per operation,
197
+ with its schemas, service and credential. Read `references/openapi.md`.
198
+
192
199
  This generates `package.json` (exports `.pikku/*` + `dist/`), `pikku.config.json` (`addon: true`), `tsconfig.json` (`#pikku` path mapping), `src/services.ts`, `src/functions/`, and `types/application-types.d.ts`. For the full file contents/exports you rarely hand-edit, read `references/addon-package-manifest.md`.
193
200
 
194
201
  ### Services
195
202
 
196
203
  ```typescript
197
204
  // src/services.ts
198
- import { pikkuAddonServices, pikkuAddonWireServices } from '#pikku/setup'
205
+ import { pikkuAddonServices, pikkuAddonWireServices } from '#pikku/addon/setup'
199
206
  import { TodoStore } from './todo-store.service.js'
200
207
 
201
208
  export const createSingletonServices = pikkuAddonServices(async () => {
@@ -213,18 +220,30 @@ export const createWireServices = pikkuAddonWireServices(
213
220
 
214
221
  ### Functions
215
222
 
216
- An addon's generated tree roots at `.pikku/addon/`, but its `imports` map points
217
- `#pikku/*` there, so it authors against the same subpaths an application does —
218
- `#pikku/function`, `#pikku/http`. The `addon` segment is the package's own
219
- business, never part of a specifier.
223
+ An addon's generated tree roots at `.pikku/addon/`, and its own source reaches
224
+ it by that path — `#pikku/addon/function`, `#pikku/addon/setup`. An application
225
+ authors against `#pikku/function`; an addon never does, because inside the addon
226
+ `#pikku/function` names a leaf that does not exist.
227
+
228
+ **Declare zod schemas in a file that never imports `#pikku`.** `pikku all` loads
229
+ the file that declares each schema to convert it, and at runtime `#pikku`
230
+ resolves through the package's `imports` — into a `dist` the first build has not
231
+ written yet. Inline, every schema fails with `Could not convert Zod schema …
232
+ Cannot find module …/dist/.pikku/…` and the addon never builds from clean. A
233
+ sibling `<fn>.schemas.ts` is what `--openapi` generates:
220
234
 
221
235
  ```typescript
222
- // src/functions/addTodo.function.ts
236
+ // src/functions/addTodo.schemas.ts
223
237
  import { z } from 'zod'
224
- import { pikkuSessionlessFunc } from '#pikku/function'
225
238
 
226
- const AddTodoInput = z.object({ title: z.string() })
227
- const AddTodoOutput = z.object({ id: z.string(), title: z.string() })
239
+ export const AddTodoInput = z.object({ title: z.string() })
240
+ export const AddTodoOutput = z.object({ id: z.string(), title: z.string() })
241
+ ```
242
+
243
+ ```typescript
244
+ // src/functions/addTodo.function.ts
245
+ import { pikkuSessionlessFunc } from '#pikku/addon/function'
246
+ import { AddTodoInput, AddTodoOutput } from './addTodo.schemas.js'
228
247
 
229
248
  export const addTodo = pikkuSessionlessFunc({
230
249
  description: 'Adds a new todo',
@@ -246,9 +265,7 @@ approvalDescription: async (_services, { title }) => `Add a todo called "${title
246
265
  ### Build
247
266
 
248
267
  ```bash
249
- yarn pikku all # Generate types
250
- yarn tsc # Compile TypeScript
251
- cp -r .pikku types dist/ # Ship the generated files and the types they import
268
+ yarn build # prebuild: pikku all, then tsc && pikku dist
252
269
  yarn pikku validate # Check the published file set holds together
253
270
  ```
254
271
 
@@ -257,12 +274,11 @@ devDependency, and building it against a different CLI than it declares is how
257
274
  generated output ends up disagreeing with the packaged one. `npx pikku new
258
275
  addon` above is the exception — it runs before the addon, and its CLI, exist.
259
276
 
260
- `types/` has to be copied alongside `.pikku`: the generated files import
261
- `SingletonServices`, `Services`, `Config` and `UserSession` from
262
- `../../types/application-types.d.js`, and `tsc` never emits a hand-written
263
- `.d.ts` to `outDir`, so nothing else puts it in `dist`. Leave it out and the
264
- addon installs fine and fails to typecheck in every app that depends on it —
265
- which is what `pikku validate` is there to catch before you publish.
277
+ `pikku dist` copies what `tsc` cannot emit — the generated `*.gen.json` meta and
278
+ the hand-written `types/*.d.ts` the generated files import — to where `tsc` put
279
+ everything else. Leave it out and the addon installs fine and fails to typecheck
280
+ in every app that depends on it, which is what `pikku validate` is there to
281
+ catch before you publish.
266
282
 
267
283
  ### Database tables
268
284
 
@@ -297,7 +313,7 @@ packed, or it never arrives:
297
313
  **An unresolvable artifact stops `db generate`.** Because the file is
298
314
  unconditional, absence means the package cannot say whether it ships tables —
299
315
  either it was built with an older CLI, or `exports`/`files` do not carry it. The
300
- error names both causes. An addon with genuinely no tables is *not* this case:
316
+ error names both causes. An addon with genuinely no tables is _not_ this case:
301
317
  it publishes `{}` and is waved through.
302
318
 
303
319
  Two more loud ones: a malformed artifact (missing the SQL for a dialect it
@@ -343,6 +359,36 @@ export const myFunc = pikkuFunc({
343
359
  })
344
360
  ```
345
361
 
362
+ ### Wrap an addon function only to reshape it
363
+
364
+ A screen that shows the addon's data as the addon returns it calls the addon
365
+ function itself: name it in `wireAddon({ expose: ['listTodos'], auth: true })`
366
+ and the frontend calls `rpc.invoke('todos:listTodos', …)` (over HTTP,
367
+ `POST /rpc/todos:listTodos` with `{ "data": … }`), still behind the session.
368
+ Use `ref('todos:listTodos')` on an HTTP or MCP wiring only when the addon needs
369
+ a route of its own. Don't write an app function that calls `rpc.invoke` and
370
+ returns the result unchanged: it's a second name and a second schema for the
371
+ same thing, and it drifts. `expose: true` exposes only what the addon itself
372
+ declared `expose: true` — an OpenAPI-generated addon declares none, so list
373
+ the names.
374
+
375
+ Write your own function when the app needs the data narrowed, typed, or
376
+ combined (a flag the upstream sends as `"0"`, a total summed from several
377
+ calls, one field out of fifty), or when the app adds a permission of its own.
378
+ `wireAddon`'s `scopes` gate every function in the addon at once, and a wiring
379
+ carries middleware, not permissions, so a rule on one addon function lives in
380
+ the `permissions` of an app function that calls it. An addon called with the
381
+ user's own credential is already limited upstream to what that user may do, so
382
+ a data-aware `pikkuPermission` repeating that check (may they read *this*
383
+ invoice?) adds nothing. A session-only `pikkuAuth` (a role, a tier) is still
384
+ worth it: it can be checked before any input exists, so the functions a user
385
+ can't call drop out of the tools an MCP client, an agent or a workflow is
386
+ offered, instead of failing upstream when called. Name it for what the screen means
387
+ (`getMyProfile`), not after the upstream operation (`usersRetrieveInfo`), and
388
+ give it an `output:` schema of only what the app uses. For an OpenAPI-generated
389
+ addon this matters more: its outputs mirror the upstream's loose, oversized
390
+ payloads, and the wrapper is where they become the app's own shape.
391
+
346
392
  ### Wire to HTTP
347
393
 
348
394
  ```typescript
@@ -22,7 +22,7 @@ An addon's generated tree roots one level down, at `.pikku/addon/`, so its own
22
22
  leaves are reached as `#pikku/addon/<leaf>` while an application's are
23
23
  `#pikku/<leaf>`. `paths` are global to a tsx process rather than scoped to the
24
24
  package that declared them, and the extra segment is what stops a linked addon's
25
- `#pikku/function` from matching the *host application's* flat leaf.
25
+ `#pikku/function` from matching the _host application's_ flat leaf.
26
26
 
27
27
  ## pikku.config.json
28
28
 
@@ -68,20 +68,25 @@ package that declared them, and the extra segment is what stops a linked addon's
68
68
  "scripts": {
69
69
  "prebuild": "pikku all",
70
70
  "pikku": "pikku all",
71
- "build": "tsc && cp -r .pikku types dist/"
71
+ "build": "tsc && pikku dist"
72
72
  }
73
73
  }
74
74
  ```
75
75
 
76
76
  **`imports` names `dist`, never the source tree.** `files: ["dist"]` is the whole
77
- published package, and `build` copies `.pikku` and `types` into it — so a
77
+ published package, and `pikku dist` copies `.pikku` and `types` into it — so a
78
78
  `#pikku/*` target under `./.pikku/` resolves for the author and for nobody else.
79
79
  It is a silent break: the addon compiles, packs, installs and then throws
80
80
  `Cannot find module '.../.pikku/addon/function/index.ts'` on first import in the
81
- consuming app, out of a file the consumer never wrote. The addon's own build
81
+ consuming app, out of a file the consumer never wrote. The addon's own `tsc`
82
82
  does not read `imports` at all — tsconfig `paths` covers it, which is why the
83
83
  two maps point at different trees.
84
84
 
85
+ `pikku all` does read `imports`, though: it loads each file that declares a zod
86
+ schema to convert it to JSON Schema, and at runtime `#pikku` resolves through
87
+ `imports`, into a `dist` the first build has not written yet. So a schema must
88
+ live in a file that never imports `#pikku` — see "Functions" in the skill.
89
+
85
90
  **`exports` targets carry the `addon` segment; the subpaths do not.** A consumer
86
91
  writes `@my-org/addon-todos/.pikku/rpc/...`, exactly as it would in an
87
92
  application, and the leaf stays the package's own business.
@@ -0,0 +1,130 @@
1
+ # An addon from an OpenAPI spec
2
+
3
+ When you are handed an OpenAPI or Swagger spec — a file, or a URL to one — the
4
+ API it describes becomes an addon: one function per operation, each with its
5
+ input and output schemas, behind one service that makes the HTTP calls. Say so
6
+ before starting ("This is an OpenAPI spec — I'll turn it into an addon first"),
7
+ then generate it. Don't hand-write the functions, and don't ask which
8
+ operations to keep: generate the whole spec, however large.
9
+
10
+ ## Recognising one
11
+
12
+ A JSON or YAML document with a top-level `openapi` key (3.x) or `swagger` key
13
+ (2.0), and a `paths` object. A URL ending in `openapi.json`, `swagger.json` or
14
+ `.yaml` is almost always one; open it and check the key before generating.
15
+
16
+ ## 1 — Look at the spec first
17
+
18
+ `--openapi` takes a path or a URL. A spec published only to signed-in callers
19
+ takes the key the way the API reads it: `--openapi-header "NAME: value"`
20
+ (repeatable), or in the URL's query string when the API reads it there
21
+ (Dolibarr's explorer takes `?DOLAPIKEY=`). A 401 while fetching says which.
22
+
23
+ The generator warns loudly when the spec has fewer than five operations, or only
24
+ auth routes. That is almost always the public half of a spec that shows more
25
+ to an authenticated caller — fetch it again with the key, don't build on it.
26
+
27
+ Keep a copy in `specs/` as the record of what the addon was generated from.
28
+
29
+ ## 2 — Generate, from the app's root
30
+
31
+ ```bash
32
+ bunx --bun pikku new addon <name> --openapi <path-or-url>
33
+ ```
34
+
35
+ One command. Inside an app it writes `packages/addon-<name>` as
36
+ `@pikku/addon-<name>`, then installs it into the app:
37
+
38
+ - the dependency in the root and the functions `package.json`
39
+ - `src/addons/<name>.addon.ts` — `wireAddon` with `auth: true` and an explicit
40
+ `expose` list (every operation in per-user modes, only the `GET`s behind a
41
+ shared secret)
42
+ - the auth wiring in `src/auth.ts` for the chosen mode
43
+ - `<NAME>_BASE_URL` in `.env`
44
+
45
+ and then runs install and the addon's build. `--no-install` generates the
46
+ package alone.
47
+
48
+ | Flag | When |
49
+ | -------------------------------------------- | -------------------------------------------------------------------------------- |
50
+ | `--auth user` (default) | Each user brings their own credential |
51
+ | `--auth shared` | One secret behind every user; locally it goes in `.env` |
52
+ | `--auth none` | The API really takes no auth |
53
+ | `--credential apikey\|bearer\|basic\|oauth2` | Override what the spec's `securitySchemes` declares |
54
+ | `--auth-config <file>` | Users sign in with their upstream login, or the spec gets auth wrong |
55
+ | `--tags a,b` / `--include` / `--exclude` | Keep part of a huge spec: tags, or globs on operationId, `/path`, `METHOD /path` |
56
+ | `--mcp` | The operations should also be MCP tools |
57
+ | `--camel-case` | The API's property names are snake_case and the app's are not |
58
+
59
+ The mode comes from the spec unless a flag says otherwise. A spec with no
60
+ machine-readable auth is refused rather than guessed: pass one of the flags the
61
+ error names. Which mode fits, and the auth-config format, are in the
62
+ `pikku-build` skill's `references/openapi.md`.
63
+
64
+ ## 3 — Check what was generated
65
+
66
+ ```
67
+ packages/addon-<name>/
68
+ ├── <name>.svg # placeholder icon; replace with the real one
69
+ ├── src/<name>-api.service.ts # one fetch wrapper, reads <NAME>_BASE_URL
70
+ ├── src/<name>.variable.ts # <NAME>_BASE_URL: z.string().url(), the first server as default
71
+ ├── src/functions/<op>.function.ts
72
+ ├── src/functions/<op>.schemas.ts # the op's zod schemas — never import #pikku here
73
+ └── src/index.ts # re-exports every function
74
+ ```
75
+
76
+ An operation whose spec gives no response, or one too vague to validate
77
+ against, outputs `z.unknown()`. Tighten it in `<op>.schemas.ts` once §6 shows
78
+ what the API really returns.
79
+
80
+ An upstream 401 on a per-user credential throws `CredentialRejectedError`
81
+ (403, `reauth: 'sign-in' | 'connect'`); a UI shows the matching screen again
82
+ rather than a generic error.
83
+
84
+ ## 4 — Call it from the app
85
+
86
+ Operations are reached by reference — `ref('<name>:<operationFn>')` in a
87
+ workflow, agent tool or HTTP wiring — the same way as any other addon. See
88
+ "Consuming an Addon" in the skill. An exposed operation is also callable from
89
+ the frontend at `POST /rpc/<name>:<operationFn>` with a body of
90
+ `{ "data": { … } }`, as the signed-in user.
91
+
92
+ ## 5 — Verify
93
+
94
+ ```bash
95
+ bunx --bun pikku all
96
+ bunx --bun pikku validate
97
+ ```
98
+
99
+ `Could not convert Zod schema … Cannot find module …/dist/.pikku/…` means a
100
+ schema is declared in a file that imports `#pikku`. The generator never does
101
+ that, so a hand edit moved it — put it back in `<op>.schemas.ts`.
102
+
103
+ `pikku validate` reports a `skewed-type-identity` error (and `pikku all` warns
104
+ `[PKU719]`) when the CLI and the app resolve different copies of `zod` or
105
+ another shared package. Codegen then reads the app's
106
+ schemas with the wrong copy and fails on schemas that are correct. Pin one
107
+ version for the whole install, as the finding says, and reinstall.
108
+
109
+ ## 6 — Check the spec against the real API
110
+
111
+ Specs are often wrong, and the generated schemas repeat every mistake. Before
112
+ building on the addon:
113
+
114
+ - **Call the reads you can.** The `GET`s the credential can reach, following ids
115
+ from lists into retrieves. Writes only if the user opts in.
116
+ - **Fix the addon, not the app**: the schema in the op's `<op>.schemas.ts`, or the
117
+ request shape in `src/<name>-api.service.ts`. Then rebuild it.
118
+ - **List each mismatch in `packages/addon-<name>/SPEC-ISSUES.md`**: a title and a
119
+ short description. No credentials or customer data.
120
+
121
+ Then tell the user in one line: "FYI, the spec deviates from the real API in
122
+ N ways: [SPEC-ISSUES.md](…)". Add to the file whenever a later call disagrees
123
+ with its schema. Ask before sending it to the API's maintainers, because an issue
124
+ on their tracker is a public post.
125
+
126
+ ## Then
127
+
128
+ Go back to the mode you were building in (`pikku-build`). The addon is a
129
+ dependency of the app, not the app: plan milestones around what the user wants
130
+ to do with the API, and reach the operations through `ref()`.
@@ -35,7 +35,9 @@ pikkuAgent({
35
35
  temperature?: number,
36
36
  providerOptions?: { // passed through untouched, keyed by provider
37
37
  openai?: { reasoningEffort?: 'minimal' | ... },
38
- },
38
+ }, // inline literals only — a computed value
39
+ // cannot be read into the generated metadata
40
+ // and is reported as PKU156
39
41
 
40
42
  // --- capabilities: all three take ref() handles, not imported values ---
41
43
  tools?: unknown[], // ref('todos:addTodo'), ref('graph:sleep'), …
@@ -1,5 +1,21 @@
1
1
  # Pikku Better Auth Integration
2
2
 
3
+ Every section, in the order it is usually needed:
4
+
5
+ - [⚠️ MANDATORY RULE — READ FIRST](#mandatory-rule--read-first)
6
+ - [Installation](#installation)
7
+ - [Core Concepts](#core-concepts)
8
+ - [Standard Setup](#standard-setup)
9
+ - [⚠️ Stateless session — ALWAYS enable `cookieCache` for deployed apps](#stateless-session--always-enable-cookiecache-for-deployed-apps)
10
+ - [Social Providers needing extra config](#social-providers-needing-extra-config)
11
+ - [Auth-Protected Functions](#auth-protected-functions)
12
+ - [HTTP surface (call the real endpoints)](#http-surface-call-the-real-endpoints)
13
+ - [Secret Management](#secret-management)
14
+ - [`pikkuBetterAuth` API](#pikkubetterauth-api)
15
+ - [Post-signup side effects](#post-signup-side-effects)
16
+ - [Two-factor (2FA / MFA)](#two-factor-2fa--mfa)
17
+ - [Security hardening](#security-hardening)
18
+
3
19
  ## ⚠️ MANDATORY RULE — READ FIRST
4
20
 
5
21
  **ALL authentication in Pikku apps MUST use `@pikku/better-auth`. No exceptions.**
@@ -291,7 +307,7 @@ singleton a 403 that leaves no platform user behind.
291
307
 
292
308
  ```typescript
293
309
  pikkuDelegatedAuth({
294
- authenticate: async ({ email, password, apiKey }) => upstream.login(...),
310
+ authenticate: async ({ login, email, password, apiKey }) => upstream.login(...),
295
311
  storeCredential: (userId, identity) =>
296
312
  credentialService.set('acme', identity.credential, userId),
297
313
  defaultRole: 'member',
@@ -302,7 +318,10 @@ pikkuDelegatedAuth({
302
318
  ```
303
319
 
304
320
  `POST /sign-in/delegated` forwards the credentials the user already has to
305
- `authenticate`. On success it JIT-provisions a real user row (email-keyed and
321
+ `authenticate`. The body takes `email`, `login` or `username` with `password`
322
+ (or `apiKey`); whichever identifier was sent reaches `authenticate` as
323
+ `credentials.login`, and `email` as well when it was one. Upstreams that sign in
324
+ with a username — most ERPs — need nothing more. On success it JIT-provisions a real user row (email-keyed and
306
325
  `emailVerified` — the upstream just verified them), links it via an `account`
307
326
  row (`providerId: 'delegated'`, `accountId: externalId`), persists the upstream
308
327
  token **before** minting the session, and returns a normal session cookie.
@@ -317,6 +336,18 @@ a warning and the user still gets in.
317
336
  `storeCredential` failing, by contrast, **fails the sign-in**: every proxied
318
337
  call would be dead anyway.
319
338
 
339
+ An upstream user with no email gets one made up from the login, and the
340
+ identity says so with `syntheticEmail: true`. A made-up address never links to
341
+ an existing user row, so it cannot take over someone else's account.
342
+
343
+ For an addon generated from an OpenAPI spec, none of this is written by hand:
344
+ `pikku new addon --openapi … --auth-config <file>` generates
345
+ `authenticate<Name>Upstream` in the addon and wires this plugin, the stored
346
+ credential and the actor credentials into `src/auth.ts`. The config format is
347
+ in the `pikku-build` skill's `references/openapi.md`. When the upstream later
348
+ refuses the stored token, the addon throws `CredentialRejectedError` (403,
349
+ `reauth: 'sign-in'`): the UI shows the sign-in again.
350
+
320
351
  #### `pikkuFabric()` — control-plane operator sign-in
321
352
 
322
353
  ```typescript