@pikku/skills 0.12.10 → 0.12.11

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 (60) hide show
  1. package/CHANGELOG.md +768 -0
  2. package/dist/skills.gen.js +1 -1
  3. package/package.json +4 -4
  4. package/skills/pikku-addon/SKILL.md +17 -11
  5. package/skills/pikku-addon/references/addon-package-manifest.md +2 -2
  6. package/skills/pikku-agent/SKILL.md +3 -3
  7. package/skills/pikku-audit/SKILL.md +28 -13
  8. package/skills/pikku-aws/SKILL.md +2 -2
  9. package/skills/pikku-better-auth/SKILL.md +97 -17
  10. package/skills/pikku-build-app/SKILL.md +621 -0
  11. package/skills/pikku-build-app/references/multi-app.md +117 -0
  12. package/skills/pikku-build-app/references/ship.md +98 -0
  13. package/skills/pikku-build-app/references/theming.md +70 -0
  14. package/skills/pikku-build-platform/SKILL.md +239 -0
  15. package/skills/pikku-build-quick/SKILL.md +238 -0
  16. package/skills/pikku-cli/SKILL.md +7 -7
  17. package/skills/pikku-cli/references/complete-example.md +1 -1
  18. package/skills/pikku-concepts/SKILL.md +5 -2
  19. package/skills/pikku-concepts/references/concept-mapping.md +1 -1
  20. package/skills/pikku-config/SKILL.md +5 -3
  21. package/skills/pikku-deploy-azure/SKILL.md +5 -4
  22. package/skills/pikku-emails/SKILL.md +5 -5
  23. package/skills/pikku-fabric/SKILL.md +27 -3
  24. package/skills/pikku-fabric-debug/SKILL.md +1 -1
  25. package/skills/pikku-feature/SKILL.md +5 -4
  26. package/skills/pikku-http/SKILL.md +4 -4
  27. package/skills/pikku-http/references/http-options.md +13 -13
  28. package/skills/pikku-i18n/SKILL.md +2 -1
  29. package/skills/pikku-info/SKILL.md +1 -1
  30. package/skills/pikku-knowledge/SKILL.md +13 -13
  31. package/skills/pikku-mcp/SKILL.md +4 -4
  32. package/skills/pikku-middleware/SKILL.md +5 -5
  33. package/skills/pikku-middleware/references/middleware-patterns.md +14 -8
  34. package/skills/pikku-n8n-import/SKILL.md +12 -12
  35. package/skills/pikku-n8n-import/SPEC.md +3 -0
  36. package/skills/pikku-n8n-import/references/code-translation.md +26 -22
  37. package/skills/pikku-n8n-import/references/loops-and-control.md +7 -7
  38. package/skills/pikku-paraglide/SKILL.md +11 -6
  39. package/skills/pikku-permissions/SKILL.md +19 -15
  40. package/skills/pikku-product-second-opinion/README.md +3 -3
  41. package/skills/pikku-product-second-opinion/example/sample-report.md +15 -12
  42. package/skills/pikku-product-second-opinion/references/report-template.md +10 -7
  43. package/skills/pikku-queue/SKILL.md +1 -1
  44. package/skills/pikku-react/SKILL.md +53 -13
  45. package/skills/pikku-realtime/SKILL.md +51 -19
  46. package/skills/pikku-rtl/SKILL.md +1 -1
  47. package/skills/pikku-scenario/SKILL.md +126 -14
  48. package/skills/pikku-schedule/SKILL.md +6 -1
  49. package/skills/pikku-schema-ajv/SKILL.md +2 -2
  50. package/skills/pikku-schema-cfworker/SKILL.md +1 -1
  51. package/skills/pikku-security/SKILL.md +9 -5
  52. package/skills/pikku-services/SKILL.md +27 -18
  53. package/skills/pikku-services/references/audit-wire-service.md +14 -8
  54. package/skills/pikku-software-archaeology/scripts/validate.mjs +146 -69
  55. package/skills/pikku-tag-middleware/SKILL.md +1 -0
  56. package/skills/pikku-template-clone/SKILL.md +2 -1
  57. package/skills/pikku-trigger/SKILL.md +3 -3
  58. package/skills/pikku-websocket/SKILL.md +4 -3
  59. package/skills/pikku-workflow/SKILL.md +2 -2
  60. package/skills/pikku-workflow/references/workflow-reference.md +24 -10
@@ -74,7 +74,7 @@ returns MCP content blocks (`{ type: 'text', text }` or `{ type: 'image', data }
74
74
  with base64), so the assistant reads prose rather than raw JSON:
75
75
 
76
76
  ```typescript
77
- import { pikkuMCPToolFunc } from '#pikku'
77
+ import { pikkuMCPToolFunc } from '#pikku/mcp'
78
78
 
79
79
  export const createTodoTool = pikkuMCPToolFunc({
80
80
  description: 'Create a todo item with title, priority, due date and tags',
@@ -96,7 +96,7 @@ presentation layer over logic that is already tested and reachable over HTTP.
96
96
  ### Resources
97
97
 
98
98
  ```typescript
99
- import { pikkuMCPResourceFunc } from '#pikku'
99
+ import { pikkuMCPResourceFunc } from '#pikku/mcp'
100
100
 
101
101
  export const getTodoResource = pikkuMCPResourceFunc<{ id: string }>(
102
102
  async (_services, { id }, { rpc, mcp }) => {
@@ -117,7 +117,7 @@ it is text only, with no blob variant. `mcp.uri` is the concrete URI the client
117
117
  asked for, which is why each entry echoes it back.
118
118
 
119
119
  ```typescript
120
- import { wireMCPResource } from '#pikku'
120
+ import { wireMCPResource } from '#pikku/mcp'
121
121
 
122
122
  wireMCPResource({
123
123
  uri: 'todos/{id}', // URI template
@@ -136,7 +136,7 @@ than handing the function an `undefined`.
136
136
  ### Prompts
137
137
 
138
138
  ```typescript
139
- import { pikkuMCPPromptFunc, wireMCPPrompt } from '#pikku'
139
+ import { pikkuMCPPromptFunc, wireMCPPrompt } from '#pikku/mcp'
140
140
 
141
141
  export const planDayPrompt = pikkuMCPPromptFunc({
142
142
  input: UserIdInputSchema,
@@ -23,7 +23,7 @@ installGroups: [core]
23
23
  ## The `pikkuMiddleware` Factory
24
24
 
25
25
  ```typescript
26
- import { pikkuMiddleware } from '#pikku'
26
+ import { pikkuMiddleware } from '#pikku/function'
27
27
 
28
28
  // Simple: just a function
29
29
  const myMiddleware = pikkuMiddleware(async (services, wire, next) => {
@@ -54,7 +54,7 @@ The `wire` object gives you:
54
54
  - `wire.getSession()` — read the current session
55
55
  - `wire.session` — the session set so far (may be undefined)
56
56
 
57
- Throw a typed error to abort: `UnauthorizedError`, `ForbiddenError`, etc. from `@pikku/core/errors`.
57
+ Throw a typed error to abort: `UnauthorizedError`, `ForbiddenError`, etc. from `#pikku/error`.
58
58
 
59
59
  ## Scoping: Five Levels
60
60
 
@@ -139,7 +139,7 @@ Tags from the function definition and the wire object are merged — middleware
139
139
  ### Registering Tag Middleware
140
140
 
141
141
  ```typescript
142
- import { addTagMiddleware } from '#pikku'
142
+ import { addTagMiddleware } from '#pikku/function'
143
143
 
144
144
  addTagMiddleware('machine-agent', [machineAgentBearerAuth])
145
145
  ```
@@ -202,8 +202,8 @@ export const getToken = () => _token
202
202
  ```typescript
203
203
  // wirings/http.wiring.ts
204
204
  import { timingSafeEqual } from 'node:crypto'
205
- import { addTagMiddleware, pikkuMiddleware } from '#pikku'
206
- import { UnauthorizedError } from '@pikku/core/errors'
205
+ import { addTagMiddleware, pikkuMiddleware } from '#pikku/function'
206
+ import { UnauthorizedError } from '#pikku/error'
207
207
  import { getToken } from '../lib/host-token.js'
208
208
 
209
209
  const bearerAuth = pikkuMiddleware(async (_services, { http }, next) => {
@@ -31,17 +31,23 @@ export function getServiceRPC(baseUrl: string, token: string): RPCInvoke {
31
31
  ## Session-Setting Middleware
32
32
 
33
33
  ```typescript
34
- const apiKeyAuth = pikkuMiddleware(async ({ kysely }, { http, setSession, session }, next) => {
35
- if (session) return next() // already authenticated
34
+ const apiKeyAuth = pikkuMiddleware(
35
+ async ({ kysely }, { http, setSession, session }, next) => {
36
+ if (session) return next() // already authenticated
36
37
 
37
- const header = http?.request?.header?.('x-api-key')
38
- if (!header) return next()
38
+ const header = http?.request?.header?.('x-api-key')
39
+ if (!header) return next()
39
40
 
40
- const row = await kysely.selectFrom('apiKey').select('userId').where('key', '=', header).executeTakeFirst()
41
- if (row) setSession?.({ userId: row.userId })
41
+ const row = await kysely
42
+ .selectFrom('apiKey')
43
+ .select('userId')
44
+ .where('key', '=', header)
45
+ .executeTakeFirst()
46
+ if (row) setSession?.({ userId: row.userId })
42
47
 
43
- return next()
44
- })
48
+ return next()
49
+ }
50
+ )
45
51
 
46
52
  addTagMiddleware('api-key-auth', [apiKeyAuth])
47
53
  ```
@@ -59,13 +59,13 @@ than the exit code to know what landed. Relay every skipped workflow to the user
59
59
  Every unmapped node is a stub that throws `… — implement me`. Classify each by its
60
60
  JSDoc marker and route to the matching reference:
61
61
 
62
- | Stub marker / signal | Handle via |
63
- |---|---|
64
- | `STUB — generated from n8n node "…" (type "n8n-nodes-base.<svc>…")` | `references/addon-mapping.md` |
65
- | `STUB — generated from n8n Code node "…"` | `references/code-translation.md` |
66
- | A `control` stub — Loop Over Items / **splitInBatches**, Switch expr-mode | `references/loops-and-control.md` |
67
- | `STUB — … vector-store … #902` | rare now (RAG ships as `<store>:query`/`:ingest`); a residual one = an unmapped store → report it, don't guess |
68
- | Importer `diagnostics` (already exited 1) | explain the reason; the workflow is un-importable as-is |
62
+ | Stub marker / signal | Handle via |
63
+ | ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
64
+ | `STUB — generated from n8n node "…" (type "n8n-nodes-base.<svc>…")` | `references/addon-mapping.md` |
65
+ | `STUB — generated from n8n Code node "…"` | `references/code-translation.md` |
66
+ | A `control` stub — Loop Over Items / **splitInBatches**, Switch expr-mode | `references/loops-and-control.md` |
67
+ | `STUB — … vector-store … #902` | rare now (RAG ships as `<store>:query`/`:ingest`); a residual one = an unmapped store → report it, don't guess |
68
+ | Importer `diagnostics` (already exited 1) | explain the reason; the workflow is un-importable as-is |
69
69
 
70
70
  Read a reference file only when you actually hit that stub class.
71
71
 
@@ -99,11 +99,11 @@ Missing integrations — install these or the nodes stay stubs:
99
99
 
100
100
  ## References
101
101
 
102
- | Open when you need to… | Read |
103
- |---|---|
104
- | map an integration stub (gmailTool, slackTool, googleSheets, plain action nodes) to an installed addon `ref(...)` | `references/addon-mapping.md` |
105
- | translate an n8n Code node body into a Pikku function body | `references/code-translation.md` |
106
- | lower a Loop Over Items / splitInBatches loop, or a Switch that stayed a stub | `references/loops-and-control.md` |
102
+ | Open when you need to… | Read |
103
+ | ----------------------------------------------------------------------------------------------------------------- | --------------------------------- |
104
+ | map an integration stub (gmailTool, slackTool, googleSheets, plain action nodes) to an installed addon `ref(...)` | `references/addon-mapping.md` |
105
+ | translate an n8n Code node body into a Pikku function body | `references/code-translation.md` |
106
+ | lower a Loop Over Items / splitInBatches loop, or a Switch that stayed a stub | `references/loops-and-control.md` |
107
107
 
108
108
  ## Final summary
109
109
 
@@ -11,6 +11,7 @@ remainder with judgment, report gaps that need a human decision, and verify.
11
11
  ## Scope
12
12
 
13
13
  In scope:
14
+
14
15
  - Running `pikku import n8n` and triaging its output.
15
16
  - Filling integration stubs (→ addon refs), Code stubs (→ function bodies), and
16
17
  loop/control stubs (→ `graph:map`/reduce/branch).
@@ -18,6 +19,7 @@ In scope:
18
19
  - Verifying via `pikku all` + `tsc` + a zero-surviving-stub check.
19
20
 
20
21
  Out of scope:
22
+
21
23
  - Extending `@pikku/n8n-import` itself (it is frozen; do not add per-service tables
22
24
  or new compiler rules to it).
23
25
  - Authoring workflows from scratch (`pikku-workflow`) or hand-written addon wiring
@@ -47,6 +49,7 @@ Out of scope:
47
49
  ## Source And Evidence Model
48
50
 
49
51
  Authoritative sources:
52
+
50
53
  - `@pikku/n8n-import` codegen (stub markers, manifest shape, `import-n8n` command).
51
54
  - `@pikku/addon-graph` function contracts (`graph:map`/`fanout`, `branch`).
52
55
  - Installed `@pikku/addon-*` source (function names verified by grep, never guessed).
@@ -20,39 +20,39 @@ refactor, add error handling, or invent fields.
20
20
  3. **Apply the rubric.**
21
21
  4. **Edit only the function body.** Leave imports, schemas, JSDoc, name, description, refs untouched unless step 5 forces it.
22
22
  5. **If the schemas are wrong** (code reads `$json.userId: string` but input is `items: z.array(z.unknown())`), tighten with the smallest change. Prefer `z.unknown()` over `z.any()`. Never widen output to `z.any()`.
23
- 6. **Add one comment** at the top of the body noting the mode: `// translated from n8n Code node, mode: runOnceForAllItems`. This is the *only* comment you may add.
23
+ 6. **Add one comment** at the top of the body noting the mode: `// translated from n8n Code node, mode: runOnceForAllItems`. This is the _only_ comment you may add.
24
24
  7. **Typecheck** (`yarn tsc` from the package root); fix errors with the smallest change.
25
25
 
26
26
  ## Rubric
27
27
 
28
28
  ### Envelope unwrapping — all-items mode
29
29
 
30
- | n8n | Pikku |
31
- |---|---|
32
- | `items` | `(data.items ?? []) as any[]` (or typed if known) |
33
- | `items[i].json.X` | `items[i].X` |
34
- | `items[i].json` | `items[i]` |
35
- | `items[i].binary` | **NOT supported** — leave a TODO and explain |
36
- | `items.length` | `items.length` |
37
- | `items.map(i => i.json.X)` | `items.map((i: any) => i.X)` |
30
+ | n8n | Pikku |
31
+ | -------------------------- | ------------------------------------------------- |
32
+ | `items` | `(data.items ?? []) as any[]` (or typed if known) |
33
+ | `items[i].json.X` | `items[i].X` |
34
+ | `items[i].json` | `items[i]` |
35
+ | `items[i].binary` | **NOT supported** — leave a TODO and explain |
36
+ | `items.length` | `items.length` |
37
+ | `items.map(i => i.json.X)` | `items.map((i: any) => i.X)` |
38
38
 
39
39
  ### Envelope unwrapping — each-item mode
40
40
 
41
- | n8n | Pikku |
42
- |---|---|
43
- | `$json.X` / `$input.item.json.X` | `data.X` (input is the item itself) |
44
- | `$input.item.json` | `data` |
45
- | `$input.all()` | not available per-item — change to all-items mode |
41
+ | n8n | Pikku |
42
+ | -------------------------------- | ------------------------------------------------- |
43
+ | `$json.X` / `$input.item.json.X` | `data.X` (input is the item itself) |
44
+ | `$input.item.json` | `data` |
45
+ | `$input.all()` | not available per-item — change to all-items mode |
46
46
 
47
47
  ### Return statement
48
48
 
49
- | n8n | Pikku |
50
- |---|---|
51
- | `return [{ json: X }]` | `return { items: [X] }` |
52
- | `return items.map(i => ({ json: ... }))` | `return { items: items.map(...) }` |
53
- | `return [{ json: X }, { json: Y }]` | `return { items: [X, Y] }` |
54
- | `return { json: X }` (each-item) | `return X` |
55
- | `return [...]` (already plain) | wrap in `{ items: [...] }` only if the output schema expects it |
49
+ | n8n | Pikku |
50
+ | ---------------------------------------- | --------------------------------------------------------------- |
51
+ | `return [{ json: X }]` | `return { items: [X] }` |
52
+ | `return items.map(i => ({ json: ... }))` | `return { items: items.map(...) }` |
53
+ | `return [{ json: X }, { json: Y }]` | `return { items: [X, Y] }` |
54
+ | `return { json: X }` (each-item) | `return X` |
55
+ | `return [...]` (already plain) | wrap in `{ items: [...] }` only if the output schema expects it |
56
56
 
57
57
  ### Built-ins — do NOT auto-translate
58
58
 
@@ -80,6 +80,7 @@ the first param).
80
80
  ## Example
81
81
 
82
82
  Before (stub):
83
+
83
84
  ```ts
84
85
  /**
85
86
  * STUB — generated from n8n Code node "Custom Code".
@@ -90,12 +91,15 @@ export const codeStubCustomCode = pikkuSessionlessFunc({
90
91
  input: CodeStubCustomCodeInput,
91
92
  output: CodeStubCustomCodeOutput,
92
93
  func: async (_services, _data) => {
93
- throw new Error('Stub: ported from n8n Code node "Custom Code" — implement me')
94
+ throw new Error(
95
+ 'Stub: ported from n8n Code node "Custom Code" — implement me'
96
+ )
94
97
  },
95
98
  })
96
99
  ```
97
100
 
98
101
  After:
102
+
99
103
  ```ts
100
104
  export const codeStubCustomCode = pikkuSessionlessFunc({
101
105
  description: 'Ported from n8n Code node "Custom Code"',
@@ -32,11 +32,11 @@ predecessor and any `$('<loop node>')` become `$item`.
32
32
 
33
33
  ### Decide the shape first
34
34
 
35
- | Loop body does… | Emit |
36
- |---|---|
37
- | transform each item independently (enrich, format, call one thing) | `graph:map` — child = the body |
38
- | accumulate across items (running total, build one object/array) | a **reduce**: a single generated function over the whole array, not a map — `graph:map` collects per-item results and *loses* the accumulator |
39
- | pure side-effect per item, nothing downstream consumes results | `graph:map` with **no `next`** (done branch empty) — the safest, unambiguous case |
35
+ | Loop body does… | Emit |
36
+ | ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
37
+ | transform each item independently (enrich, format, call one thing) | `graph:map` — child = the body |
38
+ | accumulate across items (running total, build one object/array) | a **reduce**: a single generated function over the whole array, not a map — `graph:map` collects per-item results and _loses_ the accumulator |
39
+ | pure side-effect per item, nothing downstream consumes results | `graph:map` with **no `next`** (done branch empty) — the safest, unambiguous case |
40
40
 
41
41
  ### Child arity
42
42
 
@@ -49,8 +49,8 @@ predecessor and any `$('<loop node>')` become `$item`.
49
49
 
50
50
  ### Done-branch semantics (ask if it matters)
51
51
 
52
- n8n's done output is version-dependent: it may carry the *original* items or the
53
- *accumulated* results. `graph:map`'s `next` receives the array of child results.
52
+ n8n's done output is version-dependent: it may carry the _original_ items or the
53
+ _accumulated_ results. `graph:map`'s `next` receives the array of child results.
54
54
  If a downstream node reads that array's shape and the distinction matters, add:
55
55
 
56
56
  ```ts
@@ -18,7 +18,7 @@ Use this as an execution checklist, not reference material.
18
18
  ## The rules that don't change
19
19
 
20
20
  - **Never resolve an enum key dynamically.** No `mKey('status.' + value)`, no `m['enum__status__' + value]()`, no `mExists`/`mList` helpers. Dynamic keys can't be type-checked or tree-shaken. Everything is a static `m.<literal>()` reference, generated into the map.
21
- - **The `enum__<group>__<member>` namespace.** `__` separates the prefix / group / member segments; a single `_` joins words *within* a segment (`enum__booking_status__form_received`). The prefix (`enum`) and separator (`__`) are configurable but leave them at the defaults.
21
+ - **The `enum__<group>__<member>` namespace.** `__` separates the prefix / group / member segments; a single `_` joins words _within_ a segment (`enum__booking_status__form_received`). The prefix (`enum`) and separator (`__`) are configurable but leave them at the defaults.
22
22
  - **Members must be valid JS identifiers.** Spell out leading digits — `two_guests`, not `2_guests`. The generator quotes an invalid member as a fallback but warns you to rename it.
23
23
  - **`asI18n(...)` is only for opaque server data** (names, slugs, ids returned from the API). Never `asI18n()` a hardcoded English string or an enum value — an enum value goes through its label map.
24
24
 
@@ -35,8 +35,8 @@ export type I18nMessage = () => I18nString
35
35
  export type EnumLabel<E extends string> = Record<E, I18nMessage>
36
36
 
37
37
  export const bookingStatus = {
38
- enquiry: m.enum__booking_status__enquiry,
39
- reserved: m.enum__booking_status__reserved,
38
+ enquiry: m.enum__booking_status__enquiry,
39
+ reserved: m.enum__booking_status__reserved,
40
40
  confirmed: m.enum__booking_status__confirmed,
41
41
  } satisfies EnumLabel<BookingStatus>
42
42
  export type BookingStatusKey = keyof typeof bookingStatus
@@ -61,18 +61,20 @@ const items = [{ label: m.common__nav__items__dashboard /* ← reference */ }]
61
61
  The DB column is the real source of truth for what an enum can be. The pikku CLI's db codegen emits a bare unions module — `.pikku/db/enums.gen.ts` — covering **both** Postgres native enums and SQLite `CHECK (col IN ('a','b',…))` constraints:
62
62
 
63
63
  ```ts
64
- export type BookingStatus = 'enquiry' | 'reserved' | 'confirmed' | 'ended' | 'cancelled'
64
+ export type BookingStatus =
65
+ 'enquiry' | 'reserved' | 'confirmed' | 'ended' | 'cancelled'
65
66
  ```
66
67
 
67
68
  Point `@pikku/paraglide` at that file (`enumsFile`) and each catalog group whose member set **exactly matches** a DB enum is typed `satisfies EnumLabel<DbEnum>`. The label map then **is** the reconciliation — no separate assertion:
68
69
 
69
70
  - catalog drops a DB member, or `en.json` is missing the key → `m.enum__…` doesn't exist / `Record<DbEnum,…>` isn't exhaustive → **`tsc` error naming the gap**.
70
71
  - a DB enum with **no** catalog group → `unmatchedDbEnums: 'emit'` (default) generates a label map referencing `enum__<table>_<column>__<member>` keys, so `tsc` tells you exactly which keys to add; `'warn'` only reports it.
71
- - a group with a member the DB lacks (a *derived* UI state, e.g. a `waitlisted` view of a `pending` row) → a drift warning. Make that a **standalone `m.<key>()` message**, not an enum member — the enum group must mirror the DB column exactly.
72
+ - a group with a member the DB lacks (a _derived_ UI state, e.g. a `waitlisted` view of a `pending` row) → a drift warning. Make that a **standalone `m.<key>()` message**, not an enum member — the enum group must mirror the DB column exactly.
72
73
 
73
74
  Labelling an enum that's never rendered costs nothing: Paraglide compiles only the messages actually referenced, so unused labels are tree-shaken away. So label every DB enum; don't add an opt-out.
74
75
 
75
76
  **To make a column an enum**, give it a closed domain in the migration so codegen can see it:
77
+
76
78
  - SQLite: `status TEXT NOT NULL CHECK (status IN ('enquiry','reserved','confirmed'))`
77
79
  - Postgres: a native `CREATE TYPE … AS ENUM (…)` column.
78
80
 
@@ -88,7 +90,10 @@ import { paraglideEnums } from '@pikku/paraglide/vite'
88
90
 
89
91
  export default defineConfig({
90
92
  plugins: [
91
- paraglideVitePlugin({ project: './project.inlang', outdir: './src/paraglide' }),
93
+ paraglideVitePlugin({
94
+ project: './project.inlang',
95
+ outdir: './src/paraglide',
96
+ }),
92
97
  paraglideEnums({
93
98
  catalog: './messages/en.json',
94
99
  outFile: './src/i18n/i18n-enum.gen.ts',
@@ -26,14 +26,14 @@ export const deleteBook = pikkuFunc({
26
26
  await db.deleteBook(bookId)
27
27
  },
28
28
  permissions: {
29
- owner: isBookOwner, // ← authorization here
29
+ owner: isBookOwner, // ← authorization here
30
30
  },
31
31
  })
32
32
 
33
33
  // WRONG — permission check inside func body
34
34
  export const deleteBook = pikkuFunc({
35
35
  func: async ({ db }, { bookId }, { session }) => {
36
- if (!session) throw new UnauthorizedError() // ← never do this
36
+ if (!session) throw new UnauthorizedError() // ← never do this
37
37
  await db.deleteBook(bookId)
38
38
  },
39
39
  })
@@ -54,7 +54,7 @@ Use for checks that read the session but need no request data — and that asser
54
54
  something **beyond** merely having a session (a flag, a tier, a claim).
55
55
 
56
56
  ```typescript
57
- import { pikkuAuth } from '#pikku'
57
+ import { pikkuAuth } from '#pikku/function'
58
58
 
59
59
  // Good: a real gate on the session's contents, not just its existence.
60
60
  export const isVerified = pikkuAuth(
@@ -71,21 +71,21 @@ function already enforces. A function that needs a signed-in user sets
71
71
  ```typescript
72
72
  // WRONG — redundant with auth: true; adds a permission that gates nothing.
73
73
  export const isSignedIn = pikkuAuth(async (_s, session) => !!session)
74
- pikkuFunc({ auth: true, permissions: { signedIn: isSignedIn }, /* ... */ })
74
+ pikkuFunc({ auth: true, permissions: { signedIn: isSignedIn } /* ... */ })
75
75
 
76
76
  // RIGHT — auth: true already requires the session; permissions are for capability.
77
- pikkuFunc({ auth: true, /* ... */ })
77
+ pikkuFunc({ auth: true /* ... */ })
78
78
  ```
79
79
 
80
- A permission answers "*may this user do this?*" (role, ownership, tier) — never
81
- "*is there a session?*".
80
+ A permission answers "_may this user do this?_" (role, ownership, tier) — never
81
+ "_is there a session?_".
82
82
 
83
83
  ### `pikkuPermission(fn)` — Data-Aware Checks
84
84
 
85
85
  Use when authorization depends on the actual request data (e.g., resource ownership).
86
86
 
87
87
  ```typescript
88
- import { pikkuPermission } from '#pikku'
88
+ import { pikkuPermission } from '#pikku/function'
89
89
 
90
90
  export const isBookOwner = pikkuPermission(
91
91
  async ({ db }, { bookId }, { session }) => {
@@ -132,24 +132,24 @@ export const deleteBook = pikkuFunc({
132
132
 
133
133
  ### Global (`addGlobalPermission`) — App-Wide AND Gate
134
134
 
135
- A global permission is an app-wide baseline that **every** function must additionally pass. It is an independent AND gate: it can only ever *narrow* access — it never grants access a function's own `permissions` would deny.
135
+ A global permission is an app-wide baseline that **every** function must additionally pass. It is an independent AND gate: it can only ever _narrow_ access — it never grants access a function's own `permissions` would deny.
136
136
 
137
137
  ```typescript
138
- import { addGlobalPermission } from '#pikku'
138
+ import { addGlobalPermission } from '#pikku/function'
139
139
 
140
140
  addGlobalPermission([isEmployee]) // every function now also requires an employee session
141
141
  ```
142
142
 
143
143
  Multiple `addGlobalPermission` calls accumulate and are AND'd together.
144
144
 
145
- > Wire-, tag-, and HTTP-route-level permissions (`addHTTPPermission`, `addTagPermission`, and a `permissions` field on HTTP/channel/MCP wirings) were **removed in #972**. Permissions now live only on the function definition, plus the optional global gate. Tags are organizational only — use tag/HTTP *middleware* (`addTagMiddleware`, `addHTTPMiddleware`) for cross-cutting request handling, not authorization.
145
+ > Wire-, tag-, and HTTP-route-level permissions (`addHTTPPermission`, `addTagPermission`, and a `permissions` field on HTTP/channel/MCP wirings) were **removed in #972**. Permissions now live only on the function definition, plus the optional global gate. Tags are organizational only — use tag/HTTP _middleware_ (`addTagMiddleware`, `addHTTPMiddleware`) for cross-cutting request handling, not authorization.
146
146
 
147
147
  ## Scopes — the AND Gate Above Permissions
148
148
 
149
149
  Scopes answer "what was this session granted?" before permissions ask "may this
150
150
  user do this to this resource?". They are AND-ed: every scope listed must be
151
151
  held. Because they are checked first and fail closed, a scope can only ever
152
- *narrow* access — it never grants what `permissions` would deny.
152
+ _narrow_ access — it never grants what `permissions` would deny.
153
153
 
154
154
  Declare the scope tree once with `defineScope`. The body is a no-op that
155
155
  tree-shakes away; the CLI reads the call by AST and generates a `ScopeId` union,
@@ -158,7 +158,7 @@ gating on nothing.
158
158
 
159
159
  ```typescript
160
160
  // src/scopes.ts
161
- import { defineScope } from '#pikku'
161
+ import { defineScope } from '#pikku/scopes'
162
162
 
163
163
  defineScope({
164
164
  admin: {
@@ -222,7 +222,11 @@ export const handleStripeWebhook = pikkuSessionlessFunc({
222
222
  permissionsInBody: true,
223
223
  auth: false,
224
224
  func: async ({ stripe }, data, { http }) => {
225
- stripe.webhooks.constructEvent(data.raw, http.request.header('stripe-signature'), secret)
225
+ stripe.webhooks.constructEvent(
226
+ data.raw,
227
+ http.request.header('stripe-signature'),
228
+ secret
229
+ )
226
230
  // ...
227
231
  },
228
232
  })
@@ -240,7 +244,7 @@ declared, inspectable, and reusable.
240
244
 
241
245
  ```typescript
242
246
  // src/permissions.ts
243
- import { pikkuAuth, pikkuPermission } from '#pikku'
247
+ import { pikkuAuth, pikkuPermission } from '#pikku/function'
244
248
 
245
249
  export const isVerified = pikkuAuth(
246
250
  async (_services, session) => !!session?.emailVerified
@@ -11,8 +11,8 @@ Existing repo → pikku-software-archaeology → .knowledge/ blueprint → pikku
11
11
 
12
12
  ## The split from pikku-software-archaeology
13
13
 
14
- - **pikku-software-archaeology** extracts *facts* into `.knowledge/` for a machine (Pikku) to rebuild from. Engineer/generator audience.
15
- - **pikku-product-second-opinion** reads that blueprint and writes an *opinionated report* for a human to decide from. Non-technical audience.
14
+ - **pikku-software-archaeology** extracts _facts_ into `.knowledge/` for a machine (Pikku) to rebuild from. Engineer/generator audience.
15
+ - **pikku-product-second-opinion** reads that blueprint and writes an _opinionated report_ for a human to decide from. Non-technical audience.
16
16
 
17
17
  One extracts; one advises. This skill consumes the other's output — it doesn't re-read the code.
18
18
 
@@ -30,7 +30,7 @@ One extracts; one advises. This skill consumes the other's output — it doesn't
30
30
  4. **Argue improvements in business outcomes** (more reliable / faster / cheaper / safer / easier to hand off), and say whether each is a cheap **rewire** or an expensive **rebuild** — never recommend a rewrite just because the code is messy.
31
31
  5. **Mark confidence.** Certain and "I'd need to check" are different sentences.
32
32
  6. **Cover the frontend and the other ways the app is used** when the blueprint has them — walk the screens as a journey, call out consistency, and flag the custom-logic pieces (charts/tables/editors) as the real work vs the cheap standard pieces. Name the ways the product can be driven (people/web, developers/API+SDK, AI agents/MCP, power users/CLI) — often a genuine strength.
33
- 7. **Give honest technology tradeoffs — both sides.** Every stack bet (framework, auth, hosting, key libraries) gets what-it-buys AND what-it-costs in business terms, tied to the founder's stage/goals. Don't cheerlead, don't trash, and **don't soften the disadvantages**. The app's own bets are derived from the blueprint (`architecture.json`/`integrations.json`/`frontend.json` + the repo's manifest) — never from a list in the skill, because a verdict you could write before reading the blueprint isn't a second opinion. Separately, and only when a rebuild is actually being recommended, the target stack (Pikku, Better Auth, TanStack Start) gets the *same* both-sides treatment with cons first-class — pinning someone's dependency for being pre-1.0 while staying quiet about the replacement being pre-1.0 too is a pitch, not an opinion.
33
+ 7. **Give honest technology tradeoffs — both sides.** Every stack bet (framework, auth, hosting, key libraries) gets what-it-buys AND what-it-costs in business terms, tied to the founder's stage/goals. Don't cheerlead, don't trash, and **don't soften the disadvantages**. The app's own bets are derived from the blueprint (`architecture.json`/`integrations.json`/`frontend.json` + the repo's manifest) — never from a list in the skill, because a verdict you could write before reading the blueprint isn't a second opinion. Separately, and only when a rebuild is actually being recommended, the target stack (Pikku, Better Auth, TanStack Start) gets the _same_ both-sides treatment with cons first-class — pinning someone's dependency for being pre-1.0 while staying quiet about the replacement being pre-1.0 too is a pitch, not an opinion.
34
34
 
35
35
  ## Files
36
36
 
@@ -1,5 +1,6 @@
1
1
  # Your app, in plain English — and where it could get better
2
- *A second opinion on the competitor-tracking system*
2
+
3
+ _A second opinion on the competitor-tracking system_
3
4
 
4
5
  > Worked example for the pikku-product-second-opinion skill. Shows the voice and the
5
6
  > layered structure on one real area (competitor tracking), drawn from a
@@ -18,17 +19,18 @@ websites, spots meaningful changes (pricing, hiring, product updates), summarize
18
19
  them, and feeds your briefings and dashboards so your team knows first.
19
20
 
20
21
  **The headline.**
22
+
21
23
  - The hard part — reading messy websites and telling a real change from noise — is built well.
22
- - Until recently the app wasn't re-checking sites on its own at all *(now fixed)*.
24
+ - Until recently the app wasn't re-checking sites on its own at all _(now fixed)_.
23
25
  - When it does spot a change, the follow-up work only happens if someone clicks a button — so your dashboards can quietly go stale while looking current.
24
26
 
25
27
  **If it were me, this is the order I'd tackle things:**
26
28
 
27
- | Fix | Why it matters to you | Effort | Payoff |
28
- |---|---|---|---|
29
- | Turn on automatic checking | Sites weren't refreshing themselves | *Done* | High |
30
- | Make the follow-up automatic | Stops your intelligence going stale unnoticed | Medium | High |
31
- | Make failures visible | Problems surface instead of hiding | Small | Medium |
29
+ | Fix | Why it matters to you | Effort | Payoff |
30
+ | ---------------------------- | --------------------------------------------- | ------ | ------ |
31
+ | Turn on automatic checking | Sites weren't refreshing themselves | _Done_ | High |
32
+ | Make the follow-up automatic | Stops your intelligence going stale unnoticed | Medium | High |
33
+ | Make failures visible | Problems surface instead of hiding | Small | Medium |
32
34
 
33
35
  ---
34
36
 
@@ -41,7 +43,7 @@ changes into summaries your team can act on.
41
43
 
42
44
  **How it works today.** Like a clipping service: on a timer, the app re-reads
43
45
  each competitor's site, compares it to last time, decides whether anything
44
- *meaningful* changed (it ignores trivial edits), and writes up a summary when
46
+ _meaningful_ changed (it ignores trivial edits), and writes up a summary when
45
47
  something real happens.
46
48
 
47
49
  **What's working.** The expensive, valuable part is solid — the app is genuinely
@@ -49,10 +51,11 @@ good at reading messy sites, separating real changes from noise, and summarizing
49
51
  them. Keep it.
50
52
 
51
53
  **What's holding you back.**
54
+
52
55
  - **The automatic checking wasn't switched on.** The machinery existed but nothing
53
56
  pulled the trigger, so sites weren't refreshing on their own. What it means for
54
57
  you: your "live" intelligence wasn't live. Severity: Urgent. Effort: Small.
55
- *(Already fixed.)*
58
+ _(Already fixed.)_
56
59
  - **The follow-up is manual.** When a change is found, updating your briefings and
57
60
  comparisons doesn't happen on its own — someone has to click "regenerate." What
58
61
  it means for you: if nobody clicks, the dashboard shows old information while
@@ -70,15 +73,15 @@ surprises** — which is the entire promise of the product.
70
73
 
71
74
  ### The technology bets
72
75
 
73
- **Pikku — the framework I'm suggesting you rebuild onto.** *Buys you:* one way to
76
+ **Pikku — the framework I'm suggesting you rebuild onto.** _Buys you:_ one way to
74
77
  write a capability and drive it from anywhere — web, timers, background jobs,
75
78
  assistants — so the tracking rule is written once instead of three times, which is
76
- exactly the sprawl above. *Costs you:* it hasn't shipped a stable 1.0 (it's 0.12.x;
79
+ exactly the sprawl above. _Costs you:_ it hasn't shipped a stable 1.0 (it's 0.12.x;
77
80
  0.13 is the first release promising backwards compatibility), so until then
78
81
  upgrades can break you — pin the version and budget for upgrade work. Its community
79
82
  and hiring pool are far smaller than the mainstream default's. That's normal for a
80
83
  young framework and survivable, but it's a real cost and it's yours to weigh.
81
- *Usually:* worth it when the problem is genuinely sprawl, as it is here — and worth
84
+ _Usually:_ worth it when the problem is genuinely sprawl, as it is here — and worth
82
85
  waiting if nobody has capacity to own upgrades.
83
86
 
84
87
  ---
@@ -6,7 +6,8 @@ reader. Delete any section that would be empty rather than padding it.
6
6
  ---
7
7
 
8
8
  # {App name}, in plain English — and where it could get better
9
- *A second opinion on {scope: the whole app / the competitor-tracking system / …}*
9
+
10
+ _A second opinion on {scope: the whole app / the competitor-tracking system / …}_
10
11
 
11
12
  **How to read this:** no technical background needed. Part 1 is the summary — if
12
13
  you read nothing else, read that. Parts 2–3 go area by area for anyone who wants
@@ -21,9 +22,9 @@ opportunities. No jargon.}
21
22
 
22
23
  **If it were me, this is the order I'd tackle things:**
23
24
 
24
- | Fix | Why it matters to you | Effort | Payoff |
25
- |---|---|---|---|
26
- | {…} | {business impact} | Small/Medium/Large | High/Medium/Low |
25
+ | Fix | Why it matters to you | Effort | Payoff |
26
+ | --- | --------------------- | ------------------ | --------------- |
27
+ | {…} | {business impact} | Small/Medium/Large | High/Medium/Low |
27
28
 
28
29
  ---
29
30
 
@@ -38,6 +39,7 @@ opportunities. No jargon.}
38
39
  **What's working.** {genuine credit — the parts that are solid and worth keeping}
39
40
 
40
41
  **What's holding you back.**
42
+
41
43
  - **{Problem in plain terms}.** What it means for you: {business impact}.
42
44
  Severity: {Minor / Worth fixing / Serious / Urgent}. Effort to fix: {Small /
43
45
  Medium / Large}.
@@ -54,11 +56,12 @@ off. Say whether it's a cheap rewire or an expensive rebuild.}
54
56
  libraries — AND anything a rebuild would move them ONTO. Each gets both sides.}
55
57
 
56
58
  **{Technology}.**
57
- - *Buys you:* {in business terms}
58
- - *Costs you:* {in business terms — bills, hiring, shipping speed, upgrade work,
59
+
60
+ - _Buys you:_ {in business terms}
61
+ - _Costs you:_ {in business terms — bills, hiring, shipping speed, upgrade work,
59
62
  the risk of betting on something young. Don't soften it. If it hasn't shipped a
60
63
  stable 1.0, say so and say what that means: pin the version, budget upgrades.}
61
- - *Usually:* {recommendation tied to their stage — normally "keep it, watch this"}
64
+ - _Usually:_ {recommendation tied to their stage — normally "keep it, watch this"}
62
65
 
63
66
  {The same bar applies to anything you're recommending they move to. A stack you
64
67
  propose with no cons listed is a pitch, not a second opinion.}
@@ -63,7 +63,7 @@ Not every adapter supports every option. Each adapter declares a
63
63
  silently ignored — so check the startup logs if a setting appears to have no
64
64
  effect.
65
65
 
66
- `groupConcurrency` limits how many jobs run concurrently *per group* (jobs
66
+ `groupConcurrency` limits how many jobs run concurrently _per group_ (jobs
67
67
  carrying a `JobGroup` with an `id` and optional `tier`), so one noisy tenant
68
68
  cannot consume the whole worker:
69
69