@uidu/skills 0.2.0 → 0.4.0

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": "@uidu/skills",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Agent skills for the uidu SDK — installable via the open agent skills ecosystem (skills.sh).",
5
5
  "license": "MIT",
6
6
  "author": "uidu",
@@ -1,18 +1,19 @@
1
1
  ---
2
2
  name: uidu
3
- description: Use when building uidu-powered applications — fetching CMS pages, events, stories, donation campaigns, help center articles, or forms; rendering WYSIWYG content; or scaffolding new projects with create-uidu-app. Triggers on any task involving the uidu platform, the @uidu/client SDK, or the @uidu/react bindings.
3
+ description: Use when building on the uidu platform — a public website or an app that runs inside uidu (a custom app in a Space or workspace); reading CMS pages, events, stories, donations, help center, forms; rendering WYSIWYG content; scaffolding projects; OR provisioning/authoring uidu content (create/update/delete) from the terminal via the @uidu/cli (`uidu`). Triggers on any task involving uidu, the @uidu/client SDK, @uidu/react bindings, or the uidu CLI.
4
4
  license: MIT
5
5
  metadata:
6
6
  author: uidu
7
- version: "0.1.0"
7
+ version: "0.5.0"
8
8
  ---
9
9
 
10
10
  # uidu SDK
11
11
 
12
- The uidu SDK is a TypeScript GraphQL toolkit for building websites and apps powered by the uidu platform. It is split into two packages:
12
+ The uidu SDK is a TypeScript GraphQL toolkit for building websites and apps powered by the uidu platform. It is split into three packages:
13
13
 
14
14
  - **`@uidu/client`** — framework-agnostic GraphQL client. Safe for server-side use (RSC, route handlers, server actions, scripts).
15
- - **`@uidu/react`** — React hooks (`useFields`, `useQuery`, `useForm`, `useChannel`) and components (`<PageBlocks>`, `<RichText>`).
15
+ - **`@uidu/app-bridge`** — the browser half of a custom app: the handshake with the uidu page that frames it, and its short-lived session token. Only for apps that run inside uidu.
16
+ - **`@uidu/react`** — React hooks (`useUiduClient`, `useQuery`, `useFields`, `toText`, `useUiduApp`) and components (`<UiduAppProvider>`, `<PageBlocks>`, `<BlockRenderer>`, `<RichText>`, `<DynamicForm>`). That list is the **complete** public surface — there is no `useForm`, `usePage` or `useChannel`; for anything else, call a `@uidu/client` function.
16
17
 
17
18
  `@uidu/api.js` is a legacy bridge package and is **deprecated** — use `@uidu/react` directly.
18
19
 
@@ -20,12 +21,13 @@ The uidu SDK is a TypeScript GraphQL toolkit for building websites and apps powe
20
21
 
21
22
  Use this skill when the user is:
22
23
 
23
- - Building a Next.js / Remix / Astro / React app powered by the uidu GraphQL API
24
+ - Building a Next.js / Remix / Astro / React app powered by the uidu GraphQL API — a public site, or a **custom app** that uidu shows inside a Space or workspace to signed-in members (same stack, different auth: see the rule below)
24
25
  - Fetching CMS pages, events, blog stories, donation campaigns, or help center content
25
26
  - Rendering uidu blocks with `<PageBlocks>` and a component map
26
27
  - Rendering WYSIWYG body fields with `<RichText>`
27
28
  - Bootstrapping a new project with `create-uidu-app`
28
- - Asking about `@uidu/client`, `@uidu/react`, `uidu`, or the uidu GraphQL API
29
+ - **Reading or provisioning a uidu workspace from the terminal** — the `uidu` CLI (`@uidu/cli`): `uidu login`, `uidu <entity> list|get|create|update|delete`, `uidu workspace create`
30
+ - Asking about `@uidu/client`, `@uidu/react`, the `uidu` CLI, or the uidu GraphQL API
29
31
 
30
32
  ## Package Map
31
33
 
@@ -33,6 +35,7 @@ Use this skill when the user is:
33
35
  |---|---|---|
34
36
  | `@uidu/client` | Server-side queries (RSC, server actions, scripts) | Framework-agnostic |
35
37
  | `@uidu/react` | React hooks, providers, and components | Some hooks are client-only |
38
+ | `@uidu/app-bridge` | An app that runs inside uidu (custom app) | Browser only; in React use `<UiduAppProvider>` |
36
39
  | `@uidu/api.js` | **Do not use for new code.** | Deprecated; re-exports `@uidu/react` |
37
40
 
38
41
  Install:
@@ -41,6 +44,148 @@ Install:
41
44
  npm install @uidu/client @uidu/react
42
45
  ```
43
46
 
47
+ ## The `uidu` CLI (`@uidu/cli`)
48
+
49
+ For **reading a workspace or provisioning content from the terminal** — and for driving
50
+ uidu as an AI agent — use the `uidu` CLI. It wraps `@uidu/client`, so it shares the same
51
+ engine and auth model. Every command supports `--json` (machine-readable — parse this).
52
+
53
+ ```bash
54
+ npx @uidu/cli --help # or: npm i -g @uidu/cli
55
+ uidu login # browser OAuth (PKCE); --password-grant + UIDU_PASSWORD for CI/agents
56
+ uidu whoami --json
57
+
58
+ # entities — uniform verbs
59
+ uidu <entity> list [--first 50] --json
60
+ uidu <entity> get <id> --json
61
+ uidu <entity> create --attributes '<json>' # or --name / --slug
62
+ uidu <entity> update <id> --attributes '<json>'
63
+ uidu <entity> delete <id>
64
+
65
+ # CMS (project-scoped — pass --project or set UIDU_PROJECT_ID)
66
+ uidu pages list --project <id> --json
67
+ uidu page create --name Home --slug home --attributes '{"projectId":"<id>"}'
68
+
69
+ # provisioning + scaffold
70
+ uidu workspace create --name "Acme"
71
+ uidu workspace credentials --json # api key + secret
72
+ uidu create my-app -t events # delegates to create-uidu-app
73
+ ```
74
+
75
+ Entities: `events, stories, donations, courses, forms, contacts, deals, employees,
76
+ bookings, calls, campaigns, kb-collections, kb-articles, channel` (read) and
77
+ `tasks, notes, spaces` (write-only). Not every verb exists for every entity — the CLI
78
+ reports `does not support <verb>` when it doesn't. Config resolves **flags → env
79
+ (`UIDU_*`) → `~/.uidu/config.json`** (written by `uidu login`).
80
+
81
+ ## Who the app acts for — one rule
82
+
83
+ What an app *looks like* (landing page, blog, dashboard, booking tool) changes nothing.
84
+ What changes the code is **who the request acts for**:
85
+
86
+ | Operation | Auth | Where it runs |
87
+ |---|---|---|
88
+ | **Reads** for anyone (`get*` / `list*`, `<entity> list/get`) | `publicToken` | anywhere — server, CLI, **and the browser** |
89
+ | **Writes** as the workspace (`create*` / `update*` / `delete*`, `<entity> create/update/delete`) | account **Bearer** (`apiKey`) | CLI and **server-side** only |
90
+ | **Anything as the signed-in member**, inside uidu (a custom app) | session token from the host, over `@uidu/app-bridge` | **the browser only** |
91
+
92
+ The write functions live in `@uidu/client`, so anything the CLI provisions you can also do
93
+ from a Next.js server action / route handler / RSC. **Never put the account Bearer token in a
94
+ browser client component.** Authoring mutations return a payload with an `errors` array —
95
+ always check it (non-empty = validation failure).
96
+
97
+ One app can mix the rows: public pages rendered on the server with `publicToken`, and a members'
98
+ area that only works when uidu frames it.
99
+
100
+ ### Inside uidu: the third row
101
+
102
+ uidu renders a custom app in a sandboxed iframe and hands it a short-lived session (5 minutes,
103
+ refreshed by the bridge) over `postMessage`. That session is the member's: the app can do what
104
+ that person can do, and no more. It changes five things:
105
+
106
+ 1. **The token only exists in the browser.** It arrives after the page loads, so a Server
107
+ Component or a server action never has it. Pages that show member data are `'use client'`
108
+ and read through the bridge-backed client. Fetching them in an RSC renders an empty page.
109
+ 2. **The token only works from the app's own origin.** uidu checks `Origin` against the origin
110
+ it signed the token for. Don't forward it to your own backend to call uidu from there — that
111
+ needs a server credential uidu doesn't issue yet.
112
+ 3. **Treat the token as opaque.** Never decode it. The expiry comes with it, the bridge refreshes
113
+ it, and `graphqlUrl` comes with it too: never build `https://<workspace>.uidu.org` from a slug.
114
+ 4. **Data goes in the app's own Models.** The session reaches the app's own Models, fields and
115
+ items through `node(id:)`, and the data-engine mutations. Everything else in the workspace comes
116
+ back as an error. Keep your data there rather than in a database of your own: it stays
117
+ searchable and permissioned in uidu.
118
+ `listModelItems` reads uidu's **search index**, which catches up a moment after a write:
119
+ after `createModelItem` / `deleteModelItem`, update your list from the mutation payload
120
+ instead of re-listing, or the new item is missing. And load models once per app instance
121
+ (memoize the promise): two concurrent `ensureModel` calls each create the model.
122
+ 5. **No cookies, no `localStorage` for shared state.** In a cross-origin iframe cookies are
123
+ third-party (Safari drops them). `localStorage` is this browser only. Use it only as the
124
+ fallback when the page is opened outside uidu (the bridge rejects `connect()` there).
125
+
126
+ ```tsx
127
+ 'use client'; // app/layout.tsx renders <AppProvider> around {children}
128
+ import { useEffect, useState } from 'react';
129
+ import { UiduAppProvider, useUiduApp } from '@uidu/react';
130
+ import { DEFAULT_HOST_ORIGINS } from '@uidu/app-bridge';
131
+ import {
132
+ createModelItem, ensureModel, listModelItems, toFieldValuesAttributes,
133
+ type ModelItem, type UiduClient,
134
+ } from '@uidu/client';
135
+
136
+ export function AppProvider({ children }: { children: React.ReactNode }) {
137
+ // https://*.uidu.org by default; add a local uidu or a custom domain
138
+ return <UiduAppProvider hostOrigins={[...DEFAULT_HOST_ORIGINS]}>{children}</UiduAppProvider>;
139
+ }
140
+
141
+ async function loadBookings(client: UiduClient, workspaceAppId: string) {
142
+ // interim: the app creates its own model on first load (install-time manifest comes later)
143
+ const model = await ensureModel(client, {
144
+ workspaceAppId,
145
+ name: 'Booking',
146
+ fields: [{ shortname: 'room', name: 'Room', kind: 'string' }],
147
+ });
148
+ await createModelItem(client, {
149
+ input: { attributes: { modelId: model.id, fieldValuesAttributes: toFieldValuesAttributes(model, { room: 'Blu' }) } },
150
+ });
151
+ return listModelItems(client, { modelId: model.id }); // item.fieldValuesByShortname.room
152
+ }
153
+
154
+ export function Bookings() {
155
+ const app = useUiduApp(); // 'connecting' | 'ready' | 'error'
156
+ const [items, setItems] = useState<ModelItem[]>([]);
157
+
158
+ useEffect(() => {
159
+ if (app.status === 'ready') loadBookings(app.client, app.context.workspaceApp.id).then(setItems);
160
+ }, [app]);
161
+
162
+ if (app.status === 'error') return <p>Open this app from uidu ({app.error.code})</p>; // NOT_EMBEDDED, TIMEOUT…
163
+ return <ul>{items.map((item) => <li key={item.id}>{item.fieldValuesByShortname?.room}</li>)}</ul>;
164
+ }
165
+ ```
166
+
167
+ `app.context` carries `user`, `space`, `workspaceApp`, `locale` and `theme`. Outside React: `connect()` from
168
+ `@uidu/app-bridge`, then `createClient(fromBridge(bridge))` from `@uidu/client`. Start a new
169
+ one with `npm create uidu-app@latest my-app -- -t custom-app`: it declares its Models in
170
+ `public/uidu.app.json` and sets `frame-ancestors` so only uidu can frame it.
171
+
172
+ ## Working transparently
173
+
174
+ The user often can't tell what you're doing when you drive the CLI. Stay legible:
175
+
176
+ - **Orient first.** Before a multi-step task (especially scaffolding), say in plain words what the
177
+ end result is — e.g. "this creates a Next.js 16 + Tailwind project folder on your machine, wired
178
+ to your workspace, that you'll run at http://localhost:3000" — plus what they need (Node 20+;
179
+ the CLI runs via `npx`, nothing to install globally).
180
+ - **Show which account is active first.** Run `uidu whoami --json` and state the account/workspace
181
+ before anything else, so it's obvious you're touching their real (prod) workspace.
182
+ - **Echo commands.** Print the actual `uidu … --json` command before you run it, and one line on
183
+ why.
184
+ - **Never write without confirming.** Reads (`* list`, `* get`, `whoami`, `credentials`) run
185
+ freely. Any `create` / `update` / `delete` or provisioning hits the live workspace — ask first.
186
+ - **Summarize results.** Parse the `--json` and report what came back or what changed in plain
187
+ words; always surface a non-empty `errors` array instead of moving on.
188
+
44
189
  ## API Shape
45
190
 
46
191
  All SDK functions are **standalone functions** that take the client as the first argument. They are never namespaced as methods on the client object:
@@ -216,29 +361,103 @@ const channel = await getChannel(uidu, { id: channelId });
216
361
 
217
362
  ### Forms
218
363
 
219
- Forms are fetched server-side, then rendered with the `useForm` hook on the client:
364
+ Fetch the schema server-side, render it with `<DynamicForm>`, and submit through a
365
+ **server-side** action that calls `createFormResponse`. There is no `useForm` hook.
220
366
 
221
367
  ```tsx
222
- // Server: fetch form schema
368
+ // Server: fetch the form schema
223
369
  import { getForm } from '@uidu/client';
224
370
  const form = await getForm(uidu, { id: formId });
225
371
  ```
226
372
 
227
373
  ```tsx
228
- // Client: render with @uidu/react
374
+ // Client: render it
229
375
  'use client';
230
- import { useForm } from '@uidu/react';
376
+ import { DynamicForm } from '@uidu/react';
377
+ import { submitForm } from '@/lib/actions';
231
378
 
232
379
  export function ContactForm({ form }) {
233
- const { register, handleSubmit, onSubmit } = useForm(uidu, { form });
234
- return (
235
- <form onSubmit={handleSubmit(onSubmit)}>
236
- {/* render fields by iterating form.formQuestions.edges */}
237
- </form>
238
- );
380
+ return <DynamicForm form={form} action={submitForm} />;
239
381
  }
240
382
  ```
241
383
 
384
+ `<DynamicForm>` renders the questions itself and hands your `action` a
385
+ `DynamicFormValues`: `{ contact?: { firstName, lastName, email }, fieldValues:
386
+ Array<{ fieldId, questionId, value }>, formData }`. Return `{ ok: true }` or
387
+ `{ ok: false, errors }` and it renders the right state.
388
+
389
+ #### `content` is always `{ value: … }` — never a bare value
390
+
391
+ This is the one rule that breaks form writes. The API stores each answer in a JSON
392
+ `content` column and reads it back as `content['value']`. A flat scalar is **not**
393
+ rejected loudly: the response saves with empty typed columns, and reading that field
394
+ later throws. `FieldValueAttributes` exposes no `value` key — `content` is the only way in.
395
+
396
+ ```ts
397
+ // ✅ Correct — every answer wrapped
398
+ fieldValuesAttributes: [{ fieldId: 'f1', content: { value: 'hello@example.com' } }]
399
+
400
+ // ❌ Wrong — silently loses the answer
401
+ fieldValuesAttributes: [{ fieldId: 'f1', content: 'hello@example.com' }]
402
+ ```
403
+
404
+ It holds for **every** field kind, not just strings — `{ value: 42 }`, `{ value: true }`,
405
+ `{ value: '2026-07-27' }`, `{ value: ['gid://…', 'gid://…'] }` for `multipleSelect`,
406
+ `{ value: <doc> }` for `richText`/`json`. And it holds for `fieldValuesAttributes`
407
+ **wherever** it appears — on a page block, a contact, a story, a booking — not just on
408
+ form responses.
409
+
410
+ Full submit action:
411
+
412
+ ```ts
413
+ // src/lib/actions.ts
414
+ 'use server';
415
+ import { createFormResponse } from '@uidu/client';
416
+ import type { DynamicFormValues, DynamicFormResult } from '@uidu/react';
417
+ import { uidu } from '@/lib/uidu';
418
+
419
+ export async function submitForm(
420
+ values: DynamicFormValues,
421
+ ): Promise<DynamicFormResult> {
422
+ const result = await createFormResponse(uidu, {
423
+ input: {
424
+ event: 'complete!', // omit to save as a draft
425
+ attributes: {
426
+ formId: FORM_ID,
427
+ fieldValuesAttributes: values.fieldValues.map((fv) => ({
428
+ fieldId: fv.fieldId,
429
+ content: { value: fv.value }, // ← wrapped
430
+ })),
431
+ contactAttributes: values.contact && {
432
+ email: values.contact.email,
433
+ contactableAttributes: {
434
+ kind: 'person',
435
+ firstName: values.contact.firstName,
436
+ lastName: values.contact.lastName,
437
+ },
438
+ },
439
+ },
440
+ },
441
+ });
442
+
443
+ if (result?.errors?.length) return { ok: false, errors: result.errors };
444
+ return { ok: true, meta: { id: result?.formResponse?.id } };
445
+ }
446
+ ```
447
+
448
+ Notes:
449
+
450
+ - **Never put arbitrary keys in `attributes`.** Only what `FormResponseAttributes`
451
+ declares: `formId`, `fieldValuesAttributes`, `contactAttributes`, `contactId`,
452
+ `formPageId`, `completedAt`, `responsableId`, `id`. Unknown keys fail the mutation.
453
+ - `event` lives on `input`, **not** inside `attributes`, and is a state-machine
454
+ transition (`'complete!'`, `'save_draft!'`) — not free text.
455
+ - To record a timestamp, use `completedAt` on `attributes`, not a synthetic field value.
456
+ - `createFormResponse` **returns** `{ errors, formResponse }` instead of throwing —
457
+ always check `errors` before treating a submit as successful.
458
+ - Use `updateFormResponse` for multi-step or save-as-you-type flows; pass each
459
+ answer's existing `id` in `fieldValuesAttributes` to update it rather than add a new one.
460
+
242
461
  ## Rendering WYSIWYG Content
243
462
 
244
463
  All `body` fields on Event, Story, DonationCampaign, etc. are structured documents (not strings). Render them with `<RichText>` from `@uidu/react`:
@@ -253,7 +472,7 @@ The prop is `doc`, not `children`. The component handles paragraphs, headings, l
253
472
 
254
473
  ## Provider Setup (Client-Side Hooks)
255
474
 
256
- Server-side `await getX(...)` calls do not need a provider. For React client-side hooks (`useQuery`, `useFields`, `useForm`), wrap your app in `<UiduProvider>`:
475
+ Server-side `await getX(...)` calls do not need a provider. For React client-side hooks (`useQuery`, `useUiduClient`), wrap your app in `<UiduProvider>`. (`useFields` and `toText` are pure functions — they work without a provider, on the server too.)
257
476
 
258
477
  ```tsx
259
478
  // app/layout.tsx
@@ -304,13 +523,12 @@ npm create uidu-app@latest my-app -- -t events
304
523
  | `events` | Event listing + `/event/[id]` detail |
305
524
  | `stories` | Blog listing + `/story/[id]` detail (magazine layout) |
306
525
  | `donations` | Campaigns listing + `/campaign/[id]` detail with progress bars |
526
+ | `custom-app` | An app inside a uidu Space: `<UiduAppProvider>`, data in its own Models (`public/uidu.app.json`) |
307
527
 
308
- Every template ships with:
309
- - Next.js 16 App Router
310
- - Tailwind v4
311
- - A configured `src/lib/uidu.ts`
312
- - A server-rendered listing + dynamic detail page
313
- - `.env.example` with required vars (and the CLI offers to fill them in for you)
528
+ Every template ships with Next.js 16 App Router, Tailwind v4 and an `.env.example` (the CLI
529
+ offers to fill it in for you). The site templates add a configured `src/lib/uidu.ts` and a
530
+ server-rendered listing + dynamic detail page; `custom-app` has no token to configure and
531
+ reads everything in client components.
314
532
 
315
533
  ## See Also
316
534
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uidu",
3
- "version": "0.1.0",
3
+ "version": "0.5.0",
4
4
  "author": "uidu",
5
5
  "license": "MIT",
6
6
  "homepage": "https://docs.uidu.org",