@pikku/skills 0.12.34 → 0.12.37

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 (43) 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 +41 -30
  10. package/skills/pikku-addon/references/addon-package-manifest.md +9 -4
  11. package/skills/pikku-addon/references/openapi.md +99 -0
  12. package/skills/pikku-agent/references/agents.md +3 -1
  13. package/skills/pikku-architect/SKILL.md +12 -0
  14. package/skills/pikku-auth/references/better-auth.md +16 -0
  15. package/skills/pikku-build/SKILL.md +29 -0
  16. package/skills/pikku-build/references/app.md +52 -4
  17. package/skills/pikku-build/references/feature.md +23 -96
  18. package/skills/pikku-build/references/quick.md +12 -3
  19. package/skills/pikku-changes/SKILL.md +172 -0
  20. package/skills/pikku-concepts/SKILL.md +33 -138
  21. package/skills/pikku-concepts/references/bootstrap.md +58 -0
  22. package/skills/pikku-concepts/references/concept-mapping.md +16 -0
  23. package/skills/pikku-concepts/references/language.md +87 -0
  24. package/skills/pikku-deploy/SKILL.md +1 -1
  25. package/skills/pikku-fabric/SKILL.md +26 -13
  26. package/skills/pikku-guide/SKILL.md +264 -0
  27. package/skills/pikku-knowledge/SKILL.md +10 -0
  28. package/skills/pikku-kysely/SKILL.md +1 -1
  29. package/skills/pikku-mantine/SKILL.md +80 -0
  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 -563
  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 +87 -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-wiring/references/http.md +8 -0
  42. package/skills/pikku-wiring/references/mcp.md +59 -0
  43. 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.34",
3
+ "version": "0.12.37",
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.
@@ -47,7 +49,8 @@ wireAddon({
47
49
  package: string, // NPM package name (e.g. '@pikku/addon-todos')
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
- mcp?: boolean,
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
@@ -324,11 +340,6 @@ wireAddon({ name: 'todos', package: '@my-org/addon-todos' })
324
340
 
325
341
  After registration, run `yarn pikku all` to generate types for the addon's functions.
326
342
 
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
343
  If the addon ships tables, `pikku db generate` then writes one migration per
333
344
  addon — named after the package, carrying the addon's own SQL — after Better
334
345
  Auth's and the runtime's, so an addon table may reference `user` or a runtime
@@ -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,99 @@
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 — Put the spec in the repo
17
+
18
+ `--openapi` reads a local path, not a URL. Download it to `specs/`, where it
19
+ stays as the record of what the addon was generated from:
20
+
21
+ ```bash
22
+ mkdir -p specs
23
+ curl -fsSL <url> -o specs/<name>.openapi.json
24
+ ```
25
+
26
+ ## 2 — Generate
27
+
28
+ Inside an app, the addon is a workspace package under `packages/`:
29
+
30
+ ```bash
31
+ bunx --bun pikku new addon <name> --openapi specs/<name>.openapi.json --dir packages
32
+ ```
33
+
34
+ This writes `packages/addon-<name>` as `@pikku/addon-<name>`, installs it and
35
+ builds it. Inside a workspace the generated test app depends on it as
36
+ `workspace:*`, so nothing is published.
37
+
38
+ | Flag | When |
39
+ | ----------------------------- | ----------------------------------------------------------------------------------------- |
40
+ | `--credential apikey\|bearer` | The spec's `securitySchemes` is an API key or a bearer token — each user brings their own |
41
+ | `--credential oauth2` | The spec's `securitySchemes` is OAuth2 |
42
+ | `--auth-config <file>` | The spec gets auth wrong or leaves it out: a custom header, a delegated login endpoint |
43
+ | `--mcp` | The operations should also be MCP tools |
44
+ | `--camel-case` | The API's property names are snake_case and the app's are not |
45
+
46
+ Read the spec's `securitySchemes` to pick the credential. Leave the flag off
47
+ only when the API really takes no auth.
48
+
49
+ ## 3 — Check what was generated
50
+
51
+ ```
52
+ packages/addon-<name>/
53
+ ├── src/<name>-api.service.ts # one fetch wrapper, reads <NAME>_BASE_URL
54
+ ├── src/<name>.variable.ts # <NAME>_BASE_URL, an enum of the spec's servers
55
+ ├── src/functions/<op>.function.ts
56
+ ├── src/functions/<op>.schemas.ts # the op's zod schemas — never import #pikku here
57
+ └── src/index.ts # re-exports every function
58
+ ```
59
+
60
+ `<NAME>_BASE_URL` is an enum of the spec's `servers`. When those are
61
+ placeholders or a per-tenant host (`https://{tenant}.example.com`, or a server
62
+ list that is only an example), change its schema in `src/<name>.variable.ts`
63
+ to `z.string().url()` so each deployment sets its own.
64
+
65
+ ## 4 — Wire it into the app
66
+
67
+ ```typescript
68
+ // src/addons.ts
69
+ import { wireAddon } from '#pikku/addon'
70
+
71
+ wireAddon({ name: '<name>', package: '@pikku/addon-<name>' })
72
+ ```
73
+
74
+ Then call operations by reference — `ref('<name>:<operationFn>')` in a
75
+ workflow, agent tool or HTTP wiring — the same way as any other addon. See
76
+ "Consuming an Addon" in the skill.
77
+
78
+ ## 5 — Verify
79
+
80
+ ```bash
81
+ bunx --bun pikku all
82
+ bunx --bun pikku validate
83
+ ```
84
+
85
+ `Could not convert Zod schema … Cannot find module …/dist/.pikku/…` means a
86
+ schema is declared in a file that imports `#pikku`. The generator never does
87
+ that, so a hand edit moved it — put it back in `<op>.schemas.ts`.
88
+
89
+ `pikku validate` reports a `skewed-type-identity` error (and `pikku all` warns
90
+ `[PKU719]`) when the CLI and the app resolve different copies of `zod` or
91
+ another shared package. Codegen then reads the app's
92
+ schemas with the wrong copy and fails on schemas that are correct. Pin one
93
+ version for the whole install, as the finding says, and reinstall.
94
+
95
+ ## Then
96
+
97
+ Go back to the mode you were building in (`pikku-build`). The addon is a
98
+ dependency of the app, not the app: plan milestones around what the user wants
99
+ 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'), …
@@ -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
@@ -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.**
@@ -13,6 +13,17 @@ 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
16
27
  ---
17
28
 
18
29
  # Build on Pikku
@@ -51,6 +62,24 @@ generated code depends on, and on a fresh scaffold **every command that touches
51
62
  codegen fails until it has run**, including ones you would reasonably reach for
52
63
  while still planning. Those failures look alarming and are nothing but this.
53
64
 
65
+ ## Start from what you were handed
66
+
67
+ When the request comes with a file or a URL, look at it before planning
68
+ anything. Two kinds are converted first and then built on:
69
+
70
+ | Handed | Say, then do |
71
+ | ------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
72
+ | An **OpenAPI / Swagger spec** — top-level `openapi` or `swagger` key, a `paths` object | "This is an OpenAPI spec — I'll turn it into an addon first." Follow the `pikku-addon` skill's OpenAPI reference. |
73
+ | An **n8n export** — an object with `nodes` and `connections`, an array of them, or a `{ workflows: [...] }` wrapper | "This is an n8n workflow — I'll import it first." Follow `pikku-n8n-import`. |
74
+
75
+ Say it at once, in one line, and start: this is the obvious first move, not a
76
+ question for the user. Generate the whole spec, however large.
77
+
78
+ Neither is the app. When the conversion compiles, come back here and carry on
79
+ in the mode the request calls for — App by default — planning milestones around
80
+ what the user wants to do with the API or the workflow, and reaching the
81
+ generated functions through `ref()`.
82
+
54
83
  ## What holds in every mode
55
84
 
56
85
  - **The branch and the diff are the contract.** A reviewer sees real, compiled,