@oaiz/sdk 0.0.0-stage → 0.1.4128

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OAIZ AB
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,485 @@
1
- # Temporary Holding Version
1
+ # @oaiz/sdk
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![CI](https://github.com/oaiz-io/oaiz-sdk/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/oaiz-io/oaiz-sdk/actions/workflows/ci.yaml)
4
+
5
+ The TypeScript client for apps built on [OAIZ](https://oaiz.io). It reads and writes the rows of
6
+ an OAIZ database, runs your integrations and OAIZ's built-in tools, starts workflows, publishes
7
+ events, calls a model, creates embeddings, and chats with an OAIZ agent, in the browser as a
8
+ signed-in member of your organisation or on your server with an API key.
9
+
10
+ ```sh
11
+ npm install @oaiz/sdk
12
+ ```
13
+
14
+ ## In the browser, in an OAIZ app
15
+
16
+ Give the client your app ID. It signs in a member of your organisation, and every request then runs
17
+ as that member, with their access. Where the IDs come from, and where sign-in returns, is under
18
+ [Setup](#setup).
19
+
20
+ ```ts
21
+ // src/oaiz.ts
22
+ import { createClient } from "@oaiz/sdk";
23
+
24
+ export const oaiz = createClient({
25
+ url: "https://api.oaiz.io",
26
+ database: "<database id>",
27
+ app: "<app id>",
28
+ });
29
+
30
+ export async function openTodos() {
31
+ // Every request needs a signed-in member; without one OAIZ answers 401.
32
+ const { data: session } = await oaiz.auth.getSession();
33
+ if (session === null) {
34
+ // The browser leaves for OAIZ here and comes back to this page, signed in.
35
+ const { error } = await oaiz.auth.signIn();
36
+ if (error) throw new Error(error.message);
37
+ return [];
38
+ }
39
+ const { data, error } = await oaiz.from("todos").select("id, title").eq("done", false);
40
+ if (error) throw new Error(error.message);
41
+ return data;
42
+ }
43
+ ```
44
+
45
+ ```ts
46
+ // src/session.ts
47
+ import { oaiz } from "./oaiz";
48
+
49
+ export async function whoIsSignedIn() {
50
+ const { data: session } = await oaiz.auth.getSession();
51
+ if (session === null) return null;
52
+ const { data: user } = await oaiz.auth.getUser();
53
+ return user?.name ?? user?.email ?? null;
54
+ }
55
+
56
+ export async function signOut() {
57
+ await oaiz.auth.signOut();
58
+ }
59
+ ```
60
+
61
+ The session lives in the tab's `sessionStorage`, and the client renews it before it expires.
62
+
63
+ ## On the server, with an API key
64
+
65
+ Give the client an API key instead of an app ID. Keep the key on the server; never ship it to a
66
+ browser. This client has no `auth` and no `chat`; everything else on this page works with both.
67
+
68
+ ```ts
69
+ // server/oaiz.ts
70
+ import { createClient } from "@oaiz/sdk";
71
+
72
+ export const oaiz = createClient({
73
+ url: "https://api.oaiz.io",
74
+ database: "<database id>",
75
+ key: process.env.OAIZ_API_KEY ?? "",
76
+ });
77
+ ```
78
+
79
+ ## Query a database
80
+
81
+ ```ts
82
+ // src/todos.ts
83
+ import { oaiz } from "./oaiz";
84
+
85
+ export async function addTodo(title: string) {
86
+ return oaiz.from("todos").insert({ title, done: false }).select("id, title").single();
87
+ }
88
+
89
+ export async function finishTodo(id: string) {
90
+ return oaiz.from("todos").update({ done: true }).eq("id", id);
91
+ }
92
+
93
+ export async function latestTodos() {
94
+ return oaiz.from("todos").select("id, title, done").order("created_at", { ascending: false }).limit(20);
95
+ }
96
+ ```
97
+
98
+ `upsert` and `delete` work the same way. A database view only reads.
99
+
100
+ `oaiz db types <database id> -o src/databases/<database id>.ts` writes a `Database` type for your
101
+ tables. Pass it as `createClient<Database>({ ... })` to check table names, columns and values as
102
+ you type.
103
+
104
+ `rpc` calls a function you created in the database with `create function`, with its arguments by
105
+ name. See [queries](https://oaiz.io/docs/databases/queries#rpc).
106
+
107
+ ```ts
108
+ // src/functions.ts
109
+ import { oaiz } from "./oaiz";
110
+
111
+ export async function openCount() {
112
+ const { data, error } = await oaiz.rpc("count_open_todos", { owner_id: "<member id>" });
113
+ return error ? 0 : data;
114
+ }
115
+ ```
116
+
117
+ Ask for `count` to get the number of matching rows:
118
+
119
+ ```ts
120
+ // server/report.ts
121
+ import { oaiz } from "./oaiz.js";
122
+
123
+ export async function doneCount() {
124
+ const { count, error } = await oaiz.from("todos").select("id", { count: "exact" }).eq("done", true);
125
+ return error ? null : count;
126
+ }
127
+ ```
128
+
129
+ ## Use your integrations
130
+
131
+ Every integration you connect in OAIZ (Gmail, Slack, HubSpot, ...) is a set of tools. `tools.run`
132
+ runs one by `<integration id>:<action>`, with the action's arguments. Reads and writes are separate
133
+ switches: `executeIntegrationQuery` lets the call read from the provider and
134
+ `executeIntegrationCommand` lets it write. A switch that is off makes the call return the
135
+ provider's sample output instead, so a script cannot write by accident.
136
+
137
+ On the server, read the latest unread emails, have a model write a reply, and save it as a draft.
138
+ For a longer job, start a workflow instead (see [Run a workflow](#run-a-workflow)).
139
+
140
+ ```ts
141
+ // server/inbox.ts
142
+ import { oaiz } from "./oaiz.js";
143
+
144
+ const gmail = "<gmail integration id>";
145
+
146
+ export async function unreadEmails() {
147
+ const { data, error } = await oaiz.tools.run(`${gmail}:search_emails`, {
148
+ arguments: { query: "is:unread newer_than:1d", limit: 5 },
149
+ executeIntegrationQuery: true,
150
+ executeIntegrationCommand: false,
151
+ });
152
+ if (error) throw new Error(error.message);
153
+ return data.result?.emails;
154
+ }
155
+
156
+ export async function writeReply(email: unknown) {
157
+ const { data, error } = await oaiz.llm.call({
158
+ model: "openai/gpt-6-luna",
159
+ system: "You answer customer email for Acme. Write only the reply, brief and concrete.",
160
+ input: JSON.stringify(email),
161
+ });
162
+ if (error) throw new Error(error.message);
163
+ return data.text;
164
+ }
165
+
166
+ export async function saveDraft(to: string, subject: string, body: string) {
167
+ return oaiz.tools.run(`${gmail}:create_draft`, {
168
+ arguments: { to: [to], subject, body },
169
+ executeIntegrationQuery: true,
170
+ executeIntegrationCommand: true,
171
+ });
172
+ }
173
+ ```
174
+
175
+ In the browser, give the agent the Gmail tools and let it read and draft itself. A tool with
176
+ `requireApproval` does not run until the member approves the call: the turn ends with a
177
+ `tool.approvalRequired` event, and `chat.approve` or `chat.decline`, with the same agent settings,
178
+ continues it. The app reaches only the integration actions shared with it (see [Setup](#setup)).
179
+
180
+ ```ts
181
+ // src/inbox.ts
182
+ import { oaiz } from "./oaiz";
183
+
184
+ const gmail = "<gmail integration id>";
185
+ const agent = {
186
+ systemPrompt: "You answer customer email for Acme. Be brief and concrete.",
187
+ integrationTools: [
188
+ { integrationUuid: gmail, actionType: "search_emails" },
189
+ { integrationUuid: gmail, actionType: "create_draft", requireApproval: true },
190
+ ],
191
+ };
192
+
193
+ // Returns the draft call that waits for the member, or null when the agent saved none.
194
+ export async function draftReplies() {
195
+ let waiting: { sessionId: string; toolCallId: string } | null = null;
196
+ for await (const { data: event, error } of oaiz.chat.send({ ...agent, content: "Draft replies to today's unread email." })) {
197
+ if (error) throw new Error(error.message);
198
+ if (event.type === "tool.approvalRequired") {
199
+ waiting = { sessionId: event.sessionId ?? "", toolCallId: event.toolApprovalInfo?.toolCallId ?? "" };
200
+ }
201
+ }
202
+ return waiting;
203
+ }
204
+
205
+ export async function decide(call: { sessionId: string; toolCallId: string }, approved: boolean) {
206
+ const turn = approved ? oaiz.chat.approve({ ...agent, ...call }) : oaiz.chat.decline({ ...agent, ...call });
207
+ for await (const { error } of turn) {
208
+ if (error) throw new Error(error.message);
209
+ }
210
+ }
211
+ ```
212
+
213
+ Built-in OAIZ tools run the same way, by name. In the browser, a chat turn names them in
214
+ `nativeTools`, and `chat.tools()` lists the built-in tools and integration actions the app may use.
215
+
216
+ ```ts
217
+ // server/places.ts
218
+ import { oaiz } from "./oaiz.js";
219
+
220
+ export async function coffeeNearby() {
221
+ const { data, error } = await oaiz.tools.run("google_maps_search", {
222
+ arguments: { query: "coffee near Stureplan, Stockholm", limit: 3 },
223
+ executeIntegrationQuery: true,
224
+ executeIntegrationCommand: false,
225
+ });
226
+ return error ? null : data.result;
227
+ }
228
+ ```
229
+
230
+ ## Run a workflow
231
+
232
+ `runs.start` starts a workflow's job from an event you describe; the event's `properties` are the
233
+ workflow's input. It yields the run's progress until the run ends or waits, for example for an
234
+ approval. The first frame is `root_start` with the run's ID in `actionExecutionLogUuid`. The last
235
+ is `terminal`, or `waiting` when the run waits, and its `terminalReason` says why: `success`,
236
+ `failure`, `awaiting-approval` and so on. In the browser, the app starts only the workflows shared
237
+ with it. A workflow is a set of jobs; `runs.start` takes the ID of one of its jobs
238
+ (`oaiz workflow job list <workflow id>`).
239
+
240
+ ```ts
241
+ // server/runs.ts
242
+ import { oaiz } from "./oaiz.js";
243
+
244
+ export async function weeklyReport(week: number) {
245
+ const run = oaiz.runs.start("<job id>", {
246
+ event: { eventType: { provider: "custom", name: "report-requested" }, properties: { week } },
247
+ });
248
+ let reason = "";
249
+ for await (const { data: progress, error } of run) {
250
+ if (error) throw new Error(error.message);
251
+ reason = progress.event.terminalReason ?? reason;
252
+ }
253
+ return reason;
254
+ }
255
+
256
+ // Reads a run again later, with its logs and results.
257
+ export async function readRun(runId: string) {
258
+ const { data, error } = await oaiz.runs.get(runId);
259
+ return error ? null : data.status;
260
+ }
261
+ ```
262
+
263
+ Every frame carries an `after`. When the stream breaks, `runs.follow(<run id>, { after })` reads
264
+ on from the last frame you kept; following never starts the run again. `runs.list` lists runs, for
265
+ example the ones an event started, with `triggeringEventUuid`.
266
+
267
+ ## Publish an event
268
+
269
+ An event's type is a `provider` and a `name`, each made of letters, digits and hyphens. Use the
270
+ provider `custom` for your own events; `storage`, `schedule` and the names of integration providers
271
+ such as `gmail` are reserved. Workflows that listen for the type start on it.
272
+
273
+ ```ts
274
+ // server/events.ts
275
+ import { oaiz } from "./oaiz.js";
276
+
277
+ export async function orderCreated(orderId: string, total: number) {
278
+ const { data, error } = await oaiz.events.publish({
279
+ eventType: { provider: "custom", name: "order-created" },
280
+ properties: { orderId, total },
281
+ });
282
+ if (error) throw new Error(error.message);
283
+ return data.uuid;
284
+ }
285
+ ```
286
+
287
+ ## Call a model
288
+
289
+ `llm.call` asks a model for one answer to `input`, under an optional `system` instruction, and
290
+ returns the `text`, the `model` that answered and the `usage` in tokens. The call runs as the key's
291
+ or the signed-in member's organisation, within its budget, on a model the organisation permits in
292
+ both chat and workflows; a refusal is `error.code` `budget-exceeded` or `llm-model-not-permitted`.
293
+ An `input` longer than the model's context answers `llm-call-context-length`. The call waits up to
294
+ five minutes for the answer.
295
+
296
+ ```ts
297
+ // server/summary.ts
298
+ import { oaiz } from "./oaiz.js";
299
+
300
+ export async function summarise(text: string) {
301
+ const { data, error } = await oaiz.llm.call({
302
+ model: "openai/gpt-6-luna",
303
+ system: "Summarise the text in one sentence.",
304
+ input: text,
305
+ });
306
+ if (error) throw new Error(error.message);
307
+ return data.text;
308
+ }
309
+ ```
310
+
311
+ ## Create embeddings
312
+
313
+ `embeddings.create` turns each text into a vector, with `text-embedding-3-small` or
314
+ `text-embedding-3-large`; `dimensions` asks for shorter vectors. Vectors from different models, or
315
+ from one model at different dimensions, are not comparable, so store the model beside each vector.
316
+ `JSON.stringify(vector)` is the text a pgvector column takes.
317
+
318
+ ```ts
319
+ // server/embeddings.ts
320
+ import { oaiz } from "./oaiz.js";
321
+
322
+ export async function saveArticle(title: string, text: string) {
323
+ const { data, error } = await oaiz.embeddings.create({ model: "text-embedding-3-small", input: [text] });
324
+ if (error) throw new Error(error.message);
325
+ // data: { model, dimensions, embeddings, inputTokens }
326
+ return oaiz.from("articles").insert({ title, model: data.model, embedding: JSON.stringify(data.embeddings[0]) });
327
+ }
328
+ ```
329
+
330
+ ## Chat with an agent
331
+
332
+ In the browser, as the signed-in member. `chat.send` streams the agent's answer as events.
333
+ `message.delta` events carry the text; `session.created` carries the ID of a new chat, which you
334
+ pass as `sessionId` to continue it.
335
+
336
+ ```ts
337
+ // src/chat.ts
338
+ import { oaiz } from "./oaiz";
339
+
340
+ export async function ask(question: string, onText: (text: string) => void) {
341
+ let answer = "";
342
+ const turn = oaiz.chat.send({ content: question, systemPrompt: "You help the team plan their week." });
343
+ for await (const { data: event, error } of turn) {
344
+ if (error) throw new Error(error.message);
345
+ if (event.type === "message.delta") {
346
+ answer += event.delta?.content ?? "";
347
+ onText(answer);
348
+ }
349
+ }
350
+ return answer;
351
+ }
352
+ ```
353
+
354
+ ## Test mode: `@oaiz/sdk/test`
355
+
356
+ Test mode runs your app against local copies of your databases, with no network. You need an OAIZ
357
+ account once, to write each database's schema file. The `oaizTest` Vite plugin serves sign-in, rows
358
+ and database functions from [PGlite](https://pglite.dev) while the dev server runs as
359
+ `vite --mode oaiz-test`. It runs in Node only.
360
+
361
+ ```sh
362
+ npm install --save-dev vite @electric-sql/pglite @electric-sql/pglite-pgvector
363
+ ```
364
+
365
+ ```ts
366
+ // vite.config.ts
367
+ import { oaizTest } from "@oaiz/sdk/test";
368
+ import { defineConfig } from "vite";
369
+
370
+ export default defineConfig({ plugins: [oaizTest()] });
371
+ ```
372
+
373
+ The plugin reads your app's `tests/` folder:
374
+
375
+ - `tests/databases/<database id>.sql`, the database's schema. Write it with
376
+ `oaiz db test-schema <database id> -o tests/databases/<database id>.sql`; a hand-written
377
+ `create table` file does not work, because the file also sets up the roles test mode signs in
378
+ with.
379
+ - `tests/databases/<database id>.seed.sql` (optional), SQL that inserts the starting rows.
380
+ - `tests/members.json`, the people who can sign in:
381
+
382
+ ```json
383
+ {
384
+ "teams": [{ "id": "7e570000-0000-4000-8000-000000000101", "name": "Sales" }],
385
+ "members": [
386
+ {
387
+ "id": "7e570000-0000-4000-8000-000000000001",
388
+ "name": "Ada Lovelace",
389
+ "email": "ada@example.test",
390
+ "teams": ["7e570000-0000-4000-8000-000000000101"],
391
+ "role": "administrator"
392
+ }
393
+ ],
394
+ "grants": { "<database id>": "view" }
395
+ }
396
+ ```
397
+
398
+ Only `members` is required. Member and team IDs are UUIDs, as in OAIZ. A member's `role` is
399
+ `member` (the default) or `administrator`, and its `teams` lists IDs from the top-level `teams`.
400
+ `grants` sets the app's access per database; a database left out gets `edit`.
401
+
402
+ The plugin reads the folder on the first request, so after you change a file in `tests/`, restart
403
+ the dev server.
404
+
405
+ In test mode, set the client's `url` to `` `${window.location.origin}/__oaiz-test` ``. `signIn()`
406
+ then opens a local page where you pick a member. Chat, tools, model calls, embeddings, events
407
+ and workflow runs are not supported there and answer `501` with `error.code` `test-mode-unsupported`.
408
+ When a file in `tests/` does not load, the request answers `500` with `error.code`
409
+ `test-mode-setup` and the reason in `error.message`, and the dev server prints the reason.
410
+
411
+ ## Setup
412
+
413
+ Each ID comes from the `oaiz` command-line tool or the OAIZ web app.
414
+
415
+ 1. **An OAIZ account.** Install the CLI ([install](https://oaiz.io/docs/cli#install)) and sign in
416
+ with `oaiz login` ([authentication](https://oaiz.io/docs/cli#authentication)).
417
+ 2. **A database ID.** `oaiz db create --name todos` creates a database and prints its ID. See
418
+ [databases](https://oaiz.io/docs/databases) for tables and migrations.
419
+ 3. **An app ID**, for code that runs in the browser. `oaiz view create "Todos"` creates the app
420
+ and prints its ID; the CLI calls an app a *view* and saves the ID as `viewId` in
421
+ `.oaiz/view.json`. Give the app access to the database with
422
+ `oaiz db access grant <database id> --subject-kind app --subject-id <app id> --capability edit`
423
+ (or `view` for read only).
424
+ 4. **An API key**, for code that runs on your server only. Create one in the OAIZ web app under
425
+ Settings → API → API keys ([API keys](https://oaiz.io/docs/api-keys#create)).
426
+
427
+ **What the app reaches.** In the browser, the app reaches only what you share with it, and the
428
+ member only what they may use:
429
+
430
+ - integration actions:
431
+ `oaiz integration access grant <integration id> --subject-kind app --subject-id <app id> --capability edit --tool-scope selected --action-types search_emails,create_draft`;
432
+ - workflows: `oaiz workflow access grant <workflow id> --subject-kind app --subject-id <app id> --capability view`;
433
+ - built-in tools: `oaiz app tools set <app id> --body '{"tools":["google_maps_search"]}'`.
434
+
435
+ **Where sign-in returns.** `signIn()` sends the browser to OAIZ, and OAIZ sends it back to the
436
+ same page, signed in. That page must be served from one of these addresses:
437
+
438
+ - the app's published address, or an HTTPS address you add with
439
+ `oaiz app origins add <app id> --origin https://todos.example.com`; the member needs view
440
+ access to the app;
441
+ - your local development server or a draft's preview while you develop; the member needs edit
442
+ access to the app. On your local development server, OAIZ asks them to approve every sign-in.
443
+
444
+ ## Errors never throw
445
+
446
+ Every call resolves to `{ data, error, count, status }`. On success `error` is `null`. On failure
447
+ `data` is `null` and `error` is `{ message, code, hints }`: `code` names the reason in a stable
448
+ form such as `auth-signed-out`, so compare `error.code` rather than `message`. `status` is the HTTP
449
+ status, or `0` when no answer arrived (offline, aborted). Auth calls resolve to `{ data, error }`
450
+ the same way. Streams (`chat.send`, `runs.start`) yield the same shape for each event. Only
451
+ `createClient` throws, when it gets both `key` and `app` or neither.
452
+
453
+ ## Runtime support
454
+
455
+ Releases are tested on Node.js 26 and TypeScript 7.0. The package is ESM only.
456
+
457
+ **Tests**: every push to `main` and every pull request in the
458
+ [source repository](https://github.com/oaiz-io/oaiz-sdk) runs the unit and type tests, and
459
+ compiles every TypeScript example in this README, in
460
+ [CI](https://github.com/oaiz-io/oaiz-sdk/actions/workflows/ci.yaml). A release is published only
461
+ when the same tests pass.
462
+
463
+ - **Browsers**: current evergreen browsers, on a page served over HTTPS or from your own machine
464
+ while you develop, which browsers treat as a secure context. The app client needs `fetch`,
465
+ `crypto.randomUUID`, `crypto.getRandomValues`, `crypto.subtle`, `TextEncoder`, `btoa`,
466
+ `sessionStorage` and streams. Voice chat (`chat.voice`) also needs `RTCPeerConnection`,
467
+ `getUserMedia` and `AudioContext`.
468
+ - **Node.js** 22.12 or newer (`engines`).
469
+ - **Not tested**: Bun, Deno and edge runtimes. They may work, but no release checks them.
470
+
471
+ ## Stability and changes
472
+
473
+ The package is versioned `0.x`: any release can change or remove part of the API, so pin the exact
474
+ version you test with (`npm install --save-exact @oaiz/sdk`). Each release is one commit and one
475
+ tag in the [source repository](https://github.com/oaiz-io/oaiz-sdk); compare two `sdk-v*` tags to
476
+ see what changed.
477
+
478
+ Report a bug at [github.com/oaiz-io/oaiz-sdk/issues](https://github.com/oaiz-io/oaiz-sdk/issues).
479
+ Report a vulnerability by email to security@oaiz.io, as
480
+ [SECURITY.md](https://github.com/oaiz-io/oaiz-sdk/blob/main/SECURITY.md) describes, not in a public
481
+ issue.
482
+
483
+ ## Licence
484
+
485
+ MIT