@lunnoa/client 0.0.0-development → 1.0.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/README.md CHANGED
@@ -1,338 +1,372 @@
1
- # @lunnoa/client
2
-
3
- TypeScript SDK for building applications **on top of** [Lunnoa Automate](https://github.com/lunnoa-automate/lunnoa_automate).
4
-
5
- [![npm version](https://img.shields.io/npm/v/@lunnoa/client)](https://www.npmjs.com/package/@lunnoa/client)
6
- [![license](https://img.shields.io/badge/license-Apache--2.0%20with%20Commons%20Clause-blue)](https://www.npmjs.com/package/@lunnoa/client)
7
-
8
- ## What is this?
9
-
10
- `@lunnoa/client` is the official consumption SDK for the Lunnoa Automate **Public API**: a typed client for agents and chat (with SSE streaming), workflows and executions, entities (Objects), knowledge, queues, variables, connections, and projects — plus a codegen command that types the SDK against **your** deployment's entity types, workflows, and agents.
11
-
12
- **The boundary with the other Lunnoa SDK:** [`@lunnoa/toolkit`](https://github.com/lunnoa-automate/lunnoa_toolkit) is for building integration apps that run *inside* the platform (`createAction` / `createApp`); **`@lunnoa/client` is for building applications *on top of* the platform** — including `define*` factories, `runs.start`, and CLI deploy of workflows/agents.
13
-
14
- The SDK is deliberately **presentation-free** at the core: no components or widgets. Optional **headless** React hooks ship as `@lunnoa/client/react`. You build fully custom UIs in your own design system; the SDK encodes the platform knowledge (protocols, schemas, conventions). A frontend-authoring skill for AI coding agents ships with the package (see [`.cursor/skills/lunnoa-client/SKILL.md`](./.cursor/skills/lunnoa-client/SKILL.md)).
15
-
16
- ## Installation
17
-
18
- ```bash
19
- npm install @lunnoa/client
20
- ```
21
-
22
- **Requirements:** Node.js ≥ 20 (or any runtime with `fetch` and web streams).
23
-
24
- ## Quickstart
25
-
26
- ```typescript
27
- import { LunnoaClient } from '@lunnoa/client';
28
-
29
- const lunnoa = new LunnoaClient({
30
- baseUrl: 'https://lunnoa.your-company.example',
31
- apiKey: process.env.LUNNOA_API_KEY, // lna_... — server-side only
32
- });
33
-
34
- const { data: invoices } = await lunnoa.entities.list({
35
- objectTypeSlug: 'invoice',
36
- state: 'pending',
37
- expansion: ['attributes'],
38
- });
39
- ```
40
-
41
- A SuperAdmin creates API keys in **Adminspace → API keys**, choosing the workspace and an RBAC role for the key's service account. The role is the key's entire capability model — grant the minimum permission set, not Admin.
42
-
43
- ## Two auth patterns
44
-
45
- | Pattern | Credential | Where |
46
- |---|---|---|
47
- | **A. Backend holds the key** | `apiKey: 'lna_...'` via `createServerClient` | Your server, background jobs, server-rendered pages. The key acts as a workspace-scoped service account. |
48
- | **B. End users are Lunnoa users** | User JWT via `createBrowserClient` + `lunnoa.auth.*` | Browser apps. Login, 2FA, refresh, and SSO discovery are wrapped; per-user permissions apply. |
49
-
50
- ```typescript
51
- import {
52
- createBrowserClient,
53
- createServerClient,
54
- isLoginRequires2FA,
55
- } from '@lunnoa/client';
56
-
57
- // Pattern A — server-side
58
- const server = createServerClient({
59
- baseUrl,
60
- apiKey: process.env.LUNNOA_API_KEY!,
61
- });
62
-
63
- // Pattern B — browser (tokens in localStorage by default; auto-refresh on 401)
64
- const lunnoa = createBrowserClient({ baseUrl });
65
- const result = await lunnoa.auth.login({ email, password });
66
- if (isLoginRequires2FA(result)) {
67
- await lunnoa.auth.verify2faLogin({
68
- sessionToken: result.sessionToken,
69
- token: totpCode,
70
- });
71
- }
72
- const me = await lunnoa.auth.me();
73
- ```
74
-
75
- **API keys never ship to browsers.** The SDK enforces this: constructing a client with an `lna_` key while `window` exists throws immediately. Browser apps either use Pattern B or call your own backend, which holds the key (backend-for-frontend).
76
-
77
- For Pattern B the deployment must allow your origin via its `CORS_ALLOWED_ORIGINS` configuration. See the Authentication guide under `guides/develop/client/auth` in Lunnoa Docs for SSO redirect constraints and token stores.
78
-
79
- ### React (optional)
80
-
81
- ```tsx
82
- import {
83
- LunnoaAuthProvider,
84
- useLunnoaAuth,
85
- useLunnoaClient,
86
- useExecutionProgress,
87
- useNeedsInput,
88
- useAgentChat,
89
- useApprovalsInbox,
90
- useEntityList,
91
- } from '@lunnoa/client/react';
92
-
93
- <LunnoaAuthProvider baseUrl={baseUrl}>
94
- <App />
95
- </LunnoaAuthProvider>
96
-
97
- const { user, status, login, logout } = useLunnoaAuth();
98
- const lunnoa = useLunnoaClient();
99
- const { progress, status: progressStatus } = useExecutionProgress(executionId);
100
- const { waiting, pendingInput, submitInput } = useNeedsInput(progress);
101
- const { messages, send, stop, taskId } = useAgentChat(agentId); // needs @ai-sdk/react
102
- const { items, decide } = useApprovalsInbox({ pollIntervalMs: 15_000 });
103
- const { data, page, setPage } = useEntityList({ objectTypeSlug: 'invoice' });
104
- ```
105
-
106
- Peers: `react` ≥ 18 (required for `/react`); `@ai-sdk/react` only for `useAgentChat`. Core `@lunnoa/client` stays headless.
107
-
108
- ## Resources
109
-
110
- Most namespaces map to the Public API surface (`openapi.public.json`):
111
-
112
- `auth` · `agents` · `tasks` · `agentChat` (SSE streaming) · `workflows` · `executions` · `actions` · `entities` · `entityTypes` · `knowledge` · `queues` · `queueItems` · `variables` · `connections` · `projects` · `workflowApps` · `discovery`
113
-
114
- `approvals` uses the authenticated **workspace** API (`/api/approvals/...`), not Public API v1. The inbox is filtered to requests where the current user is an eligible approver.
115
-
116
- Errors are `LunnoaApiError` with `status`, `body`, and convenience flags (`isUnauthorized`, `isForbidden`, `isRateLimited`). Paginated lists have async iteration (`entities.iterate(...)`, `queueItems.iterate(...)`).
117
-
118
- ## Worked examples
119
-
120
- Runnable versions live in [`examples/`](./examples).
121
-
122
- ### Query and create entities
123
-
124
- ```typescript
125
- const invoiceType = await lunnoa.entityTypes.getBySlug('invoice', {
126
- expansion: ['attributeSchema', 'stateSchema'],
127
- });
128
-
129
- const created = await lunnoa.entities.create({
130
- name: 'INV-2026-0042',
131
- objectTypeId: invoiceType.id,
132
- attributes: { amount: 1250.5, currency: 'CHF' }, // keyed by field ID
133
- });
134
-
135
- for await (const invoice of lunnoa.entities.iterate({ objectTypeSlug: 'invoice' })) {
136
- // pages fetched lazily
137
- }
138
- ```
139
-
140
- ### Trigger a workflow and wait for the result
141
-
142
- ```typescript
143
- const { id } = await lunnoa.workflows.execute(workflowId, { customerEmail: 'x@y.z' });
144
- const execution = await lunnoa.executions.waitUntilFinished(id); // poll with backoff
145
- console.log(execution.status, execution.output);
146
-
147
- // or in one call:
148
- const done = await lunnoa.workflows.executeAndWait(workflowId, inputs);
149
- ```
150
-
151
- ### Run a single app action (ad-hoc Execution)
152
-
153
- Creates a one-step Execution with `source: SDK` (same inbox as workflow runs). Discover `appId` / `actionId` / `inputConfig` via `workflowApps.list()`. Pass `connectionId` (the Connection instance UUID from Connections) when the action needs a connection and more than one exists for the app.
154
-
155
- ```typescript
156
- const result = await lunnoa.actions.run({
157
- appId: 'http',
158
- actionId: 'http_action_send-request',
159
- // connectionId: '...', // Connection.id UUID when needed
160
- input: { method: 'GET', url: 'https://example.com' },
161
- });
162
- console.log(result.status, result.output, result.executionPath);
163
-
164
- // Same record via the Executions API:
165
- const again = await lunnoa.executions.get(result.id, {
166
- expansion: ['status', 'source', 'executionPath'],
167
- });
168
- ```
169
-
170
- ### Resume a NEEDS_INPUT execution from a custom form
171
-
172
- Workflows can pause and wait for human input. Render a form for the waiting step and resume:
173
-
174
- ```typescript
175
- const execution = await lunnoa.executions.get(executionId, {
176
- expansion: ['status', 'executionPath'],
177
- });
178
- const waiting = execution.executionPath?.find((s) => s.status === 'NEEDS_INPUT');
179
-
180
- await lunnoa.executions.submitInput(executionId, waiting!.nodeId, {
181
- approved: true,
182
- comment: 'Release the payment.',
183
- });
184
- const finished = await lunnoa.executions.waitUntilFinished(executionId);
185
- ```
186
-
187
- The `executionPath` expansion is the business-readable step list (labels, per-step status, timings, loop iterations) custom UIs render progress from.
188
-
189
- ### Approvals inbox (Request Approval nodes)
190
-
191
- List pending approvals the authenticated user can decide, then approve or reject. Prefer a user JWT (pattern B) so eligibility matches a real person. Requires `approvals:read` / `approvals:decide`.
192
-
193
- ```typescript
194
- const { items } = await lunnoa.approvals.inbox(); // pending + partially_approved
195
-
196
- for (const person of items[0]?.eligibleApprovers ?? []) {
197
- console.log(person.email, person.name); // who can still decide
198
- }
199
-
200
- await lunnoa.approvals.decide(items[0]!.approvalId, {
201
- decision: 'approved',
202
- comment: 'Within policy limits.',
203
- });
204
-
205
- // Or by execution + node when you already have those IDs:
206
- await lunnoa.executions.submitApproval(executionId, nodeId, {
207
- decision: 'rejected',
208
- comment: 'Amount exceeds limit.',
209
- });
210
- ```
211
-
212
- Do not use `submitInput` for Request Approval nodes; that path is for generic `NEEDS_INPUT` forms.
213
-
214
- ### Stream agent chat
215
-
216
- The streaming module is hand-written (code generators handle SSE poorly) and speaks the same wire protocol as the bundled UI: AI SDK `UIMessage` chunks, SSE-framed, resumable.
217
-
218
- ```typescript
219
- const taskId = crypto.randomUUID(); // first message creates the task
220
- const stream = await lunnoa.agentChat.streamMessage(agentId, taskId, 'Hello!');
221
-
222
- for await (const chunk of stream) {
223
- if (chunk.type === 'text-delta') process.stdout.write(chunk.delta);
224
- }
225
-
226
- // Reattach after a disconnect (returns null when nothing is running):
227
- const resumed = await lunnoa.agentChat.resumeStream(agentId, taskId);
228
-
229
- // Cancel a running turn (optionally persisting the partial reply):
230
- await lunnoa.agentChat.stop(agentId, taskId);
231
- ```
232
-
233
- `stream.toUIMessageStream()` returns a `ReadableStream<UIMessageChunk>` ready for the AI SDK's `readUIMessageStream` when you want assembled `UIMessage` snapshots for rendering.
234
-
235
- ## Deployment-specific codegen
236
-
237
- Entity types, workflow inputs, and agents are defined per deployment, so the generic SDK can only type them loosely. The codegen command queries your deployment and emits typed accessors — the same developer experience as Prisma client or Supabase type generation:
238
-
239
- ```bash
240
- npx @lunnoa/client codegen --url https://lunnoa.your-company.example --api-key lna_... --out ./src/lunnoa
241
- ```
242
-
243
- This writes `lunnoa.generated.ts`:
244
-
245
- ```typescript
246
- import { LunnoaClient } from '@lunnoa/client';
247
- import { createDeploymentClient } from './lunnoa/lunnoa.generated';
248
-
249
- const lunnoa = createDeploymentClient(new LunnoaClient({ baseUrl, apiKey }));
250
-
251
- // Entity attributes and states are compile-time checked:
252
- const open = await lunnoa.entities.invoice.list({ state: 'pending' });
253
- await lunnoa.entities.invoice.create({
254
- name: 'INV-1',
255
- attributes: { amount: 99.5, status: 'pending' },
256
- });
257
-
258
- // Workflow inputs typed from the manual trigger's input schema:
259
- await lunnoa.workflows.processInvoice.executeAndWait({ invoiceId: 'abc' });
260
-
261
- // Agents by name:
262
- const stream = await lunnoa.agents.supportAgent.streamMessage(taskId, 'Hi');
263
- ```
264
-
265
- Re-run codegen whenever entity types, workflows, or agents change on the deployment.
266
-
267
- ## Define, run, and CLI deploy
268
-
269
- Author local definitions with `defineWorkflow` / `defineAgent`, run linear chains with `lunnoa.runs.start`, and upsert into a project with the CLI (prefer CLI over calling upsert from app code):
270
-
271
- ```bash
272
- npx @lunnoa/client workflows deploy ./workflows/sync-orders.ts \
273
- --url https://lunnoa.example --api-key lna_... --project-id <uuid>
274
- npx @lunnoa/client agents deploy ./agents/triage.ts \
275
- --url https://lunnoa.example --api-key lna_... --project-id <uuid>
276
- ```
277
-
278
- `defineAction` binds catalogue actions only; toolkit `createAction` is for in-platform apps. See Lunnoa Docs under `guides/develop/client/define`, `runs`, and `cli-deploy`.
279
-
280
- ## Regenerating the API types
281
-
282
- The generic types are generated from the deployment's public OpenAPI spec (`spec/openapi.public.json`, produced by the platform's `export-openapi --public`):
283
-
284
- ```bash
285
- npm run codegen:spec
286
- ```
287
-
288
- ## AI-assisted UI building
289
-
290
- The repo ships a frontend-authoring skill at [`.cursor/skills/lunnoa-client/SKILL.md`](./.cursor/skills/lunnoa-client/SKILL.md) that teaches coding agents (Cursor, Claude) how to build custom UIs against this SDK: auth patterns, discovery, codegen, and the four worked recipes above. Copy it into your app's `.cursor/skills/` folder.
291
-
292
- ## Versioning and stability
293
-
294
- This package follows [semantic versioning](https://semver.org/). It targets the Lunnoa Automate **Public API v1**: endpoints marked stable by the platform's `x-public` contract. Public endpoints only change additively; breaking platform changes come with a deprecation window.
295
-
296
- ## License
297
-
298
- Licensed under **Apache-2.0 with Commons Clause**.
299
-
300
- You may use, modify, and distribute the software under Apache 2.0 terms, with one additional restriction: you may not sell the software itself (the SDK as a standalone product). Building and operating applications that *use* the SDK is permitted.
301
-
302
- ## Development
303
-
304
- ```bash
305
- git clone https://github.com/lunnoa-automate/lunnoa_client.git
306
- cd lunnoa_client
307
- pnpm install
308
- pnpm test
309
- pnpm build
310
- ```
311
-
312
- Build output (ESM + CJS + type declarations) is written to `dist/`. Only `dist/` is included in the published npm tarball.
313
-
314
- ## Publishing
315
-
316
- Releases are fully automated with [semantic-release](https://github.com/semantic-release/semantic-release), the same setup as `@lunnoa/toolkit`: merge [Conventional Commits](https://www.conventionalcommits.org/) to `master`/`main`, CI runs tests and build, and the Release workflow computes the version, updates `CHANGELOG.md`, tags, creates the GitHub release, and publishes to npm via trusted publishing (OIDC).
317
-
318
- | Commit prefix | Version bump |
319
- | --- | --- |
320
- | `fix:` | Patch |
321
- | `feat:` | Minor |
322
- | `feat!:` / `BREAKING CHANGE:` | Major |
323
-
324
- ### npm trusted publishing (OIDC)
325
-
326
- Publishing uses [npm trusted publishers](https://docs.npmjs.com/trusted-publishers/) via GitHub Actions OIDC — no `NPM_TOKEN` secret is required.
327
-
328
- Before the first release, configure a **Trusted Publisher** on [npmjs.com](https://www.npmjs.com) (org admin login for `@lunnoa`):
329
-
330
- | Field | Value |
331
- | --- | --- |
332
- | Package | `@lunnoa/client` |
333
- | Organization / user | `lunnoa-automate` |
334
- | Repository | `lunnoa_client` |
335
- | Workflow filename | `release.yml` |
336
- | Environment | *(leave empty)* |
337
-
338
- The release workflow grants `id-token: write` and uses **semantic-release v25** with Node 24.
1
+ # @lunnoa/client
2
+
3
+ TypeScript SDK for building applications **on top of** [Lunnoa Automate](https://github.com/lunnoa-automate/lunnoa_automate).
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@lunnoa/client)](https://www.npmjs.com/package/@lunnoa/client)
6
+ [![license](https://img.shields.io/badge/license-Apache--2.0%20with%20Commons%20Clause-blue)](https://www.npmjs.com/package/@lunnoa/client)
7
+
8
+ ## What is this?
9
+
10
+ `@lunnoa/client` is the official consumption SDK for the Lunnoa Automate **Public API**: a typed client for agents and chat (with SSE streaming), workflows and executions, entities (Objects), knowledge, queues, variables, connections, and projects — plus a codegen command that types the SDK against **your** deployment's entity types, workflows, and agents.
11
+
12
+ **The boundary with the other Lunnoa SDK:** [`@lunnoa/toolkit`](https://github.com/lunnoa-automate/lunnoa_toolkit) is for building integration apps that run *inside* the platform (`createAction` / `createApp`); **`@lunnoa/client` is for building applications *on top of* the platform** — including `define*` factories, `runs.start`, and CLI deploy of workflows/agents.
13
+
14
+ The SDK is deliberately **presentation-free** at the core: no components or widgets. Optional **headless** React hooks ship as `@lunnoa/client/react`. You build fully custom UIs in your own design system; the SDK encodes the platform knowledge (protocols, schemas, conventions). Agent skills for Cursor / Claude Code ship in [`skills/`](./skills/) (see [AI-assisted building](#ai-assisted-building)).
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install @lunnoa/client
20
+ ```
21
+
22
+ **Requirements:** Node.js ≥ 20 (or any runtime with `fetch` and web streams).
23
+
24
+ ## Quickstart
25
+
26
+ ```typescript
27
+ import { LunnoaClient } from '@lunnoa/client';
28
+
29
+ const lunnoa = new LunnoaClient({
30
+ baseUrl: 'https://lunnoa.your-company.example',
31
+ apiKey: process.env.LUNNOA_API_KEY, // lna_... — server-side only
32
+ });
33
+
34
+ const { data: invoices } = await lunnoa.entities.list({
35
+ objectTypeSlug: 'invoice',
36
+ state: 'pending',
37
+ expansion: ['attributes'],
38
+ });
39
+ ```
40
+
41
+ A SuperAdmin creates API keys in **Adminspace → API keys**, choosing the workspace and an RBAC role for the key's service account. The role is the key's entire capability model — grant the minimum permission set, not Admin.
42
+
43
+ ## Two auth patterns
44
+
45
+ | Pattern | Credential | Where |
46
+ |---|---|---|
47
+ | **A. Backend holds the key** | `apiKey: 'lna_...'` via `createServerClient` | Your server, background jobs, server-rendered pages. The key acts as a workspace-scoped service account. |
48
+ | **B. End users are Lunnoa users** | User JWT via `createBrowserClient` + `lunnoa.auth.*` | Browser apps. Login, 2FA, refresh, and SSO discovery are wrapped; per-user permissions apply. |
49
+
50
+ ```typescript
51
+ import {
52
+ createBrowserClient,
53
+ createServerClient,
54
+ isLoginRequires2FA,
55
+ } from '@lunnoa/client';
56
+
57
+ // Pattern A — server-side
58
+ const server = createServerClient({
59
+ baseUrl,
60
+ apiKey: process.env.LUNNOA_API_KEY!,
61
+ });
62
+
63
+ // Pattern B — browser (tokens in localStorage by default; auto-refresh on 401)
64
+ const lunnoa = createBrowserClient({ baseUrl });
65
+ const result = await lunnoa.auth.login({ email, password });
66
+ if (isLoginRequires2FA(result)) {
67
+ await lunnoa.auth.verify2faLogin({
68
+ sessionToken: result.sessionToken,
69
+ token: totpCode,
70
+ });
71
+ }
72
+ const me = await lunnoa.auth.me();
73
+ ```
74
+
75
+ **API keys never ship to browsers.** The SDK enforces this: constructing a client with an `lna_` key while `window` exists throws immediately. Browser apps either use Pattern B or call your own backend, which holds the key (backend-for-frontend).
76
+
77
+ For Pattern B the deployment must allow your origin via its `CORS_ALLOWED_ORIGINS` configuration. See the Authentication guide under `guides/develop/client/auth` in Lunnoa Docs for SSO redirect constraints and token stores.
78
+
79
+ ### React (optional)
80
+
81
+ ```tsx
82
+ import {
83
+ LunnoaAuthProvider,
84
+ useLunnoaAuth,
85
+ useLunnoaClient,
86
+ useExecutionProgress,
87
+ useNeedsInput,
88
+ useAgentChat,
89
+ useApprovalsInbox,
90
+ useEntityList,
91
+ } from '@lunnoa/client/react';
92
+
93
+ <LunnoaAuthProvider baseUrl={baseUrl}>
94
+ <App />
95
+ </LunnoaAuthProvider>
96
+
97
+ const { user, status, login, logout } = useLunnoaAuth();
98
+ const lunnoa = useLunnoaClient();
99
+ const { progress, status: progressStatus } = useExecutionProgress(executionId);
100
+ const { waiting, pendingInput, submitInput } = useNeedsInput(progress);
101
+ const { messages, send, stop, taskId } = useAgentChat(agentId); // needs @ai-sdk/react
102
+ const { items, decide } = useApprovalsInbox({ pollIntervalMs: 15_000 });
103
+ const { data, page, setPage } = useEntityList({ objectTypeSlug: 'invoice' });
104
+ ```
105
+
106
+ Peers: `react` ≥ 18 (required for `/react`); `@ai-sdk/react` only for `useAgentChat`. Core `@lunnoa/client` stays headless.
107
+
108
+ ## Resources
109
+
110
+ Most namespaces map to the Public API surface (`openapi.public.json`):
111
+
112
+ `auth` · `agents` · `tasks` · `agentChat` (SSE streaming) · `workflows` · `executions` · `actions` · `entities` · `entityTypes` · `knowledge` · `queues` · `queueItems` · `variables` · `connections` · `projects` · `workflowApps` · `discovery`
113
+
114
+ `approvals` uses the authenticated **workspace** API (`/api/approvals/...`), not Public API v1. The inbox is filtered to requests where the current user is an eligible approver.
115
+
116
+ Errors are `LunnoaApiError` with `status`, `body`, and convenience flags (`isUnauthorized`, `isForbidden`, `isRateLimited`). Paginated lists have async iteration (`entities.iterate(...)`, `queueItems.iterate(...)`).
117
+
118
+ ## Worked examples
119
+
120
+ Runnable versions live in [`examples/`](./examples).
121
+
122
+ ### Query and create entities
123
+
124
+ ```typescript
125
+ const invoiceType = await lunnoa.entityTypes.getBySlug('invoice', {
126
+ expansion: ['attributeSchema', 'stateSchema'],
127
+ });
128
+
129
+ const created = await lunnoa.entities.create({
130
+ name: 'INV-2026-0042',
131
+ objectTypeId: invoiceType.id,
132
+ attributes: { amount: 1250.5, currency: 'CHF' }, // keyed by field ID
133
+ });
134
+
135
+ for await (const invoice of lunnoa.entities.iterate({ objectTypeSlug: 'invoice' })) {
136
+ // pages fetched lazily
137
+ }
138
+ ```
139
+
140
+ ### Trigger a workflow and wait for the result
141
+
142
+ ```typescript
143
+ const { id } = await lunnoa.workflows.execute(workflowId, { customerEmail: 'x@y.z' });
144
+ const execution = await lunnoa.executions.waitUntilFinished(id); // poll with backoff
145
+ console.log(execution.status, execution.output);
146
+
147
+ // or in one call:
148
+ const done = await lunnoa.workflows.executeAndWait(workflowId, inputs);
149
+ ```
150
+
151
+ ### Run a single app action (ad-hoc Execution)
152
+
153
+ Creates a one-step Execution with `source: SDK` (same inbox as workflow runs). Discover `appId` / `actionId` / `inputConfig` via `workflowApps.list()`. Pass `connectionId` (the Connection instance UUID from Connections) when the action needs a connection and more than one exists for the app.
154
+
155
+ ```typescript
156
+ const result = await lunnoa.actions.run({
157
+ appId: 'http',
158
+ actionId: 'http_action_send-request',
159
+ // connectionId: '...', // Connection.id UUID when needed
160
+ input: { method: 'GET', url: 'https://example.com' },
161
+ });
162
+ console.log(result.status, result.output, result.executionPath);
163
+
164
+ // Same record via the Executions API:
165
+ const again = await lunnoa.executions.get(result.id, {
166
+ expansion: ['status', 'source', 'executionPath'],
167
+ });
168
+ ```
169
+
170
+ ### Resume a NEEDS_INPUT execution from a custom form
171
+
172
+ Workflows can pause and wait for human input. Render a form for the waiting step and resume:
173
+
174
+ ```typescript
175
+ const execution = await lunnoa.executions.get(executionId, {
176
+ expansion: ['status', 'executionPath'],
177
+ });
178
+ const waiting = execution.executionPath?.find((s) => s.status === 'NEEDS_INPUT');
179
+
180
+ await lunnoa.executions.submitInput(executionId, waiting!.nodeId, {
181
+ approved: true,
182
+ comment: 'Release the payment.',
183
+ });
184
+ const finished = await lunnoa.executions.waitUntilFinished(executionId);
185
+ ```
186
+
187
+ The `executionPath` expansion is the business-readable step list (labels, per-step status, timings, loop iterations) custom UIs render progress from.
188
+
189
+ ### Approvals inbox (Request Approval nodes)
190
+
191
+ List pending approvals the authenticated user can decide, then approve or reject. Prefer a user JWT (pattern B) so eligibility matches a real person. Requires `approvals:read` / `approvals:decide`.
192
+
193
+ ```typescript
194
+ const { items } = await lunnoa.approvals.inbox(); // pending + partially_approved
195
+
196
+ for (const person of items[0]?.eligibleApprovers ?? []) {
197
+ console.log(person.email, person.name); // who can still decide
198
+ }
199
+
200
+ await lunnoa.approvals.decide(items[0]!.approvalId, {
201
+ decision: 'approved',
202
+ comment: 'Within policy limits.',
203
+ });
204
+
205
+ // Or by execution + node when you already have those IDs:
206
+ await lunnoa.executions.submitApproval(executionId, nodeId, {
207
+ decision: 'rejected',
208
+ comment: 'Amount exceeds limit.',
209
+ });
210
+ ```
211
+
212
+ Do not use `submitInput` for Request Approval nodes; that path is for generic `NEEDS_INPUT` forms.
213
+
214
+ ### Stream agent chat
215
+
216
+ The streaming module is hand-written (code generators handle SSE poorly) and speaks the same wire protocol as the bundled UI: AI SDK `UIMessage` chunks, SSE-framed, resumable.
217
+
218
+ ```typescript
219
+ const taskId = crypto.randomUUID(); // first message creates the task
220
+ const stream = await lunnoa.agentChat.streamMessage(agentId, taskId, 'Hello!');
221
+
222
+ for await (const chunk of stream) {
223
+ if (chunk.type === 'text-delta') process.stdout.write(chunk.delta);
224
+ }
225
+
226
+ // Reattach after a disconnect (returns null when nothing is running):
227
+ const resumed = await lunnoa.agentChat.resumeStream(agentId, taskId);
228
+
229
+ // Cancel a running turn (optionally persisting the partial reply):
230
+ await lunnoa.agentChat.stop(agentId, taskId);
231
+ ```
232
+
233
+ `stream.toUIMessageStream()` returns a `ReadableStream<UIMessageChunk>` ready for the AI SDK's `readUIMessageStream` when you want assembled `UIMessage` snapshots for rendering.
234
+
235
+ ## Deployment-specific codegen
236
+
237
+ Entity types, workflow inputs, and agents are defined per deployment, so the generic SDK can only type them loosely. The codegen command queries your deployment and emits typed accessors — the same developer experience as Prisma client or Supabase type generation:
238
+
239
+ ```bash
240
+ npx @lunnoa/client codegen --url https://lunnoa.your-company.example --api-key lna_... --out ./src/lunnoa
241
+ ```
242
+
243
+ This writes `lunnoa.generated.ts`:
244
+
245
+ ```typescript
246
+ import { LunnoaClient } from '@lunnoa/client';
247
+ import { createDeploymentClient } from './lunnoa/lunnoa.generated';
248
+
249
+ const lunnoa = createDeploymentClient(new LunnoaClient({ baseUrl, apiKey }));
250
+
251
+ // Entity attributes and states are compile-time checked:
252
+ const open = await lunnoa.entities.invoice.list({ state: 'pending' });
253
+ await lunnoa.entities.invoice.create({
254
+ name: 'INV-1',
255
+ attributes: { amount: 99.5, status: 'pending' },
256
+ });
257
+
258
+ // Workflow inputs typed from the manual trigger's input schema:
259
+ await lunnoa.workflows.processInvoice.executeAndWait({ invoiceId: 'abc' });
260
+
261
+ // Agents by name:
262
+ const stream = await lunnoa.agents.supportAgent.streamMessage(taskId, 'Hi');
263
+ ```
264
+
265
+ Re-run codegen whenever entity types, workflows, or agents change on the deployment.
266
+
267
+ ## Define, run, and CLI deploy
268
+
269
+ Author local definitions with `defineWorkflow` / `defineAgent`, run linear chains with `lunnoa.runs.start`, and upsert into a project with the CLI (prefer CLI over calling upsert from app code):
270
+
271
+ ```bash
272
+ npx @lunnoa/client workflows deploy ./workflows/sync-orders.ts \
273
+ --url https://lunnoa.example --api-key lna_... --project-id <uuid>
274
+ npx @lunnoa/client agents deploy ./agents/triage.ts \
275
+ --url https://lunnoa.example --api-key lna_... --project-id <uuid>
276
+ ```
277
+
278
+ `defineAction` binds catalogue actions only; toolkit `createAction` is for in-platform apps. See Lunnoa Docs under `guides/develop/client/define`, `runs`, and `cli-deploy`.
279
+
280
+ ## Regenerating the API types
281
+
282
+ The generic types are generated from the deployment's public OpenAPI spec (`spec/openapi.public.json`, produced by the platform's `export-openapi --public`):
283
+
284
+ ```bash
285
+ npm run codegen:spec
286
+ ```
287
+
288
+ ## AI-assisted building
289
+
290
+ Two Cursor / Claude Code skills ship in the published package under `skills/`:
291
+
292
+ | Skill | Path | Role |
293
+ |---|---|---|
294
+ | **lunnoa-solution-design** | [`skills/lunnoa-solution-design/SKILL.md`](./skills/lunnoa-solution-design/SKILL.md) | Interview the human, map intent to Lunnoa primitives, produce a build brief |
295
+ | **lunnoa-client** | [`skills/lunnoa-client/SKILL.md`](./skills/lunnoa-client/SKILL.md) | Implement with auth, discovery, codegen, `define*` / `runs` / CLI deploy, React hooks |
296
+
297
+ After install, copy or enable them from `node_modules/@lunnoa/client/skills/` into your app’s `.cursor/skills/` (or Claude Code skills directory). Prefer **solution-design** first when requirements are unclear, then **lunnoa-client**.
298
+
299
+ Partner paste block:
300
+
301
+ ```text
302
+ Use @lunnoa/client skills: lunnoa-solution-design then lunnoa-client.
303
+ LUNNOA_URL=… LUNNOA_API_KEY=lna_… (server only). Project ID=…
304
+ Start by interviewing me, then workflowApps.list() / codegen.
305
+ ```
306
+
307
+ Docs quickstart: [For agents](https://docs.lunnoa.com/guides/develop/client/for-agents) (or your deployment’s docs site).
308
+
309
+ ## Versioning and stability
310
+
311
+ This package follows [semantic versioning](https://semver.org/). It targets the Lunnoa Automate **Public API v1**: endpoints marked stable by the platform's `x-public` contract. Public endpoints only change additively; breaking platform changes come with a deprecation window.
312
+
313
+ ## License
314
+
315
+ Licensed under **Apache-2.0 with Commons Clause**.
316
+
317
+ You may use, modify, and distribute the software under Apache 2.0 terms, with one additional restriction: you may not sell the software itself (the SDK as a standalone product). Building and operating applications that *use* the SDK is permitted.
318
+
319
+ ## Development
320
+
321
+ ```bash
322
+ git clone https://github.com/lunnoa-automate/lunnoa_client.git
323
+ cd lunnoa_client
324
+ pnpm install
325
+ pnpm test
326
+ pnpm build
327
+ ```
328
+
329
+ Build output (ESM + CJS + type declarations) is written to `dist/`. The published npm tarball includes `dist/` and `skills/`.
330
+
331
+ ## Publishing
332
+
333
+ Releases are fully automated with [semantic-release](https://github.com/semantic-release/semantic-release), the same setup as `@lunnoa/toolkit`: merge [Conventional Commits](https://www.conventionalcommits.org/) to `master`/`main`, CI runs tests and build, and the Release workflow computes the version, updates `CHANGELOG.md`, tags, creates the GitHub release, and publishes to npm via trusted publishing (OIDC).
334
+
335
+ | Commit prefix | Version bump |
336
+ | --- | --- |
337
+ | `fix:` | Patch |
338
+ | `feat:` | Minor |
339
+ | `feat!:` / `BREAKING CHANGE:` | Major |
340
+
341
+ ### npm trusted publishing (OIDC)
342
+
343
+ Publishing uses [npm trusted publishers](https://docs.npmjs.com/trusted-publishers/) via GitHub Actions OIDC — no long-lived `NPM_TOKEN` is required after the package exists.
344
+
345
+ **Chicken-and-egg for the first publish:** npm only lets you attach a Trusted Publisher to a package that already exists. Bootstrap once (token or placeholder publish), then configure OIDC and prefer that for all later releases.
346
+
347
+ 1. Publish the initial version (e.g. via the `Bootstrap lunnoa_client publish` workflow in `lunnoa_automate`, or `NPM_TOKEN` on the Release workflow).
348
+ 2. On [npmjs.com](https://www.npmjs.com) (org admin for `@lunnoa`), open the package **Access** tab and add a Trusted Publisher:
349
+
350
+ | Field | Value |
351
+ | --- | --- |
352
+ | Package | `@lunnoa/client` |
353
+ | Organization / user | `lunnoa-automate` |
354
+ | Repository | `lunnoa_client` |
355
+ | Workflow filename | `release.yml` |
356
+ | Environment | *(leave empty)* |
357
+
358
+ 3. After OIDC works, you can remove `NPM_TOKEN` from the Release workflow env.
359
+
360
+ The release workflow grants `id-token: write` and uses **semantic-release v25** with Node 24.
361
+
362
+ ### Acceptance soak
363
+
364
+ Against a real deployment (service-account API key with at least `entities:read`, `entities:create`, `agents:use`):
365
+
366
+ ```bash
367
+ LUNNOA_URL=https://your-deployment \
368
+ LUNNOA_API_KEY=lna_... \
369
+ AGENT_ID=<agent-uuid> \
370
+ ENTITY_TYPE_SLUG=<optional-slug> \
371
+ pnpm exec tsx scripts/acceptance-soak.ts
372
+ ```