openvibers/openvibe.sdk Commits
v0.11.0: openvibe-sdk/chat (global chat, rooms and DMs over REST as the token's person; writes never retried) and openvibe-sdk/ai (runs create/get/list/cancel/retry/citations, waitFor, and the direct ai.<op> operations; a create is retried only with an idempotency key) — WS-F task 4
Changes
@@@ -3,6 +3,10 @@All notable changes to `openvibe-sdk`. The package follows semver; while it is `0.x`, a minorrelease may change an API and says so here.++## 0.11.0 (2026-09-25)++++Two more clients (roadmap WS-F task 4). **`openvibe-sdk/chat`**: OpenVibe.Chat over REST as the token's person: `global.send/history/search`, `rooms.list/create/get/update/messages/send/deleteMessage/join/leave/read/members/setMember`, `dms.list/create/get/messages/send/deleteMessage/read/unread/blocks/block/unblock`, `exportMine()`; writes are never retried, a room or conversation you may not read is `null`. **`openvibe-sdk/ai`**: OpenVibe.AI's run API: `runs.create(workflow, input, { wait, version, idempotencyKey, target, attribution, onBehalfOf, options })`, `runs.get/list/cancel/retry/citations/addCitations`, `runs.waitFor(id)` (polls until succeeded, failed or cancelled), and the direct operations `chat/generate/summarize/classify/extract/enrich/embed`; a create is retried only with an idempotency key. Both in the browser bundle, ESM and types. Additive.++## 0.10.0 (2026-09-25)**`openvibe-sdk/search`** (roadmap WS-F task 4): OpenVibe.Search's query API. `createSearchClient(client)` gives `query(text, { owner, type, lang, filter, facets, limit, cursor })` (one page: `results`, `next_cursor`, optional facet counts), `iterate(text, opts)` (every result across cursor pages, `max` to stop early), `suggest(text)` and `document(owner, type, id)` (null when missing or not yours to see). Anonymous callers get public documents; a signed-in person also gets restricted documents naming them; a first-party service with `search.query.delegate` passes `actingSubject`. Several values of one facet go as repeated `facet.<key>` parameters. In the browser bundle too. Additive.@@@ -165,6 +165,8 @@ Until OpenVibe.Tools serves the gateway facade (Tools S6), point the jobs client| `openvibe-sdk/media` | both (credentials: server) | `createMediaClient(client, {app, apiKey?, actingUserId?})`: `files.upload/list/iterate/get/delete`; `mediaUrls(origin)` public URL helpers; `createObjectsClient({app, baseUrl?, tokenClient \| apiKey \| client})`: object API v2 `upload` (single or multipart by size, resumable), `resume`, `get`, `signedUrl`, `delete`, `list`, `iterate`, and `jobs.create/get/list/approve/cancel/wait`; Media's origin from the platform descriptor when `baseUrl` is omitted || `openvibe-sdk/community` | both | `createCommunityClient(client, {actingSubject?, origin?, sourceRef?, staff?})`: `pastes.list/iterate/get/create/update/delete/fork/like/copy/versions/byUser/config`, `pastes.comments.list/create/delete`, `as(subject)` || `openvibe-sdk/search` | both | `createSearchClient(client, {actingSubject?})`: `query(text, {owner, type, lang, filter, facets, limit, cursor})`, `iterate(text, {…, max})`, `suggest(text, {owner, type, limit})`, `document(owner, type, id)` (null when missing or not visible) |++| `openvibe-sdk/chat` | both | `createChatClient(client)`: `global.send/history/search`, `rooms.*` (list, create, get, update, messages, send, deleteMessage, join, leave, read, members, setMember), `dms.*` (list, create, get, messages, send, deleteMessage, read, unread, blocks, block, unblock), `exportMine()` |++| `openvibe-sdk/ai` | both | `createAiClient(client)`: `runs.create(workflow, input, {wait, idempotencyKey, …})`, `runs.get/list/cancel/retry/citations/addCitations/waitFor`, `chat/generate/summarize/classify/extract/enrich/embed` || `openvibe-sdk/jobs` | both | `createJobsClient(client, {baseUrl?})`: `submit` (Idempotency-Key), `get`, `cancel`, `retry`, `reference`, `unreference`, `events` (SSE, reattaches), `wait`, `file` (raw Response); `isTerminal()`. Default origin: the Tools gateway's jobs facade || `openvibe-sdk/tools` | both | `createToolsClient(client, {baseUrl?, anonymousReads?})`: `list({family, q, execution, api, status})`, `get(id)`, `schema(id)`, `run(id, input, {files, waitMs, idempotencyKey, signal, timeoutMs})` (+ `.wait()`), `jobs`; `ToolRunError`, `isToolRunError()` || `openvibe-sdk/projects` | both (user token) | `createProjectsClient(client)`: `catalog`, `list`, `create`, `get`, `update`, `archive`, `setAllowance`, `setEnvironmentPolicy`, `members.*`, `apps.*`, `credentials.list/rotate/revoke`, `grants.list/request/approve/deny/revoke`, `quotas.*`, `audit`, `iterateAudit` |@@@ -14,6 +14,8 @@ module.exports = {media: require('./src/media'),community: require('./src/community'),search: require('./src/search'),++ chat: require('./src/chat'),++ ai: require('./src/ai'),jobs: require('./src/jobs'),tools: require('./src/tools'),projects: require('./src/projects'),@@@ -1,6 +1,6 @@--// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.10.0, MIT). Do not edit.++// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.11.0, MIT). Do not edit.// One self-contained ES module: core at the top level plus the browser-safe subpaths as namespaces--// (auth, registry, modules, realtime, media, community, search, jobs, tools, projects). No server-only code: no service tokens, JWT verification, events, identity.++// (auth, registry, modules, realtime, media, community, search, chat, ai, jobs, tools, projects). No server-only code: no service tokens, JWT verification, events, identity./* eslint-disable */const __defs = [// ── 0: browser.js ──────────────────────────────────────────────────@@@ -21,9 +21,11 @@ module.exports = {media: __load(12),community: __load(14),search: __load(15),-- jobs: __load(16),-- tools: __load(18),-- projects: __load(19),++ chat: __load(16),++ ai: __load(17),++ jobs: __load(18),++ tools: __load(20),++ projects: __load(21),};},// ── 1: src/core/index.js ───────────────────────────────────────────@@@ -38,7 +40,7 @@ const { paginate, offsetPager } = __load(7);const semver = __load(6);/** The package version (test/version.test.js keeps it equal to package.json). */--const SDK_VERSION = '0.10.0';++const SDK_VERSION = '0.11.0';module.exports = {createClient,@@@ -1829,7 +1831,160 @@ function createSearchClient(client, defaults = {}) {module.exports = { createSearchClient, queryOf };},--// ── 16: src/jobs.js ─────────────────────────────────────────────────++// ── 16: src/chat.js ─────────────────────────────────────────────────++function (module, exports) {++'use strict';++/**++ * openvibe-sdk/chat: OpenVibe.Chat over REST (roadmap WS-F task 4): global chat, rooms and direct++ * messages, for bots and apps. Browser-safe. Live delivery is Chat's WebSocket (/ws/chat); this is the++ * REST side of the same data.++ *++ * const chat = createChatClient(client); // a signed-in person's token, or an API token++ * await chat.global.send('hello');++ * const room = await chat.rooms.create({ name: 'Builders' });++ * await chat.rooms.send(room.room.slug, 'first!');++ * const dm = await chat.dms.create([42]);++ * await chat.dms.send(dm.conversation.id, 'hi');++ *++ * Every call acts as the token's person (Chat has no service acting-for-someone mode). Writes are never++ * retried (Chat does not deduplicate them); reads are. A missing or unreadable room or conversation is++ * null from get().++ */++const { isOpenVibeError } = __load(3);++++const enc = encodeURIComponent;++++function createChatClient(client, defaults = {}) {++ const { baseUrl } = defaults;++ const call = (opts, o = {}) => client.json({ service: 'chat', baseUrl, audience: 'openvibe.chat', ...opts, signal: o.signal });++ const once = { idempotencyKey: false };++ const orNull = (p) => p.catch((err) => { if (isOpenVibeError(err) && (err.status === 404 || err.status === 403)) return null; throw err; });++ const room = (slug) => `/api/chat/rooms/${enc(slug)}`;++ const conv = (id) => `/api/dm/conversations/${enc(id)}`;++++ return {++ global: {++ /** { messages, … } — the global feed; query: limit, after_id (a delta after a message id). */++ history: (query = {}, o = {}) => call({ path: '/api/chat/global/history', query }, o),++ /** Post to global chat as the token's person. { message: string, reply_to_id?, auto_delete_minutes? } */++ send: (message, extra = {}, o = {}) => call({ method: 'POST', path: '/api/chat/send', json: { message, ...extra }, ...once }, o),++ /** Your own lines (staff with staff.moderation.logs may pass user_id). query: q, user_id, stream_id, limit, offset */++ search: (query = {}, o = {}) => call({ path: '/api/chat/search', query }, o),++ },++ rooms: {++ list: (o = {}) => call({ path: '/api/chat/rooms' }, o),++ create: (input, o = {}) => call({ method: 'POST', path: '/api/chat/rooms', json: input, ...once }, o),++ get: (slug, o = {}) => orNull(call({ path: room(slug) }, o)),++ update: (slug, patch, o = {}) => call({ method: 'PATCH', path: room(slug), json: patch, ...once }, o),++ /** query: before | after (message id), limit */++ messages: (slug, query = {}, o = {}) => call({ path: `${room(slug)}/messages`, query }, o),++ send: (slug, message, o = {}) => call({ method: 'POST', path: `${room(slug)}/messages`, json: { message }, ...once }, o),++ deleteMessage: (slug, id, o = {}) => call({ method: 'DELETE', path: `${room(slug)}/messages/${enc(id)}`, ...once }, o),++ join: (slug, o = {}) => call({ method: 'POST', path: `${room(slug)}/join`, ...once }, o),++ leave: (slug, o = {}) => call({ method: 'POST', path: `${room(slug)}/leave`, ...once }, o),++ read: (slug, lastId, o = {}) => call({ method: 'POST', path: `${room(slug)}/read`, json: lastId != null ? { last_id: lastId } : {}, ...once }, o),++ members: (slug, o = {}) => call({ path: `${room(slug)}/members` }, o),++ /** Owners and moderators: role member | mod | blocked | none */++ setMember: (slug, username, role, o = {}) => call({ method: 'POST', path: `${room(slug)}/members`, json: { username, role }, ...once }, o),++ },++ dms: {++ list: (o = {}) => call({ path: '/api/dm/conversations' }, o),++ /** A 1:1 conversation is reused; more ids make a group ({ name }). */++ create: (userIds, extra = {}, o = {}) => call({ method: 'POST', path: '/api/dm/conversations', json: { user_ids: userIds, ...extra }, ...once }, o),++ get: (id, o = {}) => orNull(call({ path: conv(id) }, o)),++ /** query: limit, before */++ messages: (id, query = {}, o = {}) => call({ path: `${conv(id)}/messages`, query }, o),++ send: (id, message, o = {}) => call({ method: 'POST', path: `${conv(id)}/messages`, json: { message }, ...once }, o),++ deleteMessage: (id, msgId, o = {}) => call({ method: 'DELETE', path: `${conv(id)}/messages/${enc(msgId)}`, ...once }, o),++ read: (id, o = {}) => call({ method: 'POST', path: `${conv(id)}/read`, ...once }, o),++ unread: (o = {}) => call({ path: '/api/dm/unread' }, o),++ blocks: (o = {}) => call({ path: '/api/dm/blocks' }, o),++ block: (userId, o = {}) => call({ method: 'POST', path: `/api/dm/blocks/${enc(userId)}`, ...once }, o),++ unblock: (userId, o = {}) => call({ method: 'DELETE', path: `/api/dm/blocks/${enc(userId)}`, ...once }, o),++ },++ /** Every message you sent that is still visible: { username, total, truncated, messages } (JSON). */++ exportMine: (o = {}) => call({ path: '/api/chat/me/export' }, o),++ };++}++++module.exports = { createChatClient };++},++// ── 17: src/ai.js ───────────────────────────────────────────────────++function (module, exports) {++'use strict';++/**++ * openvibe-sdk/ai: OpenVibe.AI's run API (roadmap WS-F task 4). For first-party services (a Network++ * service token with ai.run.create / ai.run.read, audience openvibe.ai) and developer apps granted++ * them; workflows are namespaced by product (a caller may only run the namespaces it was granted).++ *++ * const ai = createAiClient(client);++ * const { run } = await ai.runs.create('live.stream.describe_frame', { image_url }, { wait: 20000 });++ * const done = run.status === 'succeeded' ? run : await ai.runs.waitFor(run.id);++ * const summary = await ai.summarize({ text }, { wait: 30000 }); // ai.summarize, waited for++ *++ * create() answers { run } with 201 (finished within the wait, or served from cache), 202 (still queued++ * or running: poll get() or use waitFor()), or 200 (an idempotent replay: `replayed: true`). Pass an++ * idempotencyKey so a retried create never runs twice; creates are otherwise not retried. A quota++ * refusal or a full queue is a 429 error carrying Retry-After.++ */++const { isOpenVibeError } = __load(3);++++const enc = encodeURIComponent;++const TERMINAL = new Set(['succeeded', 'failed', 'cancelled']);++const OPS = ['chat', 'generate', 'summarize', 'classify', 'extract', 'enrich', 'embed'];++++function createAiClient(client, defaults = {}) {++ const { baseUrl } = defaults;++ const call = (opts, o = {}) => client.json({ service: 'ai', baseUrl, audience: 'openvibe.ai', ...opts, signal: o.signal });++ const writeOpts = (o) => (o.idempotencyKey ? { idempotencyKey: o.idempotencyKey } : { idempotencyKey: false });++ const waitQuery = (o) => (o.wait != null ? { wait: Math.max(0, Math.floor(o.wait)) } : undefined);++ const orNull = (p) => p.catch((err) => { if (isOpenVibeError(err) && err.status === 404) return null; throw err; });++++ const runs = {++ /** workflow key (e.g. 'wiki.generate_page'), its input; o: { wait (ms), version, idempotencyKey, target, attribution, onBehalfOf, options } */++ create(workflow, input = {}, o = {}) {++ const body = { workflow, input };++ if (o.version != null) body.version = o.version;++ if (o.target !== undefined) body.target = o.target;++ if (o.attribution !== undefined) body.attribution = o.attribution;++ if (o.onBehalfOf !== undefined) body.on_behalf_of = o.onBehalfOf;++ if (o.options !== undefined) body.options = o.options;++ return call({ method: 'POST', path: '/api/v1/runs', query: waitQuery(o), json: body, ...writeOpts(o) }, o);++ },++ list: (query = {}, o = {}) => call({ path: '/api/v1/runs', query }, o),++ get: (id, o = {}) => orNull(call({ path: `/api/v1/runs/${enc(id)}` }, o)),++ cancel: (id, o = {}) => call({ method: 'POST', path: `/api/v1/runs/${enc(id)}/cancel`, idempotencyKey: false }, o),++ retry: (id, o = {}) => call({ method: 'POST', path: `/api/v1/runs/${enc(id)}/retry`, query: waitQuery(o), ...writeOpts(o) }, o),++ citations: (id, o = {}) => call({ path: `/api/v1/runs/${enc(id)}/citations` }, o),++ addCitations: (id, citations, o = {}) => call({ method: 'POST', path: `/api/v1/runs/${enc(id)}/citations`, json: { citations }, idempotencyKey: false }, o),++ /** Poll get() until the run finishes (succeeded, failed or cancelled) or `timeoutMs` passes. → the run */++ async waitFor(id, { intervalMs = 1000, timeoutMs = 120000, signal } = {}) {++ const until = Date.now() + timeoutMs;++ for (;;) {++ const got = await runs.get(id, { signal });++ const run = got && (got.run || got);++ if (!run) throw new Error(`run ${id} not found`);++ if (TERMINAL.has(run.status)) return run;++ if (Date.now() + intervalMs > until) throw new Error(`run ${id} still ${run.status} after ${timeoutMs} ms`);++ await new Promise((r) => setTimeout(r, intervalMs));++ if (signal && signal.aborted) throw new Error('aborted');++ }++ },++ };++++ const api = { runs, TERMINAL };++ // Direct operations: a run of workflow ai.<op> with the input as the body, waited for by AI.++ for (const op of OPS) {++ api[op] = (input = {}, o = {}) => call({ method: 'POST', path: `/api/v1/${op}`, query: waitQuery(o),++ json: { ...input, ...(o.target !== undefined ? { target: o.target } : {}), ...(o.attribution !== undefined ? { attribution: o.attribution } : {}), ...(o.options !== undefined ? { options: o.options } : {}) },++ ...writeOpts(o) }, o);++ }++ return api;++}++++module.exports = { createAiClient, TERMINAL, OPS };++},++// ── 18: src/jobs.js ─────────────────────────────────────────────────function (module, exports) {'use strict';/**@@@ -1867,7 +2022,7 @@ function (module, exports) {*/const { OpenVibeError, isOpenVibeError } = __load(3);const { newIdempotencyKey } = __load(5);--const { appendFiles } = __load(17);++const { appendFiles } = __load(19);const { parseSSE } = __load(11);const TERMINAL = new Set(['succeeded', 'failed', 'cancelled']);@@@ -2031,7 +2186,7 @@ function createJobsClient(client, { baseUrl, service = 'tools', audience = 'openmodule.exports = { createJobsClient, isTerminal, TERMINAL_STATES: [...TERMINAL] };},--// ── 17: src/core/form.js ────────────────────────────────────────────++// ── 19: src/core/form.js ────────────────────────────────────────────function (module, exports) {'use strict';/**@@@ -2065,7 +2220,7 @@ function appendFiles(form, list, where) {module.exports = { isBlob, isUpload, appendFiles };},--// ── 18: src/tools.js ────────────────────────────────────────────────++// ── 20: src/tools.js ────────────────────────────────────────────────function (module, exports) {'use strict';/**@@@ -2099,8 +2254,8 @@ function (module, exports) {*/const { OpenVibeError, isOpenVibeError } = __load(3);const { newIdempotencyKey } = __load(5);--const { isUpload, appendFiles } = __load(17);--const { createJobsClient } = __load(16);++const { isUpload, appendFiles } = __load(19);++const { createJobsClient } = __load(18);const enc = encodeURIComponent;const ID_RE = /^[a-z][a-z0-9-]{0,39}$/;@@@ -2334,7 +2489,7 @@ function createToolsClient(client, { baseUrl, service = 'tools', audience = 'opemodule.exports = { createToolsClient, ToolRunError, isToolRunError };},--// ── 19: src/projects.js ─────────────────────────────────────────────++// ── 21: src/projects.js ─────────────────────────────────────────────function (module, exports) {'use strict';/**@@@ -2466,6 +2621,6 @@ export const {startSpan, contextFromHeaders, ulid, newEventId, newIdempotencyKey, isActingSubjectId,satisfiesRange, compareVersions, CONTRACTS_RANGE, DEFAULT_NETWORK, SDK_VERSION, auth,registry, modules, realtime, media, community, search,-- jobs, tools, projects,++ chat, ai, jobs, tools, projects,} = sdk;export default sdk;nnew file mode 100644@@@ -0,0 +1,7 @@++// Generated by scripts/esm.js from src/ai.js. Do not edit.++import cjs from '../src/ai.js';++++export const {++ createAiClient, TERMINAL, OPS,++} = cjs;++export default cjs;nnew file mode 100644@@@ -0,0 +1,7 @@++// Generated by scripts/esm.js from src/chat.js. Do not edit.++import cjs from '../src/chat.js';++++export const {++ createChatClient,++} = cjs;++export default cjs;@@@ -6,6 +6,7 @@ export const {startSpan, contextFromHeaders, ulid, newEventId, newIdempotencyKey, isActingSubjectId,satisfiesRange, compareVersions, CONTRACTS_RANGE, DEFAULT_NETWORK, SDK_VERSION, auth,registry, identity, modules, events, realtime, media,-- community, search, jobs, tools, projects, vip,++ community, search, chat, ai, jobs, tools,++ projects, vip,} = cjs;export default cjs;@@@ -17,6 +17,8 @@ module.exports = {media: require('./src/media'),community: require('./src/community'),search: require('./src/search'),++ chat: require('./src/chat'),++ ai: require('./src/ai'),jobs: require('./src/jobs'),tools: require('./src/tools'),projects: require('./src/projects'),@@@ -1,12 +1,12 @@{"name": "openvibe-sdk",-- "version": "0.10.0",++ "version": "0.11.0","lockfileVersion": 3,"requires": true,"packages": {"": {"name": "openvibe-sdk",-- "version": "0.10.0",++ "version": "0.11.0","license": "MIT","devDependencies": {"better-sqlite3": "^11.10.0",@@@ -1,7 +1,7 @@{"name": "openvibe-sdk",-- "version": "0.10.0",-- "description": "Supported browser and server clients for the OpenVibe platform: auth (incl. developer apps), registry discovery, identity, user modules, events, realtime, media, community, search, Tools (registry, run API and jobs), VIP memberships and developer projects, with deadlines, safe retries, problem errors and trace propagation.",++ "version": "0.11.0",++ "description": "Supported browser and server clients for the OpenVibe platform: auth (incl. developer apps), registry discovery, identity, user modules, events, realtime, media, community, search, chat, AI runs, Tools (registry, run API and jobs), VIP memberships and developer projects, with deadlines, safe retries, problem errors and trace propagation.","main": "index.js","types": "types/index.d.ts","browser": "browser.js",@@@ -70,6 +70,16 @@"import": "./esm/search.mjs","default": "./src/search.js"},++ "./chat": {++ "types": "./types/chat.d.ts",++ "import": "./esm/chat.mjs",++ "default": "./src/chat.js"++ },++ "./ai": {++ "types": "./types/ai.d.ts",++ "import": "./esm/ai.mjs",++ "default": "./src/ai.js"++ },"./jobs": {"types": "./types/jobs.d.ts","import": "./esm/jobs.mjs",@@@ -24,6 +24,8 @@ const ENTRIES = {'media.mjs': '../src/media.js','community.mjs': '../src/community.js','search.mjs': '../src/search.js',++ 'chat.mjs': '../src/chat.js',++ 'ai.mjs': '../src/ai.js','jobs.mjs': '../src/jobs.js','tools.mjs': '../src/tools.js','projects.mjs': '../src/projects.js',nnew file mode 100644@@@ -0,0 +1,72 @@++'use strict';++/**++ * openvibe-sdk/ai: OpenVibe.AI's run API (roadmap WS-F task 4). For first-party services (a Network++ * service token with ai.run.create / ai.run.read, audience openvibe.ai) and developer apps granted++ * them; workflows are namespaced by product (a caller may only run the namespaces it was granted).++ *++ * const ai = createAiClient(client);++ * const { run } = await ai.runs.create('live.stream.describe_frame', { image_url }, { wait: 20000 });++ * const done = run.status === 'succeeded' ? run : await ai.runs.waitFor(run.id);++ * const summary = await ai.summarize({ text }, { wait: 30000 }); // ai.summarize, waited for++ *++ * create() answers { run } with 201 (finished within the wait, or served from cache), 202 (still queued++ * or running: poll get() or use waitFor()), or 200 (an idempotent replay: `replayed: true`). Pass an++ * idempotencyKey so a retried create never runs twice; creates are otherwise not retried. A quota++ * refusal or a full queue is a 429 error carrying Retry-After.++ */++const { isOpenVibeError } = require('./core/errors');++++const enc = encodeURIComponent;++const TERMINAL = new Set(['succeeded', 'failed', 'cancelled']);++const OPS = ['chat', 'generate', 'summarize', 'classify', 'extract', 'enrich', 'embed'];++++function createAiClient(client, defaults = {}) {++ const { baseUrl } = defaults;++ const call = (opts, o = {}) => client.json({ service: 'ai', baseUrl, audience: 'openvibe.ai', ...opts, signal: o.signal });++ const writeOpts = (o) => (o.idempotencyKey ? { idempotencyKey: o.idempotencyKey } : { idempotencyKey: false });++ const waitQuery = (o) => (o.wait != null ? { wait: Math.max(0, Math.floor(o.wait)) } : undefined);++ const orNull = (p) => p.catch((err) => { if (isOpenVibeError(err) && err.status === 404) return null; throw err; });++++ const runs = {++ /** workflow key (e.g. 'wiki.generate_page'), its input; o: { wait (ms), version, idempotencyKey, target, attribution, onBehalfOf, options } */++ create(workflow, input = {}, o = {}) {++ const body = { workflow, input };++ if (o.version != null) body.version = o.version;++ if (o.target !== undefined) body.target = o.target;++ if (o.attribution !== undefined) body.attribution = o.attribution;++ if (o.onBehalfOf !== undefined) body.on_behalf_of = o.onBehalfOf;++ if (o.options !== undefined) body.options = o.options;++ return call({ method: 'POST', path: '/api/v1/runs', query: waitQuery(o), json: body, ...writeOpts(o) }, o);++ },++ list: (query = {}, o = {}) => call({ path: '/api/v1/runs', query }, o),++ get: (id, o = {}) => orNull(call({ path: `/api/v1/runs/${enc(id)}` }, o)),++ cancel: (id, o = {}) => call({ method: 'POST', path: `/api/v1/runs/${enc(id)}/cancel`, idempotencyKey: false }, o),++ retry: (id, o = {}) => call({ method: 'POST', path: `/api/v1/runs/${enc(id)}/retry`, query: waitQuery(o), ...writeOpts(o) }, o),++ citations: (id, o = {}) => call({ path: `/api/v1/runs/${enc(id)}/citations` }, o),++ addCitations: (id, citations, o = {}) => call({ method: 'POST', path: `/api/v1/runs/${enc(id)}/citations`, json: { citations }, idempotencyKey: false }, o),++ /** Poll get() until the run finishes (succeeded, failed or cancelled) or `timeoutMs` passes. → the run */++ async waitFor(id, { intervalMs = 1000, timeoutMs = 120000, signal } = {}) {++ const until = Date.now() + timeoutMs;++ for (;;) {++ const got = await runs.get(id, { signal });++ const run = got && (got.run || got);++ if (!run) throw new Error(`run ${id} not found`);++ if (TERMINAL.has(run.status)) return run;++ if (Date.now() + intervalMs > until) throw new Error(`run ${id} still ${run.status} after ${timeoutMs} ms`);++ await new Promise((r) => setTimeout(r, intervalMs));++ if (signal && signal.aborted) throw new Error('aborted');++ }++ },++ };++++ const api = { runs, TERMINAL };++ // Direct operations: a run of workflow ai.<op> with the input as the body, waited for by AI.++ for (const op of OPS) {++ api[op] = (input = {}, o = {}) => call({ method: 'POST', path: `/api/v1/${op}`, query: waitQuery(o),++ json: { ...input, ...(o.target !== undefined ? { target: o.target } : {}), ...(o.attribution !== undefined ? { attribution: o.attribution } : {}), ...(o.options !== undefined ? { options: o.options } : {}) },++ ...writeOpts(o) }, o);++ }++ return api;++}++++module.exports = { createAiClient, TERMINAL, OPS };nnew file mode 100644@@@ -0,0 +1,75 @@++'use strict';++/**++ * openvibe-sdk/chat: OpenVibe.Chat over REST (roadmap WS-F task 4): global chat, rooms and direct++ * messages, for bots and apps. Browser-safe. Live delivery is Chat's WebSocket (/ws/chat); this is the++ * REST side of the same data.++ *++ * const chat = createChatClient(client); // a signed-in person's token, or an API token++ * await chat.global.send('hello');++ * const room = await chat.rooms.create({ name: 'Builders' });++ * await chat.rooms.send(room.room.slug, 'first!');++ * const dm = await chat.dms.create([42]);++ * await chat.dms.send(dm.conversation.id, 'hi');++ *++ * Every call acts as the token's person (Chat has no service acting-for-someone mode). Writes are never++ * retried (Chat does not deduplicate them); reads are. A missing or unreadable room or conversation is++ * null from get().++ */++const { isOpenVibeError } = require('./core/errors');++++const enc = encodeURIComponent;++++function createChatClient(client, defaults = {}) {++ const { baseUrl } = defaults;++ const call = (opts, o = {}) => client.json({ service: 'chat', baseUrl, audience: 'openvibe.chat', ...opts, signal: o.signal });++ const once = { idempotencyKey: false };++ const orNull = (p) => p.catch((err) => { if (isOpenVibeError(err) && (err.status === 404 || err.status === 403)) return null; throw err; });++ const room = (slug) => `/api/chat/rooms/${enc(slug)}`;++ const conv = (id) => `/api/dm/conversations/${enc(id)}`;++++ return {++ global: {++ /** { messages, … } — the global feed; query: limit, after_id (a delta after a message id). */++ history: (query = {}, o = {}) => call({ path: '/api/chat/global/history', query }, o),++ /** Post to global chat as the token's person. { message: string, reply_to_id?, auto_delete_minutes? } */++ send: (message, extra = {}, o = {}) => call({ method: 'POST', path: '/api/chat/send', json: { message, ...extra }, ...once }, o),++ /** Your own lines (staff with staff.moderation.logs may pass user_id). query: q, user_id, stream_id, limit, offset */++ search: (query = {}, o = {}) => call({ path: '/api/chat/search', query }, o),++ },++ rooms: {++ list: (o = {}) => call({ path: '/api/chat/rooms' }, o),++ create: (input, o = {}) => call({ method: 'POST', path: '/api/chat/rooms', json: input, ...once }, o),++ get: (slug, o = {}) => orNull(call({ path: room(slug) }, o)),++ update: (slug, patch, o = {}) => call({ method: 'PATCH', path: room(slug), json: patch, ...once }, o),++ /** query: before | after (message id), limit */++ messages: (slug, query = {}, o = {}) => call({ path: `${room(slug)}/messages`, query }, o),++ send: (slug, message, o = {}) => call({ method: 'POST', path: `${room(slug)}/messages`, json: { message }, ...once }, o),++ deleteMessage: (slug, id, o = {}) => call({ method: 'DELETE', path: `${room(slug)}/messages/${enc(id)}`, ...once }, o),++ join: (slug, o = {}) => call({ method: 'POST', path: `${room(slug)}/join`, ...once }, o),++ leave: (slug, o = {}) => call({ method: 'POST', path: `${room(slug)}/leave`, ...once }, o),++ read: (slug, lastId, o = {}) => call({ method: 'POST', path: `${room(slug)}/read`, json: lastId != null ? { last_id: lastId } : {}, ...once }, o),++ members: (slug, o = {}) => call({ path: `${room(slug)}/members` }, o),++ /** Owners and moderators: role member | mod | blocked | none */++ setMember: (slug, username, role, o = {}) => call({ method: 'POST', path: `${room(slug)}/members`, json: { username, role }, ...once }, o),++ },++ dms: {++ list: (o = {}) => call({ path: '/api/dm/conversations' }, o),++ /** A 1:1 conversation is reused; more ids make a group ({ name }). */++ create: (userIds, extra = {}, o = {}) => call({ method: 'POST', path: '/api/dm/conversations', json: { user_ids: userIds, ...extra }, ...once }, o),++ get: (id, o = {}) => orNull(call({ path: conv(id) }, o)),++ /** query: limit, before */++ messages: (id, query = {}, o = {}) => call({ path: `${conv(id)}/messages`, query }, o),++ send: (id, message, o = {}) => call({ method: 'POST', path: `${conv(id)}/messages`, json: { message }, ...once }, o),++ deleteMessage: (id, msgId, o = {}) => call({ method: 'DELETE', path: `${conv(id)}/messages/${enc(msgId)}`, ...once }, o),++ read: (id, o = {}) => call({ method: 'POST', path: `${conv(id)}/read`, ...once }, o),++ unread: (o = {}) => call({ path: '/api/dm/unread' }, o),++ blocks: (o = {}) => call({ path: '/api/dm/blocks' }, o),++ block: (userId, o = {}) => call({ method: 'POST', path: `/api/dm/blocks/${enc(userId)}`, ...once }, o),++ unblock: (userId, o = {}) => call({ method: 'DELETE', path: `/api/dm/blocks/${enc(userId)}`, ...once }, o),++ },++ /** Every message you sent that is still visible: { username, total, truncated, messages } (JSON). */++ exportMine: (o = {}) => call({ path: '/api/chat/me/export' }, o),++ };++}++++module.exports = { createChatClient };@@@ -8,7 +8,7 @@ const { paginate, offsetPager } = require('./paginate');const semver = require('./semver');/** The package version (test/version.test.js keeps it equal to package.json). */--const SDK_VERSION = '0.10.0';++const SDK_VERSION = '0.11.0';module.exports = {createClient,nnew file mode 100644@@@ -0,0 +1,51 @@++'use strict';++/** AI: create with wait and an idempotency key, no retried create without one, waitFor polls to the end, direct ops. */++const assert = require('node:assert/strict');++const { stubServer, send, run } = require('./helpers');++const { createClient } = require('../src/core');++const { createAiClient } = require('../src/ai');++++async function aiStub() {++ let polls = 0, busy = 0;++ return stubServer((req, res, body) => {++ const u = new URL(req.url, 'http://x');++ if (req.method === 'POST' && u.pathname === '/api/v1/runs') {++ const b = JSON.parse(String(body));++ if (b.workflow === 'busy.flow' && busy++ === 0) return send(res, 503, { code: 'ai.busy' });++ return send(res, 202, { run: { id: 'run_1', status: 'queued' } });++ }++ if (u.pathname === '/api/v1/runs/run_1') return send(res, 200, { run: { id: 'run_1', status: ++polls >= 3 ? 'succeeded' : 'running', output: polls >= 3 ? { ok: true } : undefined } });++ if (u.pathname === '/api/v1/runs/missing') return send(res, 404, { code: 'run.not_found' });++ if (u.pathname === '/api/v1/summarize') return send(res, 201, { run: { id: 'run_2', status: 'succeeded' } });++ return send(res, 404, {});++ });++}++++run([++ ['create sends workflow, input, wait and the idempotency key; waitFor polls until finished', async () => {++ const srv = await aiStub();++ const ai = createAiClient(createClient({ baseUrls: { ai: srv.url }, getToken: ({ audience }) => `svc-${audience}` }));++ const r = await ai.runs.create('wiki.generate_page', { title: 'X' }, { wait: 2000, idempotencyKey: 'k-1', onBehalfOf: { type: 'user', id: 'usr_x' } });++ assert.equal(r.run.status, 'queued');++ const req = srv.requests[0];++ assert.equal(req.url, '/api/v1/runs?wait=2000');++ assert.deepEqual(JSON.parse(String(req.body)), { workflow: 'wiki.generate_page', input: { title: 'X' }, on_behalf_of: { type: 'user', id: 'usr_x' } });++ assert.equal(req.headers.authorization, 'Bearer svc-openvibe.ai');++ assert.equal(req.headers['idempotency-key'], 'k-1');++ const done = await ai.runs.waitFor('run_1', { intervalMs: 5 });++ assert.equal(done.status, 'succeeded');++ assert.equal(await ai.runs.get('missing'), null);++ await srv.close();++ }],++ ['a create without a key is not retried; direct ops post the input as the body', async () => {++ const srv = await aiStub();++ const ai = createAiClient(createClient({ baseUrls: { ai: srv.url }, token: 't', retries: 2 }));++ await assert.rejects(ai.runs.create('busy.flow', {}), (e) => e.status === 503);++ assert.equal(srv.requests.length, 1, 'one attempt');++ const s = await ai.summarize({ text: 'long text' }, { wait: 30000 });++ assert.equal(s.run.id, 'run_2');++ assert.equal(srv.requests[1].url, '/api/v1/summarize?wait=30000');++ assert.deepEqual(JSON.parse(String(srv.requests[1].body)), { text: 'long text' });++ await srv.close();++ }],++]);nnew file mode 100644@@@ -0,0 +1,51 @@++'use strict';++/** Chat: paths, bodies and methods for global, rooms and DMs; writes never retried; a missing room is null. */++const assert = require('node:assert/strict');++const { stubServer, send, run } = require('./helpers');++const { createClient } = require('../src/core');++const { createChatClient } = require('../src/chat');++++async function chatStub() {++ let busy = 0;++ return stubServer((req, res) => {++ if (req.url.startsWith('/api/chat/rooms/private-one')) return send(res, 404, { error: 'Room not found' });++ if (req.url === '/api/chat/send' && busy++ === 0) return send(res, 503, { error: 'busy' });++ return send(res, 200, { ok: true, url: req.url });++ });++}++++run([++ ['global, rooms and DMs go to the right paths with the right bodies', async () => {++ const srv = await chatStub();++ const chat = createChatClient(createClient({ baseUrls: { chat: srv.url }, token: 'user-jwt', retries: 2 }));++ await chat.rooms.create({ name: 'Builders' });++ await chat.rooms.send('builders', 'first!');++ await chat.rooms.read('builders', 12);++ await chat.rooms.setMember('builders', 'bob', 'mod');++ await chat.dms.create([42], { name: 'pair' });++ await chat.dms.send(7, 'hi');++ await chat.dms.messages(7, { limit: 20 });++ await chat.global.history({ after_id: 5 });++ const got = srv.requests.map((r) => [r.method, r.url, r.body && String(r.body).length ? JSON.parse(String(r.body)) : null]);++ assert.deepEqual(got, [++ ['POST', '/api/chat/rooms', { name: 'Builders' }],++ ['POST', '/api/chat/rooms/builders/messages', { message: 'first!' }],++ ['POST', '/api/chat/rooms/builders/read', { last_id: 12 }],++ ['POST', '/api/chat/rooms/builders/members', { username: 'bob', role: 'mod' }],++ ['POST', '/api/dm/conversations', { user_ids: [42], name: 'pair' }],++ ['POST', '/api/dm/conversations/7/messages', { message: 'hi' }],++ ['GET', '/api/dm/conversations/7/messages?limit=20', null],++ ['GET', '/api/chat/global/history?after_id=5', null],++ ]);++ assert.equal(srv.requests[0].headers.authorization, 'Bearer user-jwt');++ await srv.close();++ }],++ ['a failed send is not retried; a room you may not read is null', async () => {++ const srv = await chatStub();++ const chat = createChatClient(createClient({ baseUrls: { chat: srv.url }, token: 't', retries: 2 }));++ await assert.rejects(chat.global.send('hello'), (e) => e.status === 503);++ assert.equal(srv.requests.filter((r) => r.url === '/api/chat/send').length, 1, 'one attempt only');++ assert.equal(await chat.rooms.get('private-one'), null);++ await srv.close();++ }],++]);@@@ -66,7 +66,7 @@ run([})().catch((err) => { console.error(err); process.exit(1); });`;const res = execFileSync(process.execPath, ['-e', script], { cwd: app, encoding: 'utf8' });assert.match(res, new RegExp(`consumer ok: ${subpaths.length} subpaths \\+ the browser bundle`));-- assert.equal(subpaths.length, 18);++ assert.equal(subpaths.length, 20);assert.ok(!fs.existsSync(path.join(app, 'node_modules/openvibe-contracts')), 'the optional peer is not installed');} finally {fs.rmSync(tmp, { recursive: true, force: true });nnew file mode 100644@@@ -0,0 +1,20 @@++import type { OpenVibeClient } from './core';++++export interface AiRun { id: string; status: 'queued' | 'running' | 'succeeded' | 'failed' | 'cancelled' | string; workflow?: { key: string; version: number }; output?: unknown; error?: unknown; [field: string]: unknown; }++export interface RunOptions { wait?: number; version?: number; idempotencyKey?: string; target?: unknown; attribution?: unknown; onBehalfOf?: unknown; options?: Record<string, unknown>; signal?: AbortSignal; }++export interface RunResponse { run: AiRun; replayed?: boolean }++export interface AiRuns {++ create(workflow: string, input?: Record<string, unknown>, opts?: RunOptions): Promise<RunResponse>;++ list(query?: Record<string, string | number>, opts?: { signal?: AbortSignal }): Promise<{ runs: AiRun[]; [field: string]: unknown }>;++ get(id: string, opts?: { signal?: AbortSignal }): Promise<RunResponse | null>;++ cancel(id: string, opts?: { signal?: AbortSignal }): Promise<RunResponse>;++ retry(id: string, opts?: Pick<RunOptions, 'wait' | 'idempotencyKey' | 'signal'>): Promise<RunResponse>;++ citations(id: string, opts?: { signal?: AbortSignal }): Promise<{ citations: Array<Record<string, unknown>> }>;++ addCitations(id: string, citations: Array<Record<string, unknown>>, opts?: { signal?: AbortSignal }): Promise<{ citations: Array<Record<string, unknown>> }>;++ waitFor(id: string, opts?: { intervalMs?: number; timeoutMs?: number; signal?: AbortSignal }): Promise<AiRun>;++}++type Op = (input?: Record<string, unknown>, opts?: Omit<RunOptions, 'version' | 'onBehalfOf'>) => Promise<RunResponse>;++export interface AiClient { runs: AiRuns; TERMINAL: Set<string>; chat: Op; generate: Op; summarize: Op; classify: Op; extract: Op; enrich: Op; embed: Op; }++export function createAiClient(client: OpenVibeClient, defaults?: { baseUrl?: string }): AiClient;++export const TERMINAL: Set<string>;++export const OPS: string[];@@@ -6,6 +6,8 @@ import * as realtime from './realtime';import * as media from './media';import * as community from './community';import * as search from './search';++import * as chat from './chat';++import * as ai from './ai';import * as jobs from './jobs';import * as tools from './tools';import * as projects from './projects';@@@ -19,12 +21,14 @@ export * as realtime from './realtime';export * as media from './media';export * as community from './community';export * as search from './search';++export * as chat from './chat';++export * as ai from './ai';export * as jobs from './jobs';export * as tools from './tools';export * as projects from './projects';declare const sdk: typeof core & {auth: typeof auth; registry: typeof registry; modules: typeof modules; realtime: typeof realtime;-- media: typeof media; community: typeof community; search: typeof search; jobs: typeof jobs; tools: typeof tools; projects: typeof projects;++ media: typeof media; community: typeof community; search: typeof search; chat: typeof chat; ai: typeof ai; jobs: typeof jobs; tools: typeof tools; projects: typeof projects;};export default sdk;nnew file mode 100644@@@ -0,0 +1,40 @@++import type { OpenVibeClient } from './core';++++type Opts = { signal?: AbortSignal };++type Json = Record<string, unknown>;++export interface ChatClient {++ global: {++ history(query?: { limit?: number; after_id?: number }, opts?: Opts): Promise<Json>;++ send(message: string, extra?: { reply_to_id?: number; auto_delete_minutes?: number }, opts?: Opts): Promise<Json>;++ search(query?: { q?: string; user_id?: number; stream_id?: number; limit?: number; offset?: number }, opts?: Opts): Promise<{ messages: Json[]; total?: number }>;++ };++ rooms: {++ list(opts?: Opts): Promise<Json>;++ create(input: { name: string; slug?: string; topic?: string; visibility?: 'public' | 'private' }, opts?: Opts): Promise<Json>;++ get(slug: string, opts?: Opts): Promise<Json | null>;++ update(slug: string, patch: { name?: string; topic?: string; visibility?: 'public' | 'private'; slow_seconds?: number }, opts?: Opts): Promise<Json>;++ messages(slug: string, query?: { before?: number; after?: number; limit?: number }, opts?: Opts): Promise<Json>;++ send(slug: string, message: string, opts?: Opts): Promise<Json>;++ deleteMessage(slug: string, id: number | string, opts?: Opts): Promise<Json>;++ join(slug: string, opts?: Opts): Promise<Json>;++ leave(slug: string, opts?: Opts): Promise<Json>;++ read(slug: string, lastId?: number, opts?: Opts): Promise<Json>;++ members(slug: string, opts?: Opts): Promise<Json>;++ setMember(slug: string, username: string, role: 'member' | 'mod' | 'blocked' | 'none', opts?: Opts): Promise<Json>;++ };++ dms: {++ list(opts?: Opts): Promise<Json>;++ create(userIds: number[], extra?: { name?: string }, opts?: Opts): Promise<Json>;++ get(id: number | string, opts?: Opts): Promise<Json | null>;++ messages(id: number | string, query?: { limit?: number; before?: number }, opts?: Opts): Promise<Json>;++ send(id: number | string, message: string, opts?: Opts): Promise<Json>;++ deleteMessage(id: number | string, msgId: number | string, opts?: Opts): Promise<Json>;++ read(id: number | string, opts?: Opts): Promise<Json>;++ unread(opts?: Opts): Promise<{ unread: number }>;++ blocks(opts?: Opts): Promise<Json>;++ block(userId: number | string, opts?: Opts): Promise<Json>;++ unblock(userId: number | string, opts?: Opts): Promise<Json>;++ };++ exportMine(opts?: Opts): Promise<{ username: string; exported_at: string; total: number; truncated: boolean; messages: Json[] }>;++}++export function createChatClient(client: OpenVibeClient, defaults?: { baseUrl?: string }): ChatClient;@@@ -8,6 +8,8 @@ export * as realtime from './realtime';export * as media from './media';export * as community from './community';export * as search from './search';++export * as chat from './chat';++export * as ai from './ai';export * as jobs from './jobs';export * as tools from './tools';export * as projects from './projects';
@@ -3,6 +3,10 @@
@@ -3,6 +3,10 @@
All notable changes to `openvibe-sdk`. The package follows semver; while it is `0.x`, a minor
All notable changes to `openvibe-sdk`. The package follows semver; while it is `0.x`, a minor
release may change an API and says so here.
release may change an API and says so here.
+## 0.11.0 (2026-09-25)
+
+Two more clients (roadmap WS-F task 4). **`openvibe-sdk/chat`**: OpenVibe.Chat over REST as the token's person: `global.send/history/search`, `rooms.list/create/get/update/messages/send/deleteMessage/join/leave/read/members/setMember`, `dms.list/create/get/messages/send/deleteMessage/read/unread/blocks/block/unblock`, `exportMine()`; writes are never retried, a room or conversation you may not read is `null`. **`openvibe-sdk/ai`**: OpenVibe.AI's run API: `runs.create(workflow, input, { wait, version, idempotencyKey, target, attribution, onBehalfOf, options })`, `runs.get/list/cancel/retry/citations/addCitations`, `runs.waitFor(id)` (polls until succeeded, failed or cancelled), and the direct operations `chat/generate/summarize/classify/extract/enrich/embed`; a create is retried only with an idempotency key. Both in the browser bundle, ESM and types. Additive.
+
## 0.10.0 (2026-09-25)
## 0.10.0 (2026-09-25)
**`openvibe-sdk/search`** (roadmap WS-F task 4): OpenVibe.Search's query API. `createSearchClient(client)` gives `query(text, { owner, type, lang, filter, facets, limit, cursor })` (one page: `results`, `next_cursor`, optional facet counts), `iterate(text, opts)` (every result across cursor pages, `max` to stop early), `suggest(text)` and `document(owner, type, id)` (null when missing or not yours to see). Anonymous callers get public documents; a signed-in person also gets restricted documents naming them; a first-party service with `search.query.delegate` passes `actingSubject`. Several values of one facet go as repeated `facet.<key>` parameters. In the browser bundle too. Additive.
**`openvibe-sdk/search`** (roadmap WS-F task 4): OpenVibe.Search's query API. `createSearchClient(client)` gives `query(text, { owner, type, lang, filter, facets, limit, cursor })` (one page: `results`, `next_cursor`, optional facet counts), `iterate(text, opts)` (every result across cursor pages, `max` to stop early), `suggest(text)` and `document(owner, type, id)` (null when missing or not yours to see). Anonymous callers get public documents; a signed-in person also gets restricted documents naming them; a first-party service with `search.query.delegate` passes `actingSubject`. Several values of one facet go as repeated `facet.<key>` parameters. In the browser bundle too. Additive.
@@ -165,6 +165,8 @@ Until OpenVibe.Tools serves the gateway facade (Tools S6), point the jobs client
@@ -165,6 +165,8 @@ Until OpenVibe.Tools serves the gateway facade (Tools S6), point the jobs client
| `openvibe-sdk/media` | both (credentials: server) | `createMediaClient(client, {app, apiKey?, actingUserId?})`: `files.upload/list/iterate/get/delete`; `mediaUrls(origin)` public URL helpers; `createObjectsClient({app, baseUrl?, tokenClient \| apiKey \| client})`: object API v2 `upload` (single or multipart by size, resumable), `resume`, `get`, `signedUrl`, `delete`, `list`, `iterate`, and `jobs.create/get/list/approve/cancel/wait`; Media's origin from the platform descriptor when `baseUrl` is omitted |
| `openvibe-sdk/media` | both (credentials: server) | `createMediaClient(client, {app, apiKey?, actingUserId?})`: `files.upload/list/iterate/get/delete`; `mediaUrls(origin)` public URL helpers; `createObjectsClient({app, baseUrl?, tokenClient \| apiKey \| client})`: object API v2 `upload` (single or multipart by size, resumable), `resume`, `get`, `signedUrl`, `delete`, `list`, `iterate`, and `jobs.create/get/list/approve/cancel/wait`; Media's origin from the platform descriptor when `baseUrl` is omitted |
| `openvibe-sdk/community` | both | `createCommunityClient(client, {actingSubject?, origin?, sourceRef?, staff?})`: `pastes.list/iterate/get/create/update/delete/fork/like/copy/versions/byUser/config`, `pastes.comments.list/create/delete`, `as(subject)` |
| `openvibe-sdk/community` | both | `createCommunityClient(client, {actingSubject?, origin?, sourceRef?, staff?})`: `pastes.list/iterate/get/create/update/delete/fork/like/copy/versions/byUser/config`, `pastes.comments.list/create/delete`, `as(subject)` |
| `openvibe-sdk/search` | both | `createSearchClient(client, {actingSubject?})`: `query(text, {owner, type, lang, filter, facets, limit, cursor})`, `iterate(text, {…, max})`, `suggest(text, {owner, type, limit})`, `document(owner, type, id)` (null when missing or not visible) |
| `openvibe-sdk/search` | both | `createSearchClient(client, {actingSubject?})`: `query(text, {owner, type, lang, filter, facets, limit, cursor})`, `iterate(text, {…, max})`, `suggest(text, {owner, type, limit})`, `document(owner, type, id)` (null when missing or not visible) |
+| `openvibe-sdk/chat` | both | `createChatClient(client)`: `global.send/history/search`, `rooms.*` (list, create, get, update, messages, send, deleteMessage, join, leave, read, members, setMember), `dms.*` (list, create, get, messages, send, deleteMessage, read, unread, blocks, block, unblock), `exportMine()` |
+| `openvibe-sdk/ai` | both | `createAiClient(client)`: `runs.create(workflow, input, {wait, idempotencyKey, …})`, `runs.get/list/cancel/retry/citations/addCitations/waitFor`, `chat/generate/summarize/classify/extract/enrich/embed` |
| `openvibe-sdk/jobs` | both | `createJobsClient(client, {baseUrl?})`: `submit` (Idempotency-Key), `get`, `cancel`, `retry`, `reference`, `unreference`, `events` (SSE, reattaches), `wait`, `file` (raw Response); `isTerminal()`. Default origin: the Tools gateway's jobs facade |
| `openvibe-sdk/jobs` | both | `createJobsClient(client, {baseUrl?})`: `submit` (Idempotency-Key), `get`, `cancel`, `retry`, `reference`, `unreference`, `events` (SSE, reattaches), `wait`, `file` (raw Response); `isTerminal()`. Default origin: the Tools gateway's jobs facade |
| `openvibe-sdk/tools` | both | `createToolsClient(client, {baseUrl?, anonymousReads?})`: `list({family, q, execution, api, status})`, `get(id)`, `schema(id)`, `run(id, input, {files, waitMs, idempotencyKey, signal, timeoutMs})` (+ `.wait()`), `jobs`; `ToolRunError`, `isToolRunError()` |
| `openvibe-sdk/tools` | both | `createToolsClient(client, {baseUrl?, anonymousReads?})`: `list({family, q, execution, api, status})`, `get(id)`, `schema(id)`, `run(id, input, {files, waitMs, idempotencyKey, signal, timeoutMs})` (+ `.wait()`), `jobs`; `ToolRunError`, `isToolRunError()` |
| `openvibe-sdk/projects` | both (user token) | `createProjectsClient(client)`: `catalog`, `list`, `create`, `get`, `update`, `archive`, `setAllowance`, `setEnvironmentPolicy`, `members.*`, `apps.*`, `credentials.list/rotate/revoke`, `grants.list/request/approve/deny/revoke`, `quotas.*`, `audit`, `iterateAudit` |
| `openvibe-sdk/projects` | both (user token) | `createProjectsClient(client)`: `catalog`, `list`, `create`, `get`, `update`, `archive`, `setAllowance`, `setEnvironmentPolicy`, `members.*`, `apps.*`, `credentials.list/rotate/revoke`, `grants.list/request/approve/deny/revoke`, `quotas.*`, `audit`, `iterateAudit` |
@@ -14,6 +14,8 @@ module.exports = {
@@ -14,6 +14,8 @@ module.exports = {
media: require('./src/media'),
media: require('./src/media'),
community: require('./src/community'),
community: require('./src/community'),
search: require('./src/search'),
search: require('./src/search'),
+ chat: require('./src/chat'),
+ ai: require('./src/ai'),
jobs: require('./src/jobs'),
jobs: require('./src/jobs'),
tools: require('./src/tools'),
tools: require('./src/tools'),
projects: require('./src/projects'),
projects: require('./src/projects'),
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
-// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.10.0, MIT). Do not edit.
+// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.11.0, MIT). Do not edit.
// One self-contained ES module: core at the top level plus the browser-safe subpaths as namespaces
// One self-contained ES module: core at the top level plus the browser-safe subpaths as namespaces
-// (auth, registry, modules, realtime, media, community, search, jobs, tools, projects). No server-only code: no service tokens, JWT verification, events, identity.
+// (auth, registry, modules, realtime, media, community, search, chat, ai, jobs, tools, projects). No server-only code: no service tokens, JWT verification, events, identity.
/* eslint-disable */
/* eslint-disable */
const __defs = [
const __defs = [
// ── 0: browser.js ──────────────────────────────────────────────────
// ── 0: browser.js ──────────────────────────────────────────────────
@@ -21,9 +21,11 @@ module.exports = {
@@ -21,9 +21,11 @@ module.exports = {
media: __load(12),
media: __load(12),
community: __load(14),
community: __load(14),
search: __load(15),
search: __load(15),
- jobs: __load(16),
- tools: __load(18),
- projects: __load(19),
+ chat: __load(16),
+ ai: __load(17),
+ jobs: __load(18),
+ tools: __load(20),
+ projects: __load(21),
};
};
},
},
// ── 1: src/core/index.js ───────────────────────────────────────────
// ── 1: src/core/index.js ───────────────────────────────────────────
@@ -38,7 +40,7 @@ const { paginate, offsetPager } = __load(7);
@@ -38,7 +40,7 @@ const { paginate, offsetPager } = __load(7);
const semver = __load(6);
const semver = __load(6);
/** The package version (test/version.test.js keeps it equal to package.json). */
/** The package version (test/version.test.js keeps it equal to package.json). */
-const SDK_VERSION = '0.10.0';
+const SDK_VERSION = '0.11.0';
module.exports = {
module.exports = {
createClient,
createClient,
@@ -1829,7 +1831,160 @@ function createSearchClient(client, defaults = {}) {
@@ -1829,7 +1831,160 @@ function createSearchClient(client, defaults = {}) {
module.exports = { createSearchClient, queryOf };
module.exports = { createSearchClient, queryOf };
},
},
-// ── 16: src/jobs.js ─────────────────────────────────────────────────
+// ── 16: src/chat.js ─────────────────────────────────────────────────
+function (module, exports) {
+'use strict';
+/**
+ * openvibe-sdk/chat: OpenVibe.Chat over REST (roadmap WS-F task 4): global chat, rooms and direct
+ * messages, for bots and apps. Browser-safe. Live delivery is Chat's WebSocket (/ws/chat); this is the
+ * REST side of the same data.
+ *
+ * const chat = createChatClient(client); // a signed-in person's token, or an API token
+ * await chat.global.send('hello');
+ * const room = await chat.rooms.create({ name: 'Builders' });
+ * await chat.rooms.send(room.room.slug, 'first!');
+ * const dm = await chat.dms.create([42]);
+ * await chat.dms.send(dm.conversation.id, 'hi');
+ *
+ * Every call acts as the token's person (Chat has no service acting-for-someone mode). Writes are never
+ * retried (Chat does not deduplicate them); reads are. A missing or unreadable room or conversation is
+ * null from get().
+ */
+const { isOpenVibeError } = __load(3);
+
+const enc = encodeURIComponent;
+
+function createChatClient(client, defaults = {}) {
+ const { baseUrl } = defaults;
+ const call = (opts, o = {}) => client.json({ service: 'chat', baseUrl, audience: 'openvibe.chat', ...opts, signal: o.signal });
+ const once = { idempotencyKey: false };
+ const orNull = (p) => p.catch((err) => { if (isOpenVibeError(err) && (err.status === 404 || err.status === 403)) return null; throw err; });
+ const room = (slug) => `/api/chat/rooms/${enc(slug)}`;
+ const conv = (id) => `/api/dm/conversations/${enc(id)}`;
+
+ return {
+ global: {
+ /** { messages, … } — the global feed; query: limit, after_id (a delta after a message id). */
+ history: (query = {}, o = {}) => call({ path: '/api/chat/global/history', query }, o),
+ /** Post to global chat as the token's person. { message: string, reply_to_id?, auto_delete_minutes? } */
+ send: (message, extra = {}, o = {}) => call({ method: 'POST', path: '/api/chat/send', json: { message, ...extra }, ...once }, o),
+ /** Your own lines (staff with staff.moderation.logs may pass user_id). query: q, user_id, stream_id, limit, offset */
+ search: (query = {}, o = {}) => call({ path: '/api/chat/search', query }, o),
+ },
+ rooms: {
+ list: (o = {}) => call({ path: '/api/chat/rooms' }, o),
+ create: (input, o = {}) => call({ method: 'POST', path: '/api/chat/rooms', json: input, ...once }, o),
+ get: (slug, o = {}) => orNull(call({ path: room(slug) }, o)),
+ update: (slug, patch, o = {}) => call({ method: 'PATCH', path: room(slug), json: patch, ...once }, o),
+ /** query: before | after (message id), limit */
+ messages: (slug, query = {}, o = {}) => call({ path: `${room(slug)}/messages`, query }, o),
+ send: (slug, message, o = {}) => call({ method: 'POST', path: `${room(slug)}/messages`, json: { message }, ...once }, o),
+ deleteMessage: (slug, id, o = {}) => call({ method: 'DELETE', path: `${room(slug)}/messages/${enc(id)}`, ...once }, o),
+ join: (slug, o = {}) => call({ method: 'POST', path: `${room(slug)}/join`, ...once }, o),
+ leave: (slug, o = {}) => call({ method: 'POST', path: `${room(slug)}/leave`, ...once }, o),
+ read: (slug, lastId, o = {}) => call({ method: 'POST', path: `${room(slug)}/read`, json: lastId != null ? { last_id: lastId } : {}, ...once }, o),
+ members: (slug, o = {}) => call({ path: `${room(slug)}/members` }, o),
+ /** Owners and moderators: role member | mod | blocked | none */
+ setMember: (slug, username, role, o = {}) => call({ method: 'POST', path: `${room(slug)}/members`, json: { username, role }, ...once }, o),
+ },
+ dms: {
+ list: (o = {}) => call({ path: '/api/dm/conversations' }, o),
+ /** A 1:1 conversation is reused; more ids make a group ({ name }). */
+ create: (userIds, extra = {}, o = {}) => call({ method: 'POST', path: '/api/dm/conversations', json: { user_ids: userIds, ...extra }, ...once }, o),
+ get: (id, o = {}) => orNull(call({ path: conv(id) }, o)),
+ /** query: limit, before */
+ messages: (id, query = {}, o = {}) => call({ path: `${conv(id)}/messages`, query }, o),
+ send: (id, message, o = {}) => call({ method: 'POST', path: `${conv(id)}/messages`, json: { message }, ...once }, o),
+ deleteMessage: (id, msgId, o = {}) => call({ method: 'DELETE', path: `${conv(id)}/messages/${enc(msgId)}`, ...once }, o),
+ read: (id, o = {}) => call({ method: 'POST', path: `${conv(id)}/read`, ...once }, o),
+ unread: (o = {}) => call({ path: '/api/dm/unread' }, o),
+ blocks: (o = {}) => call({ path: '/api/dm/blocks' }, o),
+ block: (userId, o = {}) => call({ method: 'POST', path: `/api/dm/blocks/${enc(userId)}`, ...once }, o),
+ unblock: (userId, o = {}) => call({ method: 'DELETE', path: `/api/dm/blocks/${enc(userId)}`, ...once }, o),
+ },
+ /** Every message you sent that is still visible: { username, total, truncated, messages } (JSON). */
+ exportMine: (o = {}) => call({ path: '/api/chat/me/export' }, o),
+ };
+}
+
+module.exports = { createChatClient };
+},
+// ── 17: src/ai.js ───────────────────────────────────────────────────
+function (module, exports) {
+'use strict';
+/**
+ * openvibe-sdk/ai: OpenVibe.AI's run API (roadmap WS-F task 4). For first-party services (a Network
+ * service token with ai.run.create / ai.run.read, audience openvibe.ai) and developer apps granted
+ * them; workflows are namespaced by product (a caller may only run the namespaces it was granted).
+ *
+ * const ai = createAiClient(client);
+ * const { run } = await ai.runs.create('live.stream.describe_frame', { image_url }, { wait: 20000 });
+ * const done = run.status === 'succeeded' ? run : await ai.runs.waitFor(run.id);
+ * const summary = await ai.summarize({ text }, { wait: 30000 }); // ai.summarize, waited for
+ *
+ * create() answers { run } with 201 (finished within the wait, or served from cache), 202 (still queued
+ * or running: poll get() or use waitFor()), or 200 (an idempotent replay: `replayed: true`). Pass an
+ * idempotencyKey so a retried create never runs twice; creates are otherwise not retried. A quota
+ * refusal or a full queue is a 429 error carrying Retry-After.
+ */
+const { isOpenVibeError } = __load(3);
+
+const enc = encodeURIComponent;
+const TERMINAL = new Set(['succeeded', 'failed', 'cancelled']);
+const OPS = ['chat', 'generate', 'summarize', 'classify', 'extract', 'enrich', 'embed'];
+
+function createAiClient(client, defaults = {}) {
+ const { baseUrl } = defaults;
+ const call = (opts, o = {}) => client.json({ service: 'ai', baseUrl, audience: 'openvibe.ai', ...opts, signal: o.signal });
+ const writeOpts = (o) => (o.idempotencyKey ? { idempotencyKey: o.idempotencyKey } : { idempotencyKey: false });
+ const waitQuery = (o) => (o.wait != null ? { wait: Math.max(0, Math.floor(o.wait)) } : undefined);
+ const orNull = (p) => p.catch((err) => { if (isOpenVibeError(err) && err.status === 404) return null; throw err; });
+
+ const runs = {
+ /** workflow key (e.g. 'wiki.generate_page'), its input; o: { wait (ms), version, idempotencyKey, target, attribution, onBehalfOf, options } */
+ create(workflow, input = {}, o = {}) {
+ const body = { workflow, input };
+ if (o.version != null) body.version = o.version;
+ if (o.target !== undefined) body.target = o.target;
+ if (o.attribution !== undefined) body.attribution = o.attribution;
+ if (o.onBehalfOf !== undefined) body.on_behalf_of = o.onBehalfOf;
+ if (o.options !== undefined) body.options = o.options;
+ return call({ method: 'POST', path: '/api/v1/runs', query: waitQuery(o), json: body, ...writeOpts(o) }, o);
+ },
+ list: (query = {}, o = {}) => call({ path: '/api/v1/runs', query }, o),
+ get: (id, o = {}) => orNull(call({ path: `/api/v1/runs/${enc(id)}` }, o)),
+ cancel: (id, o = {}) => call({ method: 'POST', path: `/api/v1/runs/${enc(id)}/cancel`, idempotencyKey: false }, o),
+ retry: (id, o = {}) => call({ method: 'POST', path: `/api/v1/runs/${enc(id)}/retry`, query: waitQuery(o), ...writeOpts(o) }, o),
+ citations: (id, o = {}) => call({ path: `/api/v1/runs/${enc(id)}/citations` }, o),
+ addCitations: (id, citations, o = {}) => call({ method: 'POST', path: `/api/v1/runs/${enc(id)}/citations`, json: { citations }, idempotencyKey: false }, o),
+ /** Poll get() until the run finishes (succeeded, failed or cancelled) or `timeoutMs` passes. → the run */
+ async waitFor(id, { intervalMs = 1000, timeoutMs = 120000, signal } = {}) {
+ const until = Date.now() + timeoutMs;
+ for (;;) {
+ const got = await runs.get(id, { signal });
+ const run = got && (got.run || got);
+ if (!run) throw new Error(`run ${id} not found`);
+ if (TERMINAL.has(run.status)) return run;
+ if (Date.now() + intervalMs > until) throw new Error(`run ${id} still ${run.status} after ${timeoutMs} ms`);
+ await new Promise((r) => setTimeout(r, intervalMs));
+ if (signal && signal.aborted) throw new Error('aborted');
+ }
+ },
+ };
+
+ const api = { runs, TERMINAL };
+ // Direct operations: a run of workflow ai.<op> with the input as the body, waited for by AI.
+ for (const op of OPS) {
+ api[op] = (input = {}, o = {}) => call({ method: 'POST', path: `/api/v1/${op}`, query: waitQuery(o),
+ json: { ...input, ...(o.target !== undefined ? { target: o.target } : {}), ...(o.attribution !== undefined ? { attribution: o.attribution } : {}), ...(o.options !== undefined ? { options: o.options } : {}) },
+ ...writeOpts(o) }, o);
+ }
+ return api;
+}
+
+module.exports = { createAiClient, TERMINAL, OPS };
+},
+// ── 18: src/jobs.js ─────────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
/**
/**
@@ -1867,7 +2022,7 @@ function (module, exports) {
@@ -1867,7 +2022,7 @@ function (module, exports) {
*/
*/
const { OpenVibeError, isOpenVibeError } = __load(3);
const { OpenVibeError, isOpenVibeError } = __load(3);
const { newIdempotencyKey } = __load(5);
const { newIdempotencyKey } = __load(5);
-const { appendFiles } = __load(17);
+const { appendFiles } = __load(19);
const { parseSSE } = __load(11);
const { parseSSE } = __load(11);
const TERMINAL = new Set(['succeeded', 'failed', 'cancelled']);
const TERMINAL = new Set(['succeeded', 'failed', 'cancelled']);
@@ -2031,7 +2186,7 @@ function createJobsClient(client, { baseUrl, service = 'tools', audience = 'open
@@ -2031,7 +2186,7 @@ function createJobsClient(client, { baseUrl, service = 'tools', audience = 'open
module.exports = { createJobsClient, isTerminal, TERMINAL_STATES: [...TERMINAL] };
module.exports = { createJobsClient, isTerminal, TERMINAL_STATES: [...TERMINAL] };
},
},
-// ── 17: src/core/form.js ────────────────────────────────────────────
+// ── 19: src/core/form.js ────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
/**
/**
@@ -2065,7 +2220,7 @@ function appendFiles(form, list, where) {
@@ -2065,7 +2220,7 @@ function appendFiles(form, list, where) {
module.exports = { isBlob, isUpload, appendFiles };
module.exports = { isBlob, isUpload, appendFiles };
},
},
-// ── 18: src/tools.js ────────────────────────────────────────────────
+// ── 20: src/tools.js ────────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
/**
/**
@@ -2099,8 +2254,8 @@ function (module, exports) {
@@ -2099,8 +2254,8 @@ function (module, exports) {
*/
*/
const { OpenVibeError, isOpenVibeError } = __load(3);
const { OpenVibeError, isOpenVibeError } = __load(3);
const { newIdempotencyKey } = __load(5);
const { newIdempotencyKey } = __load(5);
-const { isUpload, appendFiles } = __load(17);
-const { createJobsClient } = __load(16);
+const { isUpload, appendFiles } = __load(19);
+const { createJobsClient } = __load(18);
const enc = encodeURIComponent;
const enc = encodeURIComponent;
const ID_RE = /^[a-z][a-z0-9-]{0,39}$/;
const ID_RE = /^[a-z][a-z0-9-]{0,39}$/;
@@ -2334,7 +2489,7 @@ function createToolsClient(client, { baseUrl, service = 'tools', audience = 'ope
@@ -2334,7 +2489,7 @@ function createToolsClient(client, { baseUrl, service = 'tools', audience = 'ope
module.exports = { createToolsClient, ToolRunError, isToolRunError };
module.exports = { createToolsClient, ToolRunError, isToolRunError };
},
},
-// ── 19: src/projects.js ─────────────────────────────────────────────
+// ── 21: src/projects.js ─────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
/**
/**
@@ -2466,6 +2621,6 @@ export const {
@@ -2466,6 +2621,6 @@ export const {
startSpan, contextFromHeaders, ulid, newEventId, newIdempotencyKey, isActingSubjectId,
startSpan, contextFromHeaders, ulid, newEventId, newIdempotencyKey, isActingSubjectId,
satisfiesRange, compareVersions, CONTRACTS_RANGE, DEFAULT_NETWORK, SDK_VERSION, auth,
satisfiesRange, compareVersions, CONTRACTS_RANGE, DEFAULT_NETWORK, SDK_VERSION, auth,
registry, modules, realtime, media, community, search,
registry, modules, realtime, media, community, search,
- jobs, tools, projects,
+ chat, ai, jobs, tools, projects,
} = sdk;
} = sdk;
export default sdk;
export default sdk;
new file mode 100644
new file mode 100644
@@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
+// Generated by scripts/esm.js from src/ai.js. Do not edit.
+import cjs from '../src/ai.js';
+
+export const {
+ createAiClient, TERMINAL, OPS,
+} = cjs;
+export default cjs;
new file mode 100644
new file mode 100644
@@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
+// Generated by scripts/esm.js from src/chat.js. Do not edit.
+import cjs from '../src/chat.js';
+
+export const {
+ createChatClient,
+} = cjs;
+export default cjs;
@@ -6,6 +6,7 @@ export const {
@@ -6,6 +6,7 @@ export const {
startSpan, contextFromHeaders, ulid, newEventId, newIdempotencyKey, isActingSubjectId,
startSpan, contextFromHeaders, ulid, newEventId, newIdempotencyKey, isActingSubjectId,
satisfiesRange, compareVersions, CONTRACTS_RANGE, DEFAULT_NETWORK, SDK_VERSION, auth,
satisfiesRange, compareVersions, CONTRACTS_RANGE, DEFAULT_NETWORK, SDK_VERSION, auth,
registry, identity, modules, events, realtime, media,
registry, identity, modules, events, realtime, media,
- community, search, jobs, tools, projects, vip,
+ community, search, chat, ai, jobs, tools,
+ projects, vip,
} = cjs;
} = cjs;
export default cjs;
export default cjs;
@@ -17,6 +17,8 @@ module.exports = {
@@ -17,6 +17,8 @@ module.exports = {
media: require('./src/media'),
media: require('./src/media'),
community: require('./src/community'),
community: require('./src/community'),
search: require('./src/search'),
search: require('./src/search'),
+ chat: require('./src/chat'),
+ ai: require('./src/ai'),
jobs: require('./src/jobs'),
jobs: require('./src/jobs'),
tools: require('./src/tools'),
tools: require('./src/tools'),
projects: require('./src/projects'),
projects: require('./src/projects'),
@@ -1,12 +1,12 @@
@@ -1,12 +1,12 @@
{
{
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.10.0",
+ "version": "0.11.0",
"lockfileVersion": 3,
"lockfileVersion": 3,
"requires": true,
"requires": true,
"packages": {
"packages": {
"": {
"": {
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.10.0",
+ "version": "0.11.0",
"license": "MIT",
"license": "MIT",
"devDependencies": {
"devDependencies": {
"better-sqlite3": "^11.10.0",
"better-sqlite3": "^11.10.0",
@@ -1,7 +1,7 @@
@@ -1,7 +1,7 @@
{
{
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.10.0",
- "description": "Supported browser and server clients for the OpenVibe platform: auth (incl. developer apps), registry discovery, identity, user modules, events, realtime, media, community, search, Tools (registry, run API and jobs), VIP memberships and developer projects, with deadlines, safe retries, problem errors and trace propagation.",
+ "version": "0.11.0",
+ "description": "Supported browser and server clients for the OpenVibe platform: auth (incl. developer apps), registry discovery, identity, user modules, events, realtime, media, community, search, chat, AI runs, Tools (registry, run API and jobs), VIP memberships and developer projects, with deadlines, safe retries, problem errors and trace propagation.",
"main": "index.js",
"main": "index.js",
"types": "types/index.d.ts",
"types": "types/index.d.ts",
"browser": "browser.js",
"browser": "browser.js",
@@ -70,6 +70,16 @@
@@ -70,6 +70,16 @@
"import": "./esm/search.mjs",
"import": "./esm/search.mjs",
"default": "./src/search.js"
"default": "./src/search.js"
},
},
+ "./chat": {
+ "types": "./types/chat.d.ts",
+ "import": "./esm/chat.mjs",
+ "default": "./src/chat.js"
+ },
+ "./ai": {
+ "types": "./types/ai.d.ts",
+ "import": "./esm/ai.mjs",
+ "default": "./src/ai.js"
+ },
"./jobs": {
"./jobs": {
"types": "./types/jobs.d.ts",
"types": "./types/jobs.d.ts",
"import": "./esm/jobs.mjs",
"import": "./esm/jobs.mjs",
@@ -24,6 +24,8 @@ const ENTRIES = {
@@ -24,6 +24,8 @@ const ENTRIES = {
'media.mjs': '../src/media.js',
'media.mjs': '../src/media.js',
'community.mjs': '../src/community.js',
'community.mjs': '../src/community.js',
'search.mjs': '../src/search.js',
'search.mjs': '../src/search.js',
+ 'chat.mjs': '../src/chat.js',
+ 'ai.mjs': '../src/ai.js',
'jobs.mjs': '../src/jobs.js',
'jobs.mjs': '../src/jobs.js',
'tools.mjs': '../src/tools.js',
'tools.mjs': '../src/tools.js',
'projects.mjs': '../src/projects.js',
'projects.mjs': '../src/projects.js',
new file mode 100644
new file mode 100644
@@ -0,0 +1,72 @@
@@ -0,0 +1,72 @@
+'use strict';
+/**
+ * openvibe-sdk/ai: OpenVibe.AI's run API (roadmap WS-F task 4). For first-party services (a Network
+ * service token with ai.run.create / ai.run.read, audience openvibe.ai) and developer apps granted
+ * them; workflows are namespaced by product (a caller may only run the namespaces it was granted).
+ *
+ * const ai = createAiClient(client);
+ * const { run } = await ai.runs.create('live.stream.describe_frame', { image_url }, { wait: 20000 });
+ * const done = run.status === 'succeeded' ? run : await ai.runs.waitFor(run.id);
+ * const summary = await ai.summarize({ text }, { wait: 30000 }); // ai.summarize, waited for
+ *
+ * create() answers { run } with 201 (finished within the wait, or served from cache), 202 (still queued
+ * or running: poll get() or use waitFor()), or 200 (an idempotent replay: `replayed: true`). Pass an
+ * idempotencyKey so a retried create never runs twice; creates are otherwise not retried. A quota
+ * refusal or a full queue is a 429 error carrying Retry-After.
+ */
+const { isOpenVibeError } = require('./core/errors');
+
+const enc = encodeURIComponent;
+const TERMINAL = new Set(['succeeded', 'failed', 'cancelled']);
+const OPS = ['chat', 'generate', 'summarize', 'classify', 'extract', 'enrich', 'embed'];
+
+function createAiClient(client, defaults = {}) {
+ const { baseUrl } = defaults;
+ const call = (opts, o = {}) => client.json({ service: 'ai', baseUrl, audience: 'openvibe.ai', ...opts, signal: o.signal });
+ const writeOpts = (o) => (o.idempotencyKey ? { idempotencyKey: o.idempotencyKey } : { idempotencyKey: false });
+ const waitQuery = (o) => (o.wait != null ? { wait: Math.max(0, Math.floor(o.wait)) } : undefined);
+ const orNull = (p) => p.catch((err) => { if (isOpenVibeError(err) && err.status === 404) return null; throw err; });
+
+ const runs = {
+ /** workflow key (e.g. 'wiki.generate_page'), its input; o: { wait (ms), version, idempotencyKey, target, attribution, onBehalfOf, options } */
+ create(workflow, input = {}, o = {}) {
+ const body = { workflow, input };
+ if (o.version != null) body.version = o.version;
+ if (o.target !== undefined) body.target = o.target;
+ if (o.attribution !== undefined) body.attribution = o.attribution;
+ if (o.onBehalfOf !== undefined) body.on_behalf_of = o.onBehalfOf;
+ if (o.options !== undefined) body.options = o.options;
+ return call({ method: 'POST', path: '/api/v1/runs', query: waitQuery(o), json: body, ...writeOpts(o) }, o);
+ },
+ list: (query = {}, o = {}) => call({ path: '/api/v1/runs', query }, o),
+ get: (id, o = {}) => orNull(call({ path: `/api/v1/runs/${enc(id)}` }, o)),
+ cancel: (id, o = {}) => call({ method: 'POST', path: `/api/v1/runs/${enc(id)}/cancel`, idempotencyKey: false }, o),
+ retry: (id, o = {}) => call({ method: 'POST', path: `/api/v1/runs/${enc(id)}/retry`, query: waitQuery(o), ...writeOpts(o) }, o),
+ citations: (id, o = {}) => call({ path: `/api/v1/runs/${enc(id)}/citations` }, o),
+ addCitations: (id, citations, o = {}) => call({ method: 'POST', path: `/api/v1/runs/${enc(id)}/citations`, json: { citations }, idempotencyKey: false }, o),
+ /** Poll get() until the run finishes (succeeded, failed or cancelled) or `timeoutMs` passes. → the run */
+ async waitFor(id, { intervalMs = 1000, timeoutMs = 120000, signal } = {}) {
+ const until = Date.now() + timeoutMs;
+ for (;;) {
+ const got = await runs.get(id, { signal });
+ const run = got && (got.run || got);
+ if (!run) throw new Error(`run ${id} not found`);
+ if (TERMINAL.has(run.status)) return run;
+ if (Date.now() + intervalMs > until) throw new Error(`run ${id} still ${run.status} after ${timeoutMs} ms`);
+ await new Promise((r) => setTimeout(r, intervalMs));
+ if (signal && signal.aborted) throw new Error('aborted');
+ }
+ },
+ };
+
+ const api = { runs, TERMINAL };
+ // Direct operations: a run of workflow ai.<op> with the input as the body, waited for by AI.
+ for (const op of OPS) {
+ api[op] = (input = {}, o = {}) => call({ method: 'POST', path: `/api/v1/${op}`, query: waitQuery(o),
+ json: { ...input, ...(o.target !== undefined ? { target: o.target } : {}), ...(o.attribution !== undefined ? { attribution: o.attribution } : {}), ...(o.options !== undefined ? { options: o.options } : {}) },
+ ...writeOpts(o) }, o);
+ }
+ return api;
+}
+
+module.exports = { createAiClient, TERMINAL, OPS };
new file mode 100644
new file mode 100644
@@ -0,0 +1,75 @@
@@ -0,0 +1,75 @@
+'use strict';
+/**
+ * openvibe-sdk/chat: OpenVibe.Chat over REST (roadmap WS-F task 4): global chat, rooms and direct
+ * messages, for bots and apps. Browser-safe. Live delivery is Chat's WebSocket (/ws/chat); this is the
+ * REST side of the same data.
+ *
+ * const chat = createChatClient(client); // a signed-in person's token, or an API token
+ * await chat.global.send('hello');
+ * const room = await chat.rooms.create({ name: 'Builders' });
+ * await chat.rooms.send(room.room.slug, 'first!');
+ * const dm = await chat.dms.create([42]);
+ * await chat.dms.send(dm.conversation.id, 'hi');
+ *
+ * Every call acts as the token's person (Chat has no service acting-for-someone mode). Writes are never
+ * retried (Chat does not deduplicate them); reads are. A missing or unreadable room or conversation is
+ * null from get().
+ */
+const { isOpenVibeError } = require('./core/errors');
+
+const enc = encodeURIComponent;
+
+function createChatClient(client, defaults = {}) {
+ const { baseUrl } = defaults;
+ const call = (opts, o = {}) => client.json({ service: 'chat', baseUrl, audience: 'openvibe.chat', ...opts, signal: o.signal });
+ const once = { idempotencyKey: false };
+ const orNull = (p) => p.catch((err) => { if (isOpenVibeError(err) && (err.status === 404 || err.status === 403)) return null; throw err; });
+ const room = (slug) => `/api/chat/rooms/${enc(slug)}`;
+ const conv = (id) => `/api/dm/conversations/${enc(id)}`;
+
+ return {
+ global: {
+ /** { messages, … } — the global feed; query: limit, after_id (a delta after a message id). */
+ history: (query = {}, o = {}) => call({ path: '/api/chat/global/history', query }, o),
+ /** Post to global chat as the token's person. { message: string, reply_to_id?, auto_delete_minutes? } */
+ send: (message, extra = {}, o = {}) => call({ method: 'POST', path: '/api/chat/send', json: { message, ...extra }, ...once }, o),
+ /** Your own lines (staff with staff.moderation.logs may pass user_id). query: q, user_id, stream_id, limit, offset */
+ search: (query = {}, o = {}) => call({ path: '/api/chat/search', query }, o),
+ },
+ rooms: {
+ list: (o = {}) => call({ path: '/api/chat/rooms' }, o),
+ create: (input, o = {}) => call({ method: 'POST', path: '/api/chat/rooms', json: input, ...once }, o),
+ get: (slug, o = {}) => orNull(call({ path: room(slug) }, o)),
+ update: (slug, patch, o = {}) => call({ method: 'PATCH', path: room(slug), json: patch, ...once }, o),
+ /** query: before | after (message id), limit */
+ messages: (slug, query = {}, o = {}) => call({ path: `${room(slug)}/messages`, query }, o),
+ send: (slug, message, o = {}) => call({ method: 'POST', path: `${room(slug)}/messages`, json: { message }, ...once }, o),
+ deleteMessage: (slug, id, o = {}) => call({ method: 'DELETE', path: `${room(slug)}/messages/${enc(id)}`, ...once }, o),
+ join: (slug, o = {}) => call({ method: 'POST', path: `${room(slug)}/join`, ...once }, o),
+ leave: (slug, o = {}) => call({ method: 'POST', path: `${room(slug)}/leave`, ...once }, o),
+ read: (slug, lastId, o = {}) => call({ method: 'POST', path: `${room(slug)}/read`, json: lastId != null ? { last_id: lastId } : {}, ...once }, o),
+ members: (slug, o = {}) => call({ path: `${room(slug)}/members` }, o),
+ /** Owners and moderators: role member | mod | blocked | none */
+ setMember: (slug, username, role, o = {}) => call({ method: 'POST', path: `${room(slug)}/members`, json: { username, role }, ...once }, o),
+ },
+ dms: {
+ list: (o = {}) => call({ path: '/api/dm/conversations' }, o),
+ /** A 1:1 conversation is reused; more ids make a group ({ name }). */
+ create: (userIds, extra = {}, o = {}) => call({ method: 'POST', path: '/api/dm/conversations', json: { user_ids: userIds, ...extra }, ...once }, o),
+ get: (id, o = {}) => orNull(call({ path: conv(id) }, o)),
+ /** query: limit, before */
+ messages: (id, query = {}, o = {}) => call({ path: `${conv(id)}/messages`, query }, o),
+ send: (id, message, o = {}) => call({ method: 'POST', path: `${conv(id)}/messages`, json: { message }, ...once }, o),
+ deleteMessage: (id, msgId, o = {}) => call({ method: 'DELETE', path: `${conv(id)}/messages/${enc(msgId)}`, ...once }, o),
+ read: (id, o = {}) => call({ method: 'POST', path: `${conv(id)}/read`, ...once }, o),
+ unread: (o = {}) => call({ path: '/api/dm/unread' }, o),
+ blocks: (o = {}) => call({ path: '/api/dm/blocks' }, o),
+ block: (userId, o = {}) => call({ method: 'POST', path: `/api/dm/blocks/${enc(userId)}`, ...once }, o),
+ unblock: (userId, o = {}) => call({ method: 'DELETE', path: `/api/dm/blocks/${enc(userId)}`, ...once }, o),
+ },
+ /** Every message you sent that is still visible: { username, total, truncated, messages } (JSON). */
+ exportMine: (o = {}) => call({ path: '/api/chat/me/export' }, o),
+ };
+}
+
+module.exports = { createChatClient };
@@ -8,7 +8,7 @@ const { paginate, offsetPager } = require('./paginate');
@@ -8,7 +8,7 @@ const { paginate, offsetPager } = require('./paginate');
const semver = require('./semver');
const semver = require('./semver');
/** The package version (test/version.test.js keeps it equal to package.json). */
/** The package version (test/version.test.js keeps it equal to package.json). */
-const SDK_VERSION = '0.10.0';
+const SDK_VERSION = '0.11.0';
module.exports = {
module.exports = {
createClient,
createClient,
new file mode 100644
new file mode 100644
@@ -0,0 +1,51 @@
@@ -0,0 +1,51 @@
+'use strict';
+/** AI: create with wait and an idempotency key, no retried create without one, waitFor polls to the end, direct ops. */
+const assert = require('node:assert/strict');
+const { stubServer, send, run } = require('./helpers');
+const { createClient } = require('../src/core');
+const { createAiClient } = require('../src/ai');
+
+async function aiStub() {
+ let polls = 0, busy = 0;
+ return stubServer((req, res, body) => {
+ const u = new URL(req.url, 'http://x');
+ if (req.method === 'POST' && u.pathname === '/api/v1/runs') {
+ const b = JSON.parse(String(body));
+ if (b.workflow === 'busy.flow' && busy++ === 0) return send(res, 503, { code: 'ai.busy' });
+ return send(res, 202, { run: { id: 'run_1', status: 'queued' } });
+ }
+ if (u.pathname === '/api/v1/runs/run_1') return send(res, 200, { run: { id: 'run_1', status: ++polls >= 3 ? 'succeeded' : 'running', output: polls >= 3 ? { ok: true } : undefined } });
+ if (u.pathname === '/api/v1/runs/missing') return send(res, 404, { code: 'run.not_found' });
+ if (u.pathname === '/api/v1/summarize') return send(res, 201, { run: { id: 'run_2', status: 'succeeded' } });
+ return send(res, 404, {});
+ });
+}
+
+run([
+ ['create sends workflow, input, wait and the idempotency key; waitFor polls until finished', async () => {
+ const srv = await aiStub();
+ const ai = createAiClient(createClient({ baseUrls: { ai: srv.url }, getToken: ({ audience }) => `svc-${audience}` }));
+ const r = await ai.runs.create('wiki.generate_page', { title: 'X' }, { wait: 2000, idempotencyKey: 'k-1', onBehalfOf: { type: 'user', id: 'usr_x' } });
+ assert.equal(r.run.status, 'queued');
+ const req = srv.requests[0];
+ assert.equal(req.url, '/api/v1/runs?wait=2000');
+ assert.deepEqual(JSON.parse(String(req.body)), { workflow: 'wiki.generate_page', input: { title: 'X' }, on_behalf_of: { type: 'user', id: 'usr_x' } });
+ assert.equal(req.headers.authorization, 'Bearer svc-openvibe.ai');
+ assert.equal(req.headers['idempotency-key'], 'k-1');
+ const done = await ai.runs.waitFor('run_1', { intervalMs: 5 });
+ assert.equal(done.status, 'succeeded');
+ assert.equal(await ai.runs.get('missing'), null);
+ await srv.close();
+ }],
+ ['a create without a key is not retried; direct ops post the input as the body', async () => {
+ const srv = await aiStub();
+ const ai = createAiClient(createClient({ baseUrls: { ai: srv.url }, token: 't', retries: 2 }));
+ await assert.rejects(ai.runs.create('busy.flow', {}), (e) => e.status === 503);
+ assert.equal(srv.requests.length, 1, 'one attempt');
+ const s = await ai.summarize({ text: 'long text' }, { wait: 30000 });
+ assert.equal(s.run.id, 'run_2');
+ assert.equal(srv.requests[1].url, '/api/v1/summarize?wait=30000');
+ assert.deepEqual(JSON.parse(String(srv.requests[1].body)), { text: 'long text' });
+ await srv.close();
+ }],
+]);
new file mode 100644
new file mode 100644
@@ -0,0 +1,51 @@
@@ -0,0 +1,51 @@
+'use strict';
+/** Chat: paths, bodies and methods for global, rooms and DMs; writes never retried; a missing room is null. */
+const assert = require('node:assert/strict');
+const { stubServer, send, run } = require('./helpers');
+const { createClient } = require('../src/core');
+const { createChatClient } = require('../src/chat');
+
+async function chatStub() {
+ let busy = 0;
+ return stubServer((req, res) => {
+ if (req.url.startsWith('/api/chat/rooms/private-one')) return send(res, 404, { error: 'Room not found' });
+ if (req.url === '/api/chat/send' && busy++ === 0) return send(res, 503, { error: 'busy' });
+ return send(res, 200, { ok: true, url: req.url });
+ });
+}
+
+run([
+ ['global, rooms and DMs go to the right paths with the right bodies', async () => {
+ const srv = await chatStub();
+ const chat = createChatClient(createClient({ baseUrls: { chat: srv.url }, token: 'user-jwt', retries: 2 }));
+ await chat.rooms.create({ name: 'Builders' });
+ await chat.rooms.send('builders', 'first!');
+ await chat.rooms.read('builders', 12);
+ await chat.rooms.setMember('builders', 'bob', 'mod');
+ await chat.dms.create([42], { name: 'pair' });
+ await chat.dms.send(7, 'hi');
+ await chat.dms.messages(7, { limit: 20 });
+ await chat.global.history({ after_id: 5 });
+ const got = srv.requests.map((r) => [r.method, r.url, r.body && String(r.body).length ? JSON.parse(String(r.body)) : null]);
+ assert.deepEqual(got, [
+ ['POST', '/api/chat/rooms', { name: 'Builders' }],
+ ['POST', '/api/chat/rooms/builders/messages', { message: 'first!' }],
+ ['POST', '/api/chat/rooms/builders/read', { last_id: 12 }],
+ ['POST', '/api/chat/rooms/builders/members', { username: 'bob', role: 'mod' }],
+ ['POST', '/api/dm/conversations', { user_ids: [42], name: 'pair' }],
+ ['POST', '/api/dm/conversations/7/messages', { message: 'hi' }],
+ ['GET', '/api/dm/conversations/7/messages?limit=20', null],
+ ['GET', '/api/chat/global/history?after_id=5', null],
+ ]);
+ assert.equal(srv.requests[0].headers.authorization, 'Bearer user-jwt');
+ await srv.close();
+ }],
+ ['a failed send is not retried; a room you may not read is null', async () => {
+ const srv = await chatStub();
+ const chat = createChatClient(createClient({ baseUrls: { chat: srv.url }, token: 't', retries: 2 }));
+ await assert.rejects(chat.global.send('hello'), (e) => e.status === 503);
+ assert.equal(srv.requests.filter((r) => r.url === '/api/chat/send').length, 1, 'one attempt only');
+ assert.equal(await chat.rooms.get('private-one'), null);
+ await srv.close();
+ }],
+]);
@@ -66,7 +66,7 @@ run([
@@ -66,7 +66,7 @@ run([
})().catch((err) => { console.error(err); process.exit(1); });`;
})().catch((err) => { console.error(err); process.exit(1); });`;
const res = execFileSync(process.execPath, ['-e', script], { cwd: app, encoding: 'utf8' });
const res = execFileSync(process.execPath, ['-e', script], { cwd: app, encoding: 'utf8' });
assert.match(res, new RegExp(`consumer ok: ${subpaths.length} subpaths \\+ the browser bundle`));
assert.match(res, new RegExp(`consumer ok: ${subpaths.length} subpaths \\+ the browser bundle`));
- assert.equal(subpaths.length, 18);
+ assert.equal(subpaths.length, 20);
assert.ok(!fs.existsSync(path.join(app, 'node_modules/openvibe-contracts')), 'the optional peer is not installed');
assert.ok(!fs.existsSync(path.join(app, 'node_modules/openvibe-contracts')), 'the optional peer is not installed');
} finally {
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
fs.rmSync(tmp, { recursive: true, force: true });
new file mode 100644
new file mode 100644
@@ -0,0 +1,20 @@
@@ -0,0 +1,20 @@
+import type { OpenVibeClient } from './core';
+
+export interface AiRun { id: string; status: 'queued' | 'running' | 'succeeded' | 'failed' | 'cancelled' | string; workflow?: { key: string; version: number }; output?: unknown; error?: unknown; [field: string]: unknown; }
+export interface RunOptions { wait?: number; version?: number; idempotencyKey?: string; target?: unknown; attribution?: unknown; onBehalfOf?: unknown; options?: Record<string, unknown>; signal?: AbortSignal; }
+export interface RunResponse { run: AiRun; replayed?: boolean }
+export interface AiRuns {
+ create(workflow: string, input?: Record<string, unknown>, opts?: RunOptions): Promise<RunResponse>;
+ list(query?: Record<string, string | number>, opts?: { signal?: AbortSignal }): Promise<{ runs: AiRun[]; [field: string]: unknown }>;
+ get(id: string, opts?: { signal?: AbortSignal }): Promise<RunResponse | null>;
+ cancel(id: string, opts?: { signal?: AbortSignal }): Promise<RunResponse>;
+ retry(id: string, opts?: Pick<RunOptions, 'wait' | 'idempotencyKey' | 'signal'>): Promise<RunResponse>;
+ citations(id: string, opts?: { signal?: AbortSignal }): Promise<{ citations: Array<Record<string, unknown>> }>;
+ addCitations(id: string, citations: Array<Record<string, unknown>>, opts?: { signal?: AbortSignal }): Promise<{ citations: Array<Record<string, unknown>> }>;
+ waitFor(id: string, opts?: { intervalMs?: number; timeoutMs?: number; signal?: AbortSignal }): Promise<AiRun>;
+}
+type Op = (input?: Record<string, unknown>, opts?: Omit<RunOptions, 'version' | 'onBehalfOf'>) => Promise<RunResponse>;
+export interface AiClient { runs: AiRuns; TERMINAL: Set<string>; chat: Op; generate: Op; summarize: Op; classify: Op; extract: Op; enrich: Op; embed: Op; }
+export function createAiClient(client: OpenVibeClient, defaults?: { baseUrl?: string }): AiClient;
+export const TERMINAL: Set<string>;
+export const OPS: string[];
@@ -6,6 +6,8 @@ import * as realtime from './realtime';
@@ -6,6 +6,8 @@ import * as realtime from './realtime';
import * as media from './media';
import * as media from './media';
import * as community from './community';
import * as community from './community';
import * as search from './search';
import * as search from './search';
+import * as chat from './chat';
+import * as ai from './ai';
import * as jobs from './jobs';
import * as jobs from './jobs';
import * as tools from './tools';
import * as tools from './tools';
import * as projects from './projects';
import * as projects from './projects';
@@ -19,12 +21,14 @@ export * as realtime from './realtime';
@@ -19,12 +21,14 @@ export * as realtime from './realtime';
export * as media from './media';
export * as media from './media';
export * as community from './community';
export * as community from './community';
export * as search from './search';
export * as search from './search';
+export * as chat from './chat';
+export * as ai from './ai';
export * as jobs from './jobs';
export * as jobs from './jobs';
export * as tools from './tools';
export * as tools from './tools';
export * as projects from './projects';
export * as projects from './projects';
declare const sdk: typeof core & {
declare const sdk: typeof core & {
auth: typeof auth; registry: typeof registry; modules: typeof modules; realtime: typeof realtime;
auth: typeof auth; registry: typeof registry; modules: typeof modules; realtime: typeof realtime;
- media: typeof media; community: typeof community; search: typeof search; jobs: typeof jobs; tools: typeof tools; projects: typeof projects;
+ media: typeof media; community: typeof community; search: typeof search; chat: typeof chat; ai: typeof ai; jobs: typeof jobs; tools: typeof tools; projects: typeof projects;
};
};
export default sdk;
export default sdk;
new file mode 100644
new file mode 100644
@@ -0,0 +1,40 @@
@@ -0,0 +1,40 @@
+import type { OpenVibeClient } from './core';
+
+type Opts = { signal?: AbortSignal };
+type Json = Record<string, unknown>;
+export interface ChatClient {
+ global: {
+ history(query?: { limit?: number; after_id?: number }, opts?: Opts): Promise<Json>;
+ send(message: string, extra?: { reply_to_id?: number; auto_delete_minutes?: number }, opts?: Opts): Promise<Json>;
+ search(query?: { q?: string; user_id?: number; stream_id?: number; limit?: number; offset?: number }, opts?: Opts): Promise<{ messages: Json[]; total?: number }>;
+ };
+ rooms: {
+ list(opts?: Opts): Promise<Json>;
+ create(input: { name: string; slug?: string; topic?: string; visibility?: 'public' | 'private' }, opts?: Opts): Promise<Json>;
+ get(slug: string, opts?: Opts): Promise<Json | null>;
+ update(slug: string, patch: { name?: string; topic?: string; visibility?: 'public' | 'private'; slow_seconds?: number }, opts?: Opts): Promise<Json>;
+ messages(slug: string, query?: { before?: number; after?: number; limit?: number }, opts?: Opts): Promise<Json>;
+ send(slug: string, message: string, opts?: Opts): Promise<Json>;
+ deleteMessage(slug: string, id: number | string, opts?: Opts): Promise<Json>;
+ join(slug: string, opts?: Opts): Promise<Json>;
+ leave(slug: string, opts?: Opts): Promise<Json>;
+ read(slug: string, lastId?: number, opts?: Opts): Promise<Json>;
+ members(slug: string, opts?: Opts): Promise<Json>;
+ setMember(slug: string, username: string, role: 'member' | 'mod' | 'blocked' | 'none', opts?: Opts): Promise<Json>;
+ };
+ dms: {
+ list(opts?: Opts): Promise<Json>;
+ create(userIds: number[], extra?: { name?: string }, opts?: Opts): Promise<Json>;
+ get(id: number | string, opts?: Opts): Promise<Json | null>;
+ messages(id: number | string, query?: { limit?: number; before?: number }, opts?: Opts): Promise<Json>;
+ send(id: number | string, message: string, opts?: Opts): Promise<Json>;
+ deleteMessage(id: number | string, msgId: number | string, opts?: Opts): Promise<Json>;
+ read(id: number | string, opts?: Opts): Promise<Json>;
+ unread(opts?: Opts): Promise<{ unread: number }>;
+ blocks(opts?: Opts): Promise<Json>;
+ block(userId: number | string, opts?: Opts): Promise<Json>;
+ unblock(userId: number | string, opts?: Opts): Promise<Json>;
+ };
+ exportMine(opts?: Opts): Promise<{ username: string; exported_at: string; total: number; truncated: boolean; messages: Json[] }>;
+}
+export function createChatClient(client: OpenVibeClient, defaults?: { baseUrl?: string }): ChatClient;
@@ -8,6 +8,8 @@ export * as realtime from './realtime';
@@ -8,6 +8,8 @@ export * as realtime from './realtime';
export * as media from './media';
export * as media from './media';
export * as community from './community';
export * as community from './community';
export * as search from './search';
export * as search from './search';
+export * as chat from './chat';
+export * as ai from './ai';
export * as jobs from './jobs';
export * as jobs from './jobs';
export * as tools from './tools';
export * as tools from './tools';
export * as projects from './projects';
export * as projects from './projects';