@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/LICENSE +202 -202
- package/README.md +372 -338
- package/dist/cli.js +31 -1
- package/dist/index.cjs +31 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +703 -585
- package/dist/index.d.ts +703 -585
- package/dist/index.js +31 -1
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +31 -1
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +780 -719
- package/dist/react.d.ts +780 -719
- package/dist/react.js +31 -1
- package/dist/react.js.map +1 -1
- package/package.json +7 -5
- package/skills/lunnoa-client/SKILL.md +517 -0
- package/skills/lunnoa-solution-design/SKILL.md +190 -0
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
|
-
[](https://www.npmjs.com/package/@lunnoa/client)
|
|
6
|
-
[](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).
|
|
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
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
|
336
|
-
|
|
|
337
|
-
|
|
338
|
-
|
|
1
|
+
# @lunnoa/client
|
|
2
|
+
|
|
3
|
+
TypeScript SDK for building applications **on top of** [Lunnoa Automate](https://github.com/lunnoa-automate/lunnoa_automate).
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@lunnoa/client)
|
|
6
|
+
[](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
|
+
```
|