@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 +1 -1
- package/skills/uidu/SKILL.md +241 -23
- package/skills/uidu/metadata.json +1 -1
package/package.json
CHANGED
package/skills/uidu/SKILL.md
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: uidu
|
|
3
|
-
description: Use when building uidu
|
|
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.
|
|
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
|
|
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/
|
|
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
|
-
-
|
|
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
|
-
|
|
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
|
|
374
|
+
// Client: render it
|
|
229
375
|
'use client';
|
|
230
|
-
import {
|
|
376
|
+
import { DynamicForm } from '@uidu/react';
|
|
377
|
+
import { submitForm } from '@/lib/actions';
|
|
231
378
|
|
|
232
379
|
export function ContactForm({ form }) {
|
|
233
|
-
|
|
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`, `
|
|
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
|
-
|
|
310
|
-
-
|
|
311
|
-
|
|
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
|
|