@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.
- package/CHANGELOG.md +768 -0
- package/dist/skills.gen.js +1 -1
- package/package.json +4 -4
- package/skills/pikku-addon/SKILL.md +17 -11
- package/skills/pikku-addon/references/addon-package-manifest.md +2 -2
- package/skills/pikku-agent/SKILL.md +3 -3
- package/skills/pikku-audit/SKILL.md +28 -13
- package/skills/pikku-aws/SKILL.md +2 -2
- package/skills/pikku-better-auth/SKILL.md +97 -17
- package/skills/pikku-build-app/SKILL.md +621 -0
- package/skills/pikku-build-app/references/multi-app.md +117 -0
- package/skills/pikku-build-app/references/ship.md +98 -0
- package/skills/pikku-build-app/references/theming.md +70 -0
- package/skills/pikku-build-platform/SKILL.md +239 -0
- package/skills/pikku-build-quick/SKILL.md +238 -0
- package/skills/pikku-cli/SKILL.md +7 -7
- package/skills/pikku-cli/references/complete-example.md +1 -1
- package/skills/pikku-concepts/SKILL.md +5 -2
- package/skills/pikku-concepts/references/concept-mapping.md +1 -1
- package/skills/pikku-config/SKILL.md +5 -3
- package/skills/pikku-deploy-azure/SKILL.md +5 -4
- package/skills/pikku-emails/SKILL.md +5 -5
- package/skills/pikku-fabric/SKILL.md +27 -3
- package/skills/pikku-fabric-debug/SKILL.md +1 -1
- package/skills/pikku-feature/SKILL.md +5 -4
- package/skills/pikku-http/SKILL.md +4 -4
- package/skills/pikku-http/references/http-options.md +13 -13
- package/skills/pikku-i18n/SKILL.md +2 -1
- package/skills/pikku-info/SKILL.md +1 -1
- package/skills/pikku-knowledge/SKILL.md +13 -13
- package/skills/pikku-mcp/SKILL.md +4 -4
- package/skills/pikku-middleware/SKILL.md +5 -5
- package/skills/pikku-middleware/references/middleware-patterns.md +14 -8
- package/skills/pikku-n8n-import/SKILL.md +12 -12
- package/skills/pikku-n8n-import/SPEC.md +3 -0
- package/skills/pikku-n8n-import/references/code-translation.md +26 -22
- package/skills/pikku-n8n-import/references/loops-and-control.md +7 -7
- package/skills/pikku-paraglide/SKILL.md +11 -6
- package/skills/pikku-permissions/SKILL.md +19 -15
- package/skills/pikku-product-second-opinion/README.md +3 -3
- package/skills/pikku-product-second-opinion/example/sample-report.md +15 -12
- package/skills/pikku-product-second-opinion/references/report-template.md +10 -7
- package/skills/pikku-queue/SKILL.md +1 -1
- package/skills/pikku-react/SKILL.md +53 -13
- package/skills/pikku-realtime/SKILL.md +51 -19
- package/skills/pikku-rtl/SKILL.md +1 -1
- package/skills/pikku-scenario/SKILL.md +126 -14
- package/skills/pikku-schedule/SKILL.md +6 -1
- package/skills/pikku-schema-ajv/SKILL.md +2 -2
- package/skills/pikku-schema-cfworker/SKILL.md +1 -1
- package/skills/pikku-security/SKILL.md +9 -5
- package/skills/pikku-services/SKILL.md +27 -18
- package/skills/pikku-services/references/audit-wire-service.md +14 -8
- package/skills/pikku-software-archaeology/scripts/validate.mjs +146 -69
- package/skills/pikku-tag-middleware/SKILL.md +1 -0
- package/skills/pikku-template-clone/SKILL.md +2 -1
- package/skills/pikku-trigger/SKILL.md +3 -3
- package/skills/pikku-websocket/SKILL.md +4 -3
- package/skills/pikku-workflow/SKILL.md +2 -2
- 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
|
|
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 '
|
|
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(
|
|
35
|
-
|
|
34
|
+
const apiKeyAuth = pikkuMiddleware(
|
|
35
|
+
async ({ kysely }, { http, setSession, session }, next) => {
|
|
36
|
+
if (session) return next() // already authenticated
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
const header = http?.request?.header?.('x-api-key')
|
|
39
|
+
if (!header) return next()
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
|
63
|
-
|
|
64
|
-
| `STUB — generated from n8n node "…" (type "n8n-nodes-base.<svc>…")`
|
|
65
|
-
| `STUB — generated from n8n Code node "…"`
|
|
66
|
-
| A `control` stub — Loop Over Items / **splitInBatches**, Switch expr-mode | `references/loops-and-control.md`
|
|
67
|
-
| `STUB — … vector-store … #902`
|
|
68
|
-
| Importer `diagnostics` (already exited 1)
|
|
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…
|
|
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
|
|
106
|
-
| lower a Loop Over Items / splitInBatches loop, or a Switch that stayed a stub
|
|
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
|
|
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
|
|
31
|
-
|
|
32
|
-
| `items`
|
|
33
|
-
| `items[i].json.X`
|
|
34
|
-
| `items[i].json`
|
|
35
|
-
| `items[i].binary`
|
|
36
|
-
| `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
|
|
42
|
-
|
|
43
|
-
| `$json.X` / `$input.item.json.X` | `data.X` (input is the item itself)
|
|
44
|
-
| `$input.item.json`
|
|
45
|
-
| `$input.all()`
|
|
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
|
|
50
|
-
|
|
51
|
-
| `return [{ json: X }]`
|
|
52
|
-
| `return items.map(i => ({ json: ... }))` | `return { items: items.map(...) }`
|
|
53
|
-
| `return [{ json: X }, { json: Y }]`
|
|
54
|
-
| `return { json: X }` (each-item)
|
|
55
|
-
| `return [...]` (already plain)
|
|
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(
|
|
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…
|
|
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)
|
|
39
|
-
| pure side-effect per item, nothing downstream consumes results
|
|
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
|
|
53
|
-
|
|
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
|
|
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:
|
|
39
|
-
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 =
|
|
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
|
|
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({
|
|
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,
|
|
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()
|
|
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 "
|
|
81
|
-
"
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
15
|
-
- **pikku-product-second-opinion** reads that blueprint and writes an
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
28
|
-
|
|
29
|
-
| Turn on automatic checking
|
|
30
|
-
| Make the follow-up automatic | Stops your intelligence going stale unnoticed | Medium | High
|
|
31
|
-
| Make failures visible
|
|
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
|
-
|
|
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
|
-
|
|
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.**
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
25
|
-
|
|
26
|
-
| {…} | {business impact}
|
|
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
|
-
|
|
58
|
-
-
|
|
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
|
-
-
|
|
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
|
|
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
|
|