@pikku/skills 0.12.60 → 0.12.62

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/skills",
3
- "version": "0.12.60",
3
+ "version": "0.12.62",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/pikkujs/pikku.git",
@@ -47,10 +47,12 @@ wireAddon({
47
47
  name: string, // Namespace for addon functions (e.g. 'todos')
48
48
  package: string, // NPM package name (e.g. '@pikku/addon-todos')
49
49
  rpcEndpoint?: string, // Optional remote RPC endpoint for distributed execution
50
+ uses?: Record<string, string>, // alias the addon declared → name this app wired it under (only declared aliases; PKU355)
50
51
  auth?: boolean, // Require a session for every function in the addon
51
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
52
- 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)
53
+ expose?: boolean | string[], // opt-in: what rpc.exposed / POST /rpc may reach. true = every function the addon declared expose: true, a list = those of them named (typed; PKU343 on unknown or undeclared names), unset/false = none
53
54
  tags?: string[], // Tags applied to all addon functions
55
+ ui?: boolean, // Mount the screens the addon declares with pikkuStudio
54
56
  scopes?: string[], // Required of every function, on top of its own
55
57
  secretOverrides?: Record<string, string>, // Remap secret names (and grant them)
56
58
  variableOverrides?: Record<string, string>, // Remap variable names
@@ -159,6 +161,12 @@ export const createSingletonServices = pikkuAddonServices(
159
161
  )
160
162
  ```
161
163
 
164
+ Export the call as it is. A cast around it (`pikkuAddonServices(...) as unknown
165
+ as CreateSingletonServices<...>`, `<T>`, `!`) hides the factory
166
+ from the inspector, which would leave it unregistered and the addon's workflows
167
+ failing at runtime; the build fails with `PKU564` instead. Fix the type the cast
168
+ was hiding. `satisfies` and parentheses are fine: the inspector reads through them.
169
+
162
170
  `secrets` and `variables` arrive **typed against the addon's own declarations**,
163
171
  and a secret is a `SecretValue` — `.reveal()` is the only way to the plaintext
164
172
  (see `pikku-services`). `pikkuAddonConfig` is the matching factory for the addon's
@@ -279,6 +287,155 @@ everything else. Leave it out and the addon installs fine and fails to typecheck
279
287
  in every app that depends on it, which is what `pikku validate` is there to
280
288
  catch before you publish.
281
289
 
290
+ ### Remote functions and the SDK
291
+
292
+ A `remote: true` function is called over the wire by apps that use
293
+ `wireRemoteAddon`. Those apps build against the addon's SDK, not its source, so
294
+ **a remote function's input and output must be exported zod schemas**. If it
295
+ uses generics (`pikkuSessionlessFunc<In, Out>`) or returns a type declared in
296
+ `src/`, codegen fails with PKU922. Functions that are not remote can keep their
297
+ generics.
298
+
299
+ ```typescript
300
+ // src/functions/listNotes.schemas.ts
301
+ import { z } from 'zod'
302
+
303
+ export const NoteRows = z.array(z.object({ id: z.string(), body: z.string() }))
304
+ ```
305
+
306
+ ```typescript
307
+ // src/functions/listNotes.function.ts
308
+ import { pikkuSessionlessFunc } from '#pikku/addon/function'
309
+ import { NoteRows } from './listNotes.schemas.js'
310
+
311
+ export const listNotes = pikkuSessionlessFunc({
312
+ output: NoteRows,
313
+ func: async ({ kysely }) =>
314
+ kysely.selectFrom('notes').select(['id', 'body']).execute(),
315
+ remote: true,
316
+ })
317
+ ```
318
+
319
+ ```bash
320
+ yarn pikku all # also writes .pikku/sdk/ + <scope>-<name>-<version>.sdk.tgz
321
+ yarn pikku versions check # fails on a remote contract changed without a new version
322
+ ```
323
+
324
+ In an addon, `pikku all` packs the SDK on every run, at the package.json
325
+ version. The SDK carries only the remote RPCs, the `expose: true` workflows and
326
+ the types they reach. Changing a published `remote: true` contract needs a new
327
+ `version` on the function, which `pikku versions check` enforces.
328
+
329
+ ### Calling another addon
330
+
331
+ Declare it in the addon's **package.json** (not pikku.config.json), by the
332
+ alias your code calls it by and a range:
333
+
334
+ ```json
335
+ "pikku": { "uses": { "stripe": "@pikkustudio/addon-stripe@^0.12.0" } }
336
+ ```
337
+
338
+ Then call it typed, with no cast and no hand-written wrapper:
339
+
340
+ ```typescript
341
+ const charge = await rpc.invoke('stripe:charge', { amountCents })
342
+ await rpc.startWorkflow('stripe:refund', { chargeId: charge.chargeId })
343
+ ```
344
+
345
+ `pikku all` in the addon types `stripe:*` from the dependency's SDK (its remote
346
+ surface). A package in the same workspace is read from its own
347
+ `<outDir>/sdk`, so run `pikku all` there first (PKU338 if missing, PKU354 if
348
+ its version is outside the range); anything else is fetched from the registry
349
+ and pinned in `pikku-addons.lock`. Never cast `rpc` or call by the package
350
+ name; the alias is the only name that works.
351
+
352
+ The app wiring your addon maps the alias with `uses`, local or remote alike:
353
+
354
+ ```typescript
355
+ wireRemoteAddon({
356
+ name: 'payments',
357
+ package: '@pikkustudio/addon-stripe@^0.12.0',
358
+ serverUrl,
359
+ })
360
+ wireAddon({
361
+ name: 'billing',
362
+ package: '@acme/addon-billing',
363
+ uses: { stripe: 'payments' },
364
+ })
365
+ ```
366
+
367
+ | Code | The app's `pikku all` fails when |
368
+ | ------ | ------------------------------------------------------------------- |
369
+ | PKU348 | an alias the addon declared is not mapped |
370
+ | PKU347 | it is mapped to a name not wired, or wired from another package |
371
+ | PKU355 | `uses` maps a name the addon did not declare |
372
+ | PKU354 | the mapped instance's version is outside the range |
373
+ | PKU356 | the addon's `pikku.uses` entry is malformed (alias, type, no range) |
374
+
375
+ At runtime an unmapped alias, or the wired name called directly, throws
376
+ `AddonNotUsedError` (403) for invoke, workflows and agents.
377
+
378
+ ### Studio screens
379
+
380
+ An addon declares the screens it adds to Pikku Studio with `pikkuStudio`, in
381
+ `src/screens.ts` (`pikku new addon-ui <name>` scaffolds one). It replaces
382
+ `defineScreens`, which no longer exists.
383
+
384
+ ```typescript
385
+ import { pikkuStudio } from '@pikku/core/addon'
386
+
387
+ export default pikkuStudio({
388
+ title: 'invoices__title', // message key: the addon's name in Studio
389
+ screens: [
390
+ {
391
+ path: '/invoices',
392
+ title: 'invoices_list__title', // message key, also the nav label
393
+ scope: 'project', // 'studio' → /<path>, 'team' → /<team>/<path>,
394
+ // 'project' → /<team>/<project>[/<target>]/<path>
395
+ nav: {
396
+ area: 'project',
397
+ icon: 'receipt',
398
+ order: 30,
399
+ badge: 'unpaidCount',
400
+ },
401
+ needsApp: true, // project screens only; default false
402
+ scopes: ['invoices:read'], // hides the screen; NOT access control
403
+ component: () => import('./screens/Invoices.js'),
404
+ },
405
+ {
406
+ path: '/invoices/print',
407
+ title: 'invoices_print__title',
408
+ scope: 'project',
409
+ layout: 'bare',
410
+ app: 'print',
411
+ },
412
+ ],
413
+ })
414
+ ```
415
+
416
+ - A screen is a lazy `component` or an `app`, never both. `nav` is optional:
417
+ without it the screen is routed but not listed. `nav.area` is `'rail'`,
418
+ `'team'`, `'project'` or `'account'`; `badge` names one of the addon's
419
+ functions that returns a count.
420
+ - `replaces: true` takes over the core screen at the same path (first-party
421
+ addons only; Studio enforces that). `layout: 'bare'` renders without the shell.
422
+ - Titles are message keys in the addon's `messages/{locale}.json`, found by
423
+ convention next to its `package.json`; there is no field for them.
424
+ - `scopes` only hide the screen. The addon's functions must enforce the same
425
+ scopes.
426
+
427
+ The inspector reads every field statically, so each must be a literal.
428
+ `pikku all` fails on:
429
+
430
+ | Code | When |
431
+ | ------ | ------------------------------------------------------------ |
432
+ | PKU345 | a field is not a literal (variable, call, spread, bad value) |
433
+ | PKU357 | two screens of the addon use the same path |
434
+ | PKU358 | a title key is missing from `messages/en.json` |
435
+ | PKU359 | `nav.badge` names no function of the addon |
436
+
437
+ A key missing from another locale's catalog is a warning.
438
+
282
439
  ### Database tables
283
440
 
284
441
  An addon may **ship** tables. It must never **create** them: it runs inside the
@@ -361,6 +518,13 @@ covered, and writes only the delta after an addon upgrade. An addon wired with
361
518
  `wireRemoteAddon` contributes no schema at all: its tables belong to the
362
519
  deployment that runs its functions.
363
520
 
521
+ A `wireRemoteAddon` whose `package` carries a range (`'@scope/name@^1.4.0'`)
522
+ is not installed at all: `pikku all` downloads its SDK into
523
+ `.pikku/remote` and pins it in `pikku-addons.lock` (commit the lock). If
524
+ codegen reports PKU338 for one, run `pikku all`, not an install; to move
525
+ to a newer SDK, edit the range by hand (`pikku update` prints the file:line
526
+ and the new literal; it never rewrites source), then run `pikku all`.
527
+
364
528
  ### Call via RPC
365
529
 
366
530
  ```typescript
@@ -381,9 +545,10 @@ and the frontend calls `rpc.invoke('todos:listTodos', …)` (over HTTP,
381
545
  Use `ref('todos:listTodos')` on an HTTP or MCP wiring only when the addon needs
382
546
  a route of its own. Don't write an app function that calls `rpc.invoke` and
383
547
  returns the result unchanged: it's a second name and a second schema for the
384
- same thing, and it drifts. `expose: true` exposes only what the addon itself
385
- declared `expose: true` — an OpenAPI-generated addon declares none, so list
386
- the names.
548
+ same thing, and it drifts. Nothing in an addon is reachable over `/rpc` until
549
+ the wiring opts in, and it can only open what the addon itself declared
550
+ `expose: true` — an OpenAPI-generated addon declares every operation, so list
551
+ the ones the frontend needs.
387
552
 
388
553
  Write your own function when the app needs the data narrowed, typed, or
389
554
  combined (a flag the upstream sends as `"0"`, a total summed from several
@@ -81,6 +81,12 @@ package that declared them, and the extra segment is what stops a linked addon's
81
81
  }
82
82
  ```
83
83
 
84
+ **`pikku.uses` names the addons this addon calls**, as
85
+ `{ "<alias>": "<package>@<range>" }`. The addon calls `<alias>:<fn>`, typed from
86
+ that package's SDK, and the app wiring it maps each alias with `wireAddon`
87
+ `uses` (see "Calling another addon" in the skill). The older list form,
88
+ `["kysely"]`, names keys the app must map, with no SDK or version check.
89
+
84
90
  **`imports` names `dist`, never the source tree.** `files: ["dist"]` is the whole
85
91
  published package, and `pikku dist` copies `.pikku` and `types` into it — so a
86
92
  `#pikku/*` target under `./.pikku/` resolves for the author and for nobody else.
@@ -60,7 +60,7 @@ Most surfaces are switched on by the CLI rather than hand-wired:
60
60
  bunx --bun pikku enable workflow # workflow workers
61
61
  bunx --bun pikku enable agent # public agent endpoints
62
62
  bunx --bun pikku enable events # realtime events channel + SSE stream
63
- bunx --bun pikku enable remote-rpc # internal RPC queue worker + HTTP endpoint
63
+ bunx --bun pikku enable mesh-rpc # mesh RPC queue worker + HTTP endpoint
64
64
  bunx --bun pikku enable webhook # outgoing webhook delivery queue worker
65
65
  bunx --bun pikku enable scenarios # scenario instrumentation
66
66
  bunx --bun pikku enable rpc # public RPC endpoint
@@ -74,7 +74,7 @@ builds anyway (`config`, `logger`, `variables`, `schema`, `secrets`, and the
74
74
  per-request `rpc`/`mcp`/`channel`/`userSession`). Units are named for that set —
75
75
  `svc-todo-store`, `svc-event-hub-todo-store`, `svc-base` for functions needing
76
76
  nothing else — and each records it as `servicesKey` in the manifest. On the
77
- `templates/functions` app it turns 44 units into 10.
77
+ `recipes/functions` app it turns 44 units into 10.
78
78
 
79
79
  The deploy target is part of the key, so a `server` unit is named `-server` and
80
80
  can never merge with its serverless twin. That is what keeps `services` from
@@ -74,6 +74,12 @@ service's paths and size limit; omitting it does not disable it.
74
74
  - **Do not take `services` as a named parameter and cast in the body.** The
75
75
  inspector reads the destructuring pattern to build the manifest; a cast makes
76
76
  the service invisible to it, so tree-shaking drops what the function needs.
77
+ - **Do not cast a service factory.** `pikkuServices(...) as X` (or `as unknown
78
+ as`, `!`, `<X>`) hides the call from the inspector,
79
+ so the factory would not be registered; the build fails with `PKU564`
80
+ instead. Export the direct `pikkuServices(...)` / `pikkuWireServices(...)` /
81
+ `pikkuConfig(...)` call and fix the type mismatch the cast was hiding.
82
+ `satisfies` and parentheses are fine: the inspector reads through them.
77
83
  - **Do not hand-roll an audit table.** `auditLog.write()` on an `audit: true`
78
84
  function enriches the event with the function id, wire type, trace id and user
79
85
  identity; an `insertInto('audit_log')` of your own gets none of that, and a
@@ -167,6 +167,36 @@ cron expression can decide which tasks fire. `pikku-deploy` has the comparison.
167
167
  Enabling the RPC endpoint says the endpoint exists, not who may call it — each
168
168
  `expose: true` function is still gated by its own `auth`, permissions and scopes.
169
169
 
170
+ ## A function nothing uses: wire it or disable it
171
+
172
+ `pikku verify` fails on an app function that is not wired, exposed, or called
173
+ by name (`rpc.invoke`, a workflow step, an agent tool) — `[PKU563]`, naming the
174
+ function and its file. `pikku all` still builds; only verify fails. Addon
175
+ functions are never flagged, in the addon or in an app that uses it.
176
+
177
+ Workflows are checked too. A workflow is used when it is wired, exposed, or
178
+ started by a literal name: `rpc.startWorkflow('name')`,
179
+ `workflowService.startWorkflow` / `runToCompletion('name')`,
180
+ `rpc.invoke('name')`, another workflow's step, or a client helper. A workflow
181
+ started only through a computed name is flagged.
182
+
183
+ To keep a function in the source but out of the build, mark it `disabled`:
184
+
185
+ ```typescript
186
+ export const oldReport = pikkuSessionlessFunc({
187
+ disabled: 'replaced by liveReport', // or `true`
188
+ func: async () => 'old',
189
+ })
190
+ ```
191
+
192
+ A disabled function is left out of everything: meta, RPC maps, clients, MCP,
193
+ CLI, HTTP, addon SDKs. It cannot be registered at runtime either. Anything
194
+ that still references it — a wiring, an `rpc.invoke('oldReport')`, a workflow
195
+ step, an agent tool — fails the build with `[PKU562]`, naming both sides.
196
+ `pikku all` lists the disabled functions once, with file and reason.
197
+
198
+ The value must be `true` or a string literal; a computed value is refused.
199
+
170
200
  ## What NOT to do
171
201
 
172
202
  - Do not import a wiring factory from `@pikku/core/*`. It compiles and silently
@@ -13,14 +13,14 @@
13
13
  | Method | Purpose |
14
14
  | -------------------------------- | ----------------------------------------- |
15
15
  | `rpc.invoke(name, data)` | Internal call to any wired function |
16
- | `rpc.remote(name, data)` | Remote call via DeploymentService |
16
+ | `rpc.mesh(name, data)` | Mesh call via DeploymentService |
17
17
  | `rpc.exposed(name, data)` | Call functions marked with `expose: true` |
18
18
  | `rpc.startWorkflow(name, input)` | Start a workflow (see `pikku-workflow`) |
19
19
  | `rpc.agent.run/stream(...)` | Run an AI agent (see `pikku-agent`) |
20
20
  | `rpc.agent.resume/approve(...)` | Answer a tool-approval interrupt |
21
21
  | `rpc.agent.interrupt(runId)` | Stop an in-flight run |
22
22
 
23
- `rpc.invoke`, `rpc.remote` and `rpc.startWorkflow` are typed off the generated
23
+ `rpc.invoke`, `rpc.mesh` and `rpc.startWorkflow` are typed off the generated
24
24
  RPC map, so the name and the payload are checked. `rpc.exposed` is deliberately
25
25
  `(name: string, data: any) => Promise<any>` — it exists to dispatch a name that
26
26
  arrived from outside, which by definition cannot be checked at compile time.