openvibers/openvibe.sdk Commits
SDK 0.148.0: one train for thirteen waiting PRs, on Contracts 0.406.0 (#223)
Changes
@@@ -1,5 +1,28 @@# Changelog++## 0.148.0 — 2026-11-11++++- #208: add `openvibe-sdk/composer`, the shared composer's client: `attachments.upload(file, { onProgress, signal })` (multipart through `XMLHttpRequest`, real browser progress), `attachments.remove(id)`, `composer.prefs.get()/put(prefs, rev)` and `mentions.search(q, kinds)`, typed by `composer.attachment@1`, `composer.prefs@1` and `composer.mention@1`.++- #210: add `feed.list({ kind, cursor })` for the cross-site Community Feed, keep the existing Community reader callable, and type preview video media.++- #211: add the `openvibe-sdk/sso` one-time cross-site sign-in handoff: `POST /auth/handoff` mints a 60 s ticket and `sso.handoff()` arrival middleware redeems it before the page renders.++- #212: add `agents` SDK methods to fetch run snapshots, follow resumable AG-UI event streams and post approvals.++- #213: add `wallet.changes()` for the owner-scoped Vibes, OpenCoins and channel-points gain and loss window, and++ `wallet.markRead()` for each currency's own read cursor.++- #214: add browser-safe `player.current()`, `player.handoff(url)` and `player.on(listener)` for a live stream carried++ between OpenVibe sites by Shared's floating player.++- #215: add typed Chat readers for a person's compacted day, week and month memory, plus personal erase and opt-out.++- #216: add `exchange.createCatalogSearchClient().search(q, filters)` for public model and provider offers (prices,++ measured speed and Bench quality when available), and type the image search filters and source metadata.++- #217: add the owner-only agent sheet reader and Actor pause, resume and memory controls through Network's relay.++- #219: the app sign-in test checks a token's shape, not the letters "eyJ" (a macOS flake).++- #220: `homeFeed.network.popular({ limit })` (the signed-out front door's five things people do now) and++ `homeFeed.network.tryNext({ limit })` (the signed-in person's own suggestions).++- #222: type the exact reply target and its display name on a Community comment; a thread's first page includes two++ replies and the rest use the parent cursor.++- #207: add `codes.tasks` for the Dock's Code tab: `list({ limit })`, `create({ text, mode, repo?, attachments? })` and++ `stop(id)` (`codes.task-summary@1`).++- openvibe-contracts pinned at v0.406.0.++## 0.147.0 — 2026-11-10- #181: add `chat.activity` (the person's feed: mentions, replies, reactions, server invites, accepted friend requests and roles), `rooms.react()` and `reply_to_id` on room and DM sends.@@@ -8,12 +8,12 @@**Install and test:** `npm ci && npm test` (Node 20 or newer)```bash--npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.147.0++npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.148.0```> Supported browser and server clients for the OpenVibe platform.--**Status:** alpha, v0.147.0 (the five 1.0 core entries — `core`, `auth`, `events`, `db` and `media` — are frozen: their exported names change only with a major release; roadmap Wave 2; developer apps from Wave 20; Media objects v2 from Wave 4; the Tools platform API; Search, Chat, AI, Vision and consent clients; sign-in with recovery and a client health check). Every release is tagged (see [CHANGELOG.md](CHANGELOG.md)). Tested against local stub servers and the built-in mock platform only, never against the live platform. Services pin release tags; each repository's `STATUS.json` names the one it uses.++**Status:** alpha, v0.148.0 (the five 1.0 core entries — `core`, `auth`, `events`, `db` and `media` — are frozen: their exported names change only with a major release; roadmap Wave 2; developer apps from Wave 20; Media objects v2 from Wave 4; the Tools platform API; Search, Chat, AI, Vision and consent clients; sign-in with recovery and a client health check). Every release is tagged (see [CHANGELOG.md](CHANGELOG.md)). Tested against local stub servers and the built-in mock platform only, never against the live platform. Services pin release tags; each repository's `STATUS.json` names the one it uses.**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30.**License:** MIT ([LICENSE](LICENSE)). This package is a library that apps outside the network embed, so it uses MIT. The OpenVibe services themselves stay AGPL-3.0.@@@ -22,7 +22,7 @@ npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tagsAfter v0.97.1 is tagged:```bash--npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.147.0++npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.148.0```It has no runtime dependencies. It needs Node ≥ 20, or any browser with `fetch`, Web Crypto and `TextDecoder`. There is no build step. The package is CommonJS with ESM entry points (`import` works). Each subpath has its own `.d.ts`. For a page with no bundler, `browser/openvibe-sdk.mjs` is one self-contained ES module of the browser-safe subpaths (see [Browser without a bundler](#browser-without-a-bundler)). It does not depend on `openvibe-contracts` at runtime: it copies the contract types it uses (from the temporary Contracts sharing branch, a devDependency the tests check them against).@@@ -203,9 +203,11 @@ Until OpenVibe.Tools serves the gateway facade (Tools S6), point the jobs client| `openvibe-sdk/sso` | server | `createSsoClient({site, baseUrl, clientId, clientSecret, networkUrl, networkInternalUrl, secureCookies})`: a product site's sign-in with OpenVibe.Network: `router(express)` (`/login` with state + PKCE S256, `/callback`, `/fedcm`, `/logout`, `/me`, `/refresh`), `optionalAuth()`, `requireAuth()`, `verify(token)` (offline, shared JWKS), `extractToken(req)`; `sanitizeNext`, `COOKIES` || `openvibe-sdk/realtime` | both | `subscribe(topics, onEvent, {lastEventId, onGap, …})`, `createRealtimeClient(client)`, `parseSSE(body)` || `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/composer` | both | `createComposerClient(client, {mediaBaseUrl?, networkBaseUrl?})`: `attachments.upload(file, {onProgress, signal})` (multipart to Media through `XMLHttpRequest`, real browser progress), `attachments.remove(id)`, `composer.prefs.get()/put(prefs, rev)` (a stale revision is 409 `composer.rev_conflict`), `mentions.search(q, kinds)` || `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`, shared `comments.resolve/get/upload/create/edit/delete/visibility`, `as(subject)` || `openvibe-sdk/history` | both | Private history reads: cursor pages, title search, day/week summaries, recent agent reads, settings and owner-scoped deletes. || `openvibe-sdk/home-feed` | both | `createHomeFeedClient(client)` gives `ov.home.get()/set(layout)`, `ov.network.today()`, `ov.feed.list({ for: 'me', cursor })`, `ov.feed.post(text)`, and `ov.feed.contribute({ id, text, url })`. Personal calls need a user Bearer token; contribution needs an app token granted `community.pulse.contribute`. Today is public and cached by Network for five minutes. |++| `openvibe-sdk/feed` | both | `createFeedClient(client)` gives `ov.feed.list({ kind, cursor })` for the public cross-site Community Feed. Pass the returned `next_cursor` as `cursor` to read the next page. || `openvibe-sdk/bot` | both | `createBotClient(client, {actingSubject?})`: OpenVibe.Bot robots. `robots.list/get/create/update/delete/pairingCode`, `robots.operators.list/add/remove`, `robots.devices`, `robots.streaming.get/set`, `robots.audit`/`iterateAudit`, `robots.estop`/`clearEstop`, `robots.command` (`bot.command@1`), `embedUrl(robotId, mode)` (panel, overlay or compact), `devices.rotate/revoke`, public `profiles.list/get` and `kits.list/get`, `pair` (the agent's one-time code, never retried), `as(subject)`; a service acts for a person with `X-OV-Subject` (capabilities `bot.robot.read`/`manage`/`control`, `bot.device.connect`) || `openvibe-sdk/themes` | both | `createThemesClient(client)`: `catalog({mode, search, sort, limit})` (seasonal themes carry `season`), `get(idOrSlug)` (null when missing), `export(idOrSlug)`, `active()` (`{theme_id, custom_variables, display}`), `set(themeId, {custom})`, `setDisplay({motion, text, seasonal})`, `seasons({date})`, `submissions()`, `submit(theme)`, `import(file)`. In the browser: `followTheme(fn)` calls back now and on every theme, display or season change with `readTheme()` (`{id, mode, tokens, display: {motion, text, textScale, seasonal, calm}, season, seasonMark}`) and returns `stop()`, for a canvas or game to keep up; `applyTheme(vars, {root})`, `seasonOn(date, seasons)`, `onColor(hex)`, `DISPLAY_OPTIONS`, `TOKENS` || `openvibe-sdk/bench` | both | `createBenchClient(client, {actingSubject?})`: `kinds.list/get`, `tasks.create/get`, `rounds.create/get`, `entries.submit`, `judgements.next/cast`, `boards.get`, `ratings.get` (public, at least twenty games), `models.get`, `datasets.get/iterate`, `evals.create/get/run/results`, `mcpTools()`. Audience `openvibe.bench`; service id `bench`. |@@@ -217,6 +219,7 @@ Until OpenVibe.Tools serves the gateway facade (Tools S6), point the jobs client| `openvibe-sdk/chat` | both | `createChatClient(client)`: `global.send/history/search`, `rooms.*` (list, create, get, update, messages, send, deleteMessage, join, leave, read, members, setMember, react), `dms.*` (list, create, get, messages, send, deleteMessage, read, unread, blocks, block, unblock), `activity.*` (list, unread, readOne, read: what happened to the person in Chat), `embeds(message)` (the typed `chat.embed@1` address previews on a message), `exportMine()`, `vibe.*` (list, start, rename, remove: chats with Vibe), `inboxes.*` (list, create, get, update, remove, addMember, removeMember, conversations, messages, reply, setStatus, read, identity, replies, saveReply, deleteReply, webhooks, addWebhook, testWebhook, removeWebhook: a support inbox's team), `support.*` (list, messages, send: their own conversations with sites' chats), `feeds.*` (mine, create, update, rotateKey, remove, view, onLive). With no account: `createInboxVisitor(client, { inbox, token, user, account })` (visit, send, messages, handoff, rate: your own panel for an inbox, from any site) and `createFeedPublisher(client, { feed, key })` (hello, publish, claudeCode: vibe coding events with the feed's key) || `openvibe-sdk/ai` | both | `createAiClient(client)`: `runs.create(workflow, input, {wait, idempotencyKey, latency, deadlineHours, …})`, `runs.get/list/cancel/retry/citations/addCitations/waitFor`, `batches.create/get/list/cancel`, `creations()` (the public catalogue of what AI can make, no token sent), `chat/generate/summarize/classify/extract/enrich/embed`. A run may ask for the class it may wait in (`latency`: `interactive`, `background`, `batch`; `deadlineHours`: 1–72 for a batch run); `waitFor` gives a batch run back pending rather than throwing when the wait is over — the answer comes with the run's completion event or webhook. || `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/agents` | both | `createAgentsClient(client, {baseUrl?, service?, audience?})`: `run(id)` fetches an agent run snapshot (null when missing), `events(id, {lastEventId, signal})` follows the resumable AG-UI event stream, `post(id, approval)` answers an approval request || `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, prefer})` (+ `.wait()`), `jobs`; `ToolRunError`, `isToolRunError()`; `prefer` picks the worker placement (`auto`, `home`, `host`) ([details](docs/tools-routing.md)) || `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/resources` | server | `parseResourceName`/`resourceName`/`resourceNameOf` (OVRN, via the pinned `contracts.resources`) and `RESOURCE_KINDS` (only the chosen `med`/`wch` prefixes); `createResourceIndex({authorities, token, fetch, pageLimit, concurrency, timeoutMs, maxPages, maxMs})`: `list({project, kind, owner, limit})` -> `{resources, stale}` (`owner`: a subject id, each authority answering only what it owns), fanning out over the authorities' `GET /api/v1/resources`, following each opaque `next_cursor` and merging `common.resource-list-result@1` pages (an authority that fails — or whose cursor chain repeats, outruns `maxPages`, or outruns `maxMs` — is reported in `stale`, never fatal), and `iterate`; `createResourceClient({origin, token})`: `control(request)` POSTs a `common.resource-control-request@1` to `/api/v1/resources/control` — idempotency-keyed, transient failures retried with the same key and body, a thrown error carrying that `idempotencyKey` — and returns the `common.resource-control-result@1` (`done`/`pending`/`refused`/`failed`, a refused/failed one returned even with a non-retryable non-2xx) |@@@ -3,7 +3,7 @@"stage": "alpha","code": true,"wave": "W2",-- "version": "0.147.0",++ "version": "0.148.0","plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30","deployed": false,"deployment": "none: a package released as git tags; services and apps pin a tag",@@@ -50,5 +50,5 @@"vibes (openvibe-sdk/vibes): Vibes Pay charges (create with the app reference as the Idempotency-Key, get, refund) and constant-time webhook verification (X-OpenVibe-Signature-V2, ±300 s) for developer apps","streams (openvibe-sdk/streams): the Agent Streams plan S1 — start an agent stream on OpenVibe.Live (live.agent-stream@1) with its publish ticket, renew and revoke that ticket (the kill switch), poll state, post the last frame, set the curtain, switch visibility, end it and mint the view ticket; the publish ticket is a bearer of its own on the desk routes"],-- "updated": "2026-11-10"++ "updated": "2026-11-11"}@@@ -16,11 +16,13 @@ module.exports = {modules: require('./src/modules'),realtime: require('./src/realtime'),media: require('./src/media'),++ composer: require('./src/composer'),cam: require('./src/cam'),mediaUpload: require('./src/media-upload'),ui: require('./src/ui'),community: require('./src/community'),homeFeed: require('./src/home-feed'),++ feed: require('./src/feed'),bot: require('./src/bot'),search: require('./src/search'),bench: require('./src/bench'),@@@ -33,9 +35,12 @@ module.exports = {ai: require('./src/ai'),credentials: require('./src/credentials'),jobs: require('./src/jobs'),++ agents: require('./src/agents'),tools: require('./src/tools'),projects: require('./src/projects'),geo: require('./src/geo'),history: require('./src/history'),userCard: require('./src/user-card'),++ player: require('./src/player'),++ agentSheet: require('./src/agent-sheet'),};@@@ -1,6 +1,6 @@--// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.147.0, MIT). Do not edit.++// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.148.0, MIT). Do not edit.// One self-contained ES module: core at the top level plus the browser-safe subpaths as namespaces--// (auth, locker, registry, trust, levels, modules, realtime, media, cam, mediaUpload, ui, community, homeFeed, bot, search, bench, meme, vision, themes, design, redact, chat, ai, credentials, jobs, tools, projects, geo, history, userCard). No server-only code: no service tokens, JWT verification, events, identity.++// (auth, locker, registry, trust, levels, modules, realtime, media, composer, cam, mediaUpload, ui, community, homeFeed, feed, bot, search, bench, meme, vision, themes, design, redact, chat, ai, credentials, jobs, agents, tools, projects, geo, history, userCard, player, agentSheet). No server-only code: no service tokens, JWT verification, events, identity./* eslint-disable */const __defs = [// ── 0: browser.js ──────────────────────────────────────────────────@@@ -23,28 +23,33 @@ module.exports = {modules: __load(24),realtime: __load(25),media: __load(26),-- cam: __load(28),-- mediaUpload: __load(29),-- ui: __load(30),-- community: __load(31),-- homeFeed: __load(32),-- bot: __load(33),-- search: __load(34),-- bench: __load(35),-- meme: __load(36),-- vision: __load(37),-- themes: __load(38),-- design: __load(39),-- redact: __load(40),-- chat: __load(41),-- ai: __load(43),-- credentials: __load(45),-- jobs: __load(46),-- tools: __load(48),-- projects: __load(49),-- geo: __load(50),-- history: __load(51),-- userCard: __load(52),++ composer: __load(28),++ cam: __load(29),++ mediaUpload: __load(30),++ ui: __load(31),++ community: __load(32),++ homeFeed: __load(33),++ feed: __load(34),++ bot: __load(35),++ search: __load(36),++ bench: __load(37),++ meme: __load(38),++ vision: __load(39),++ themes: __load(40),++ design: __load(41),++ redact: __load(42),++ chat: __load(43),++ ai: __load(45),++ credentials: __load(47),++ jobs: __load(48),++ agents: __load(50),++ tools: __load(51),++ projects: __load(52),++ geo: __load(53),++ history: __load(54),++ userCard: __load(55),++ player: __load(56),++ agentSheet: __load(57),};},// ── 1: src/core/index.js ───────────────────────────────────────────@@@ -59,7 +64,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.147.0';++const SDK_VERSION = '0.148.0';module.exports = {createClient,@@@ -711,10 +716,11 @@ module.exports = { paginate, offsetPager };function (module, exports) {'use strict';--/** Personal bearer wallet/store facade. Amounts and ownership are enforced by Billing and Inventory. */--function createWalletClient(client, { billingUrl, inventoryUrl } = {}) {++/** Personal bearer wallet/store facade. Amounts and ownership are enforced by their ledger services. */++function createWalletClient(client, { billingUrl, inventoryUrl, networkUrl } = {}) {if (!client || typeof client.json !== 'function') throw new TypeError('createWalletClient requires an SDK core client');-- const call = (service, path, opts = {}) => client.json({ service, baseUrl: service === 'billing' ? billingUrl : inventoryUrl,++ const urls = { billing: billingUrl, inventory: inventoryUrl, network: networkUrl };++ const call = (service, path, opts = {}) => client.json({ service, baseUrl: urls[service],audience: `openvibe.${service}`, path, credentials: 'omit', ...opts });const key = (v) => {if (typeof v !== 'string' || !/^[A-Za-z0-9._:-]{8,128}$/.test(v)) throw new TypeError('An idempotency_key of 8–128 letters, digits, dot, colon, underscore or dash is required');@@@ -722,12 +728,38 @@ function createWalletClient(client, { billingUrl, inventoryUrl } = {}) {};const whole = (v) => { if (!Number.isSafeInteger(v) || v < 1) throw new TypeError('Choose a positive whole amount'); return v; };const itemId = (v) => { if (!/^itd_[0-9A-HJKMNP-TV-Z]{26}$/.test(v || '')) throw new TypeError('An item id is an itd_ id'); return encodeURIComponent(v); };++ const cursor = (v, currency) => {++ if (v == null) return v;++ if (currency === 'vibes') {++ if (typeof v !== 'string' || !/^txn_[0-9A-HJKMNP-TV-Z]{26}$/.test(v)) throw new TypeError('vibes cursor is a txn_ id');++ } else if (!Number.isSafeInteger(Number(v)) || Number(v) <= 0 || !/^[0-9]+$/.test(String(v))) {++ throw new TypeError('cursor is a positive move id');++ }++ return v;++ };++ const streamer = (v) => { if (!Number.isSafeInteger(v) || v <= 0) throw new TypeError('streamer is a positive user id'); return v; };// The BODY key is durable in both services. Retries keep it; the SDK must not add an unrelated header key.const write = (service, path, json, { signal } = {}) => call(service, path, { method: 'POST', json,idempotencyKey: false, idempotent: true, signal });return {wallet: {get: ({ signal } = {}) => call('billing', '/api/v1/me/wallet', { signal }),++ changes: ({ since_vibes, since_opencoins, since_channel_points, streamer: channel, limit, signal } = {}) => {++ if (limit != null && (!Number.isSafeInteger(limit) || limit < 1 || limit > 500)) throw new TypeError('limit is 1 to 500');++ return call('network', '/api/wallet/changes', { query: {++ since_vibes: cursor(since_vibes, 'vibes'), since_opencoins: cursor(since_opencoins, 'opencoins'),++ since_channel_points: cursor(since_channel_points, 'channel-points'),++ streamer: channel == null ? undefined : streamer(channel), limit,++ }, signal });++ },++ markRead: ({ currency, cursor: at, streamer: channel }, { signal } = {}) => {++ if (!['vibes', 'opencoins', 'channel-points'].includes(currency)) throw new TypeError('currency is vibes, opencoins or channel-points');++ if (at == null) throw new TypeError('cursor is required');++ if (currency === 'channel-points' && channel == null) throw new TypeError('streamer is required for channel points');++ return call('network', '/api/wallet/changes/read', { method: 'PUT', json: {++ currency, cursor: cursor(at, currency), ...(currency === 'channel-points' ? { streamer: streamer(channel) } : {}),++ }, signal });++ },topUp: ({ provider, amount_cents, idempotency_key }, options) => write('billing', '/api/v1/me/wallet/topups',{ provider, amount_cents: whole(amount_cents), idempotency_key: key(idempotency_key) }, options),},@@@ -3306,7 +3338,150 @@ function createObjectsClient({module.exports = { createObjectsClient };},--// ── 28: src/cam/index.js ────────────────────────────────────────────++// ── 28: src/composer.js ─────────────────────────────────────────────++function (module, exports) {++'use strict';++/**++ * openvibe-sdk/composer: the shared composer's client, the same on every OpenVibe site.++ *++ * attachments.upload(file, { onProgress, signal, filename, contentType, xhr })++ * POST /api/v1/attachments on Media (multipart `file`), through XMLHttpRequest so upload progress is real.++ * -> composer.attachment@1++ * attachments.remove(id)++ * DELETE /api/v1/attachments/<id>; true when it went, false when it was already gone.++ * composer.prefs.get() / composer.prefs.put(prefs, rev)++ * GET/PUT /api/v1/me/composer on Network (composer.prefs@1). A PUT sends the revision it read; a stale one is++ * 409 composer.rev_conflict and err.problem.current is the other tab's document.++ * mentions.search(q, kinds)++ * GET /api/v1/mentions on Network (composer.mention@1): people the caller follows first, then public people,++ * repositories from OpenVibe Git, first-party sites. Signed-in only; at most 8.++ *++ * Browser-safe. The composer page calls Network directly for prefs and mentions (every site's connect-src allows it)++ * and Media for the bytes (the site's CSP must allow OPENVIBE_MEDIA_ORIGIN).++ */++const { OpenVibeError, isOpenVibeError } = __load(3);++++const MEDIA_ORIGIN = 'https://openvibe.media';++const NETWORK_ORIGIN = 'https://openvibe.network';++const ATTACHMENT_MAX = 300 * 1024 * 1024;++const enc = encodeURIComponent;++++function toBlob(file, contentType) {++ if (typeof Blob !== 'undefined' && file instanceof Blob) return file;++ if (typeof file === 'string' || file instanceof ArrayBuffer || ArrayBuffer.isView(file)) {++ return new Blob([file], contentType ? { type: contentType } : undefined);++ }++ throw new TypeError('attachments.upload: pass a File/Blob, ArrayBuffer, typed array (Buffer) or string');++}++++function abortError() {++ return new OpenVibeError({ code: 'sdk.aborted', message: 'the upload was aborted' });++}++++function createComposerClient(client, { mediaBaseUrl, networkBaseUrl } = {}) {++ if (!client || typeof client.json !== 'function') throw new TypeError('createComposerClient: an SDK client is required');++ const net = (opts) => client.json({ service: 'network', audience: 'openvibe.network', ...(networkBaseUrl ? { baseUrl: networkBaseUrl } : {}), ...opts });++++ /** The Media token for an XHR upload (the XHR cannot borrow the fetch client's request loop). */++ async function mediaToken() {++ const o = client.options || {};++ if (o.token != null) return o.token;++ if (typeof o.getToken === 'function') return (await o.getToken({ service: 'media', audience: 'openvibe.media' })) || null;++ return null;++ }++++ const attachments = {++ /**++ * One file -> composer.attachment@1. Progress is the browser's upload progress (XHR), not a guess.++ * `xhr` injects an XMLHttpRequest implementation (tests, a non-browser shell); otherwise the global one.++ */++ upload(file, { onProgress, signal, filename, contentType, xhr } = {}) {++ return new Promise((resolve, reject) => {++ const XHR = xhr || (typeof XMLHttpRequest !== 'undefined' ? XMLHttpRequest : null);++ if (!XHR) return reject(new OpenVibeError({ code: 'sdk.xhr_unavailable', message: 'attachments.upload needs XMLHttpRequest (pass { xhr })' }));++ let blob;++ try { blob = toBlob(file, contentType); } catch (err) { return reject(err); }++ if (blob.size > ATTACHMENT_MAX) {++ return reject(new OpenVibeError({ code: 'media.attachment.too_large', status: 413, message: `a file may be at most ${ATTACHMENT_MAX / (1024 * 1024)} MB` }));++ }++ const url = client.url({ service: 'media', ...(mediaBaseUrl ? { baseUrl: mediaBaseUrl } : {}), path: '/api/v1/attachments' });++ Promise.resolve(url).then(async (target) => {++ if (signal && signal.aborted) return reject(abortError());++ const fd = new FormData();++ fd.append('file', blob, filename || (file && file.name) || 'file');++ const req = new XHR();++ let settled = false;++ const finish = (fn, v) => { if (settled) return; settled = true; cleanup(); fn(v); };++ const offAbort = signal && typeof signal.addEventListener === 'function' ? () => signal.removeEventListener('abort', onAbort) : () => {};++ function onAbort() { try { req.abort(); } catch { /* */ } finish(reject, abortError()); }++ const cleanup = () => { offAbort(); };++ req.open('POST', target);++ req.setRequestHeader('Accept', 'application/json');++ const token = await mediaToken();++ if (token) req.setRequestHeader('Authorization', `Bearer ${token}`);++ if (req.upload && typeof onProgress === 'function') {++ req.upload.onprogress = (e) => onProgress({ loaded: e.loaded || 0, total: e.total || blob.size, lengthComputable: e.lengthComputable !== false });++ }++ req.onload = () => {++ let body = null;++ try { body = req.responseText ? JSON.parse(req.responseText) : null; } catch { body = req.responseText || null; }++ if (req.status >= 200 && req.status < 300) return finish(resolve, body);++ const err = OpenVibeError.fromResponse({ status: req.status, body, method: 'POST', url: target });++ if (body && body.current) err.current = body.current;++ finish(reject, err);++ };++ req.onerror = () => finish(reject, new OpenVibeError({ code: 'sdk.network_error', message: 'the upload could not reach Media', method: 'POST', url: target }));++ req.ontimeout = () => finish(reject, new OpenVibeError({ code: 'sdk.timeout', message: 'the upload timed out', method: 'POST', url: target }));++ if (signal && typeof signal.addEventListener === 'function') signal.addEventListener('abort', onAbort, { once: true });++ try { req.send(fd); } catch (err) { finish(reject, err); }++ }).catch(reject);++ });++ },++++ /** DELETE /api/v1/attachments/<id>: true when it went, false when it was already gone. */++ async remove(id) {++ if (!id) throw new TypeError('attachments.remove: pass the attachment id');++ try {++ await client.json({ service: 'media', ...(mediaBaseUrl ? { baseUrl: mediaBaseUrl } : {}), audience: 'openvibe.media', method: 'DELETE', path: `/api/v1/attachments/${enc(id)}` });++ return true;++ } catch (err) {++ if (isOpenVibeError(err) && err.status === 404) return false;++ throw err;++ }++ },++ };++++ const composer = {++ prefs: {++ /** composer.prefs@1, with its rev. */++ get: () => net({ path: '/api/v1/me/composer' }),++ /**++ * composer.prefs@1 in (rev is the one you read; 0 for a new person), the new document out.++ * A stale rev is 409 composer.rev_conflict with err.problem.current (and err.current).++ */++ put(prefs, rev) {++ if (!prefs || typeof prefs !== 'object') throw new TypeError('composer.prefs.put: pass the prefs object');++ if (!Number.isInteger(rev) || rev < 0) throw new TypeError('composer.prefs.put: pass the revision you read (0 for new)');++ return net({ method: 'PUT', path: '/api/v1/me/composer', json: { ...prefs, rev } });++ },++ },++ };++++ const mentions = {++ /** @ menu entries (composer.mention@1[]). kinds: person | repo | site | file; omitted = all. */++ async search(q, kinds) {++ const list = Array.isArray(kinds) ? kinds : (kinds ? String(kinds).split(',') : []);++ const data = await net({ path: '/api/v1/mentions', query: { q: q || undefined, kinds: list.length ? list.join(',') : undefined } });++ return (data && Array.isArray(data.mentions)) ? data.mentions : [];++ },++ };++++ return { attachments, composer, mentions, MEDIA_ORIGIN, NETWORK_ORIGIN };++}++++module.exports = { createComposerClient, MEDIA_ORIGIN, NETWORK_ORIGIN, ATTACHMENT_MAX };++},++// ── 29: src/cam/index.js ────────────────────────────────────────────function (module, exports) {'use strict';/**@@@ -3416,7 +3591,7 @@ function defineOvCamera(options = {}) {module.exports = { defineOvCamera, OV_CAMERA_SRC, CAM_KIT_SRC, CAM_KIT_SRI, loadCamKit, open, settings };},--// ── 29: src/media-upload.js ─────────────────────────────────────────++// ── 30: src/media-upload.js ─────────────────────────────────────────function (module, exports) {'use strict';const { createObjectsClient } = __load(26);@@@ -3436,7 +3611,7 @@ function createUploadClient(config) {module.exports = { createUploadClient };},--// ── 30: src/ui.js ───────────────────────────────────────────────────++// ── 31: src/ui.js ───────────────────────────────────────────────────function (module, exports) {'use strict';@@@ -3449,7 +3624,7 @@ function progress(el, options) {module.exports = { progress };},--// ── 31: src/community.js ────────────────────────────────────────────++// ── 32: src/community.js ────────────────────────────────────────────function (module, exports) {'use strict';/**@@@ -3566,6 +3741,8 @@ function createCommunityClient(client, defaults = {}) {query: { kind: query.kind, site: query.site, q: query.q, cursor: query.cursor, limit: query.limit,following: query.following === true || query.following === 1 ? '1' : undefined },}, o);++ // Named form used by new integrations; the callable form remains for existing SDK consumers.++ feed.list = feed;const posts = {/*** A status post in the Feed at once: { text, pictures?: [{ media_id, alt }] } (at most four, alt required,@@@ -3610,15 +3787,16 @@ function createCommunityClient(client, defaults = {}) {module.exports = { createCommunityClient, actingHeaders };},--// ── 32: src/home-feed.js ────────────────────────────────────────────++// ── 33: src/home-feed.js ────────────────────────────────────────────function (module, exports) {'use strict';--/** The menu's portable Home layout, network day counts and personal wall. Pass a user-token core client;-- * contribute() needs an app token with the person's community.pulse.contribute grant. */++/** The menu's portable Home layout, network day counts, what people do now and personal wall. Pass a user-token core++ * client; contribute() needs an app token with the person's community.pulse.contribute grant. */function createHomeFeedClient(client, { networkBaseUrl, communityBaseUrl } = {}) {if (!client || typeof client.json !== 'function') throw new TypeError('createHomeFeedClient: pass an openvibe-sdk/core client');const network = (opts) => client.json({ service: 'network', baseUrl: networkBaseUrl, audience: 'openvibe.network', ...opts });const community = (opts) => client.json({ service: 'community', baseUrl: communityBaseUrl, audience: 'openvibe.community', ...opts });++ const limitQuery = (limit) => (limit == null ? undefined : { limit });return {home: {get: ({ signal } = {}) => network({ path: '/api/v1/me/home-screen', signal }),@@@ -3626,6 +3804,10 @@ function createHomeFeedClient(client, { networkBaseUrl, communityBaseUrl } = {})},network: {today: ({ signal } = {}) => network({ path: '/api/v1/frame/today', auth: false, signal }),++ // The signed-out front door's list: Report's public aggregate, no credentials sent.++ popular: ({ limit, signal } = {}) => network({ path: '/api/v1/home/popular', query: limitQuery(limit), auth: false, signal }),++ // The signed-in person's own suggestions; Network computes them in the caller's request.++ tryNext: ({ limit, signal } = {}) => network({ path: '/api/v1/me/try-next', query: limitQuery(limit), signal }),},feed: {list: ({ for: audience = 'me', cursor, signal } = {}) => community({ path: '/api/v1/pulse', query: { for: audience, cursor }, signal }),@@@ -3637,7 +3819,25 @@ function createHomeFeedClient(client, { networkBaseUrl, communityBaseUrl } = {})module.exports = { createHomeFeedClient };},--// ── 33: src/bot.js ──────────────────────────────────────────────────++// ── 34: src/feed.js ─────────────────────────────────────────────────++function (module, exports) {++'use strict';++++/** Browser-safe reader for the public, cross-site Feed cards on OpenVibe.Community. */++function createFeedClient(client, { communityBaseUrl } = {}) {++ if (!client || typeof client.json !== 'function') throw new TypeError('createFeedClient: pass an openvibe-sdk/core client');++ return {++ /** Read one page of cards; pass next_cursor back as cursor to continue. */++ list: ({ kind, cursor } = {}, { signal } = {}) => client.json({++ service: 'community', baseUrl: communityBaseUrl, audience: 'openvibe.community',++ path: '/api/v1/feed', query: { kind, cursor }, signal,++ }),++ };++}++++module.exports = { createFeedClient };++},++// ── 35: src/bot.js ──────────────────────────────────────────────────function (module, exports) {'use strict';/**@@@ -3878,7 +4078,7 @@ function createBotClient(client, defaults = {}) {module.exports = { createBotClient, actingHeaders };},--// ── 34: src/search.js ───────────────────────────────────────────────++// ── 36: src/search.js ───────────────────────────────────────────────function (module, exports) {'use strict';/**@@@ -4024,7 +4224,7 @@ function createSearchClient(client, defaults = {}) {module.exports = { createSearchClient, queryOf, fabricOf, codeOf };},--// ── 35: src/bench.js ────────────────────────────────────────────────++// ── 37: src/bench.js ────────────────────────────────────────────────function (module, exports) {'use strict';/** openvibe-sdk/bench: OpenVibe.Bench's JSON API and streamed open dataset. Browser-safe. */@@@ -4132,7 +4332,7 @@ function createBenchClient(client, defaults = {}) {module.exports = { createBenchClient };},--// ── 36: src/meme.js ─────────────────────────────────────────────────++// ── 38: src/meme.js ─────────────────────────────────────────────────function (module, exports) {'use strict';/**@@@ -4215,7 +4415,7 @@ function createMemeClient(client, defaults = {}) {module.exports = { createMemeClient };},--// ── 37: src/vision.js ───────────────────────────────────────────────++// ── 39: src/vision.js ───────────────────────────────────────────────function (module, exports) {'use strict';/**@@@ -4373,7 +4573,7 @@ function pick(o, keys) {module.exports = { createVisionClient, DEFAULT_AUDIENCE };},--// ── 38: src/themes.js ───────────────────────────────────────────────++// ── 40: src/themes.js ───────────────────────────────────────────────function (module, exports) {'use strict';/**@@@ -4523,7 +4723,7 @@ function followTheme(fn, opts = {}) {module.exports = { createThemesClient, followTheme, readTheme, applyTheme, seasonOn, onColor, DISPLAY_OPTIONS, TOKENS };},--// ── 39: src/design.js ───────────────────────────────────────────────++// ── 41: src/design.js ───────────────────────────────────────────────function (module, exports) {'use strict';@@@ -4590,7 +4790,7 @@ function createDesignClient({ accessToken = null, baseUrl = DEFAULT_DESIGN_URL,module.exports = { createDesignClient, DEFAULT_DESIGN_URL };},--// ── 40: src/redact.js ───────────────────────────────────────────────++// ── 42: src/redact.js ───────────────────────────────────────────────function (module, exports) {'use strict';/**@@@ -4837,7 +5037,7 @@ function onlySecret(text) {module.exports = { redact, onlySecret };},--// ── 41: src/chat.js ─────────────────────────────────────────────────++// ── 43: src/chat.js ─────────────────────────────────────────────────function (module, exports) {'use strict';/**@@@ -4870,7 +5070,7 @@ function (module, exports) {const { isOpenVibeError } = __load(3);const enc = encodeURIComponent;--const history = __load(42);++const history = __load(44);/** The kinds chat.embed@1 names; anything else is not a preview this SDK returns. */const EMBED_KINDS = new Set(['link', 'video', 'image', 'live', 'picture', 'design', 'post', 'profile', 'game']);@@@ -4913,6 +5113,14 @@ function createChatClient(client, defaults = {}) {history,/** The typed address previews on a message (chat.embed@1): [] when it has none. */embeds: (message) => readEmbeds(message),++ /** Compacted day, week or month memory for a person (chat.person-memory-result@1). */++ people: {++ memory: (username, { channel, level = 'day' } = {}, o = {}) => call({ path: `/api/v1/people/${enc(String(username))}/memory`, query: { channel, level } }, o),++ /** Erase the signed-in person's memory and opt out. */++ forget: (username, o = {}) => call({ method: 'DELETE', path: `/api/v1/people/${enc(String(username))}/memory`, ...once }, o),++ /** Set the signed-in person's opt-out preference. */++ preferences: (username, optedOut, o = {}) => call({ method: 'PUT', path: `/api/v1/people/${enc(String(username))}/memory/preferences`, json: { opted_out: !!optedOut }, ...once }, o),++ },/** Vibe's model comparison API, authenticated as the person. */ask: (input, o = {}) => call({ method: 'POST', path: '/api/vibe/ask', json: input, ...once }, o),pick: (messageId, model, o = {}) => call({ method: 'POST', path: '/api/vibe/pick', json: { message_id: messageId, model }, ...once }, o),@@@ -5151,7 +5359,7 @@ function createFeedPublisher(client, { feed, key, baseUrl } = {}) {module.exports = { createChatClient, createInboxVisitor, createFeedPublisher, history, readEmbeds };},--// ── 42: src/chat-history.js ─────────────────────────────────────────++// ── 44: src/chat-history.js ─────────────────────────────────────────function (module, exports) {'use strict';@@@ -5230,7 +5438,7 @@ function attach({ feed, status = null, fetchPage, render, hasMore = true, threshmodule.exports = { pages, attach };},--// ── 43: src/ai.js ───────────────────────────────────────────────────++// ── 45: src/ai.js ───────────────────────────────────────────────────function (module, exports) {'use strict';/**@@@ -5408,7 +5616,7 @@ function createAiClient(client, defaults = {}) {remove(id) { requireFiles('remove'); return fileObjects.delete(id); },};-- const metrics = __load(44).createMetricsClient(call);++ const metrics = __load(46).createMetricsClient(call);const api = { tasks, creations, choices, run, admin, runs, batches, files, metrics, TERMINAL, CLASSES };// Direct operations: a run of workflow ai.<op> with the input as the body, waited for by AI.for (const op of OPS) {@@@ -5421,7 +5629,7 @@ function createAiClient(client, defaults = {}) {module.exports = { createAiClient, TERMINAL, OPS, CLASSES };},--// ── 44: src/ai-metrics.js ───────────────────────────────────────────++// ── 46: src/ai-metrics.js ───────────────────────────────────────────function (module, exports) {'use strict';// Public measured reads: no bearer token, prompt, person or site identifiers.@@@ -5450,7 +5658,7 @@ function createMetricsClient(call) {}module.exports = { createMetricsClient };},--// ── 45: src/credentials.js ──────────────────────────────────────────++// ── 47: src/credentials.js ──────────────────────────────────────────function (module, exports) {'use strict';@@@ -5468,7 +5676,7 @@ function createCredentialsClient(client, defaults = {}) {module.exports = { createCredentialsClient };},--// ── 46: src/jobs.js ─────────────────────────────────────────────────++// ── 48: src/jobs.js ─────────────────────────────────────────────────function (module, exports) {'use strict';/**@@@ -5506,7 +5714,7 @@ function (module, exports) {*/const { OpenVibeError, isOpenVibeError } = __load(3);const { newIdempotencyKey } = __load(5);--const { appendFiles } = __load(47);++const { appendFiles } = __load(49);const { parseSSE } = __load(25);const TERMINAL = new Set(['succeeded', 'failed', 'cancelled']);@@@ -5670,7 +5878,7 @@ function createJobsClient(client, { baseUrl, service = 'tools', audience = 'openmodule.exports = { createJobsClient, isTerminal, TERMINAL_STATES: [...TERMINAL] };},--// ── 47: src/core/form.js ────────────────────────────────────────────++// ── 49: src/core/form.js ────────────────────────────────────────────function (module, exports) {'use strict';/**@@@ -5704,7 +5912,62 @@ function appendFiles(form, list, where) {module.exports = { isBlob, isUpload, appendFiles };},--// ── 48: src/tools.js ────────────────────────────────────────────────++// ── 50: src/agents.js ───────────────────────────────────────────────++function (module, exports) {++'use strict';++/** Agent run snapshots, AG-UI event streams and approval responses. */++const { OpenVibeError, isOpenVibeError } = __load(3);++const { parseSSE } = __load(25);++const enc = encodeURIComponent;++const pause = (ms, signal) => new Promise((resolve) => {++ const t = setTimeout(resolve, ms);++ if (signal) signal.addEventListener('abort', () => { clearTimeout(t); resolve(); }, { once: true });++});++function createAgentsClient(client, { baseUrl, service = 'network', audience = 'openvibe.network' } = {}) {++ const call = (opts) => client.request({ service, baseUrl, audience, ...opts });++ async function run(id, { signal } = {}) {++ if (!id) throw new TypeError('agents.run: id is required');++ try { return (await call({ path: `/api/v1/runs/${enc(id)}`, signal })).data; }++ catch (err) { if (isOpenVibeError(err) && err.status === 404) return null; throw err; }++ }++ async function post(id, approval, { signal } = {}) {++ if (!id) throw new TypeError('agents.post: id is required');++ if (!approval || typeof approval !== 'object' || Array.isArray(approval)) throw new TypeError('agents.post: approval must be an object');++ return (await call({ method: 'POST', path: `/api/v1/runs/${enc(id)}/approvals`, json: approval, signal })).data;++ }++ async function* events(id, { lastEventId = 0, signal, maxReconnects = 20, reconnectDelayMs = 1000 } = {}) {++ if (!id) throw new TypeError('agents.events: id is required');++ let last = String(lastEventId || '0'), failures = 0, serverRetryMs = 0;++ while (!(signal && signal.aborted)) {++ let delivered = false;++ try {++ const res = await call({ path: `/api/v1/runs/${enc(id)}/events`, responseType: 'response', retries: 0, signal,++ headers: { Accept: 'text/event-stream', 'Cache-Control': 'no-cache', 'Last-Event-ID': last } });++ if (res.status === 204) return;++ for await (const msg of parseSSE(res.data.body, { onRetry: (ms) => { serverRetryMs = ms; } })) {++ if (msg.id !== undefined && msg.id !== '') { if (/^\d+$/.test(msg.id) && Number(msg.id) <= Number(last)) continue; last = msg.id; }++ let data; try { data = JSON.parse(msg.data); } catch { data = null; }++ delivered = true; failures = 0;++ yield { id: msg.id ?? null, event: msg.event || (data && data.type) || null, data };++ if (data && ['RUN_FINISHED', 'RUN_ERROR'].includes(data.type)) return;++ }++ } catch (err) {++ if (signal && signal.aborted) return;++ if (isOpenVibeError(err) && err.status >= 400 && err.status < 500) throw err;++ if (!isOpenVibeError(err) && !['TypeError', 'AbortError'].includes(err && err.name)) throw err;++ }++ failures = delivered ? 0 : failures + 1;++ if (failures > maxReconnects) throw new OpenVibeError({ code: 'sdk.network_error', retryable: true, message: `agents: event stream ${id} failed ${failures} times in a row` });++ const backoffMs = Math.min(reconnectDelayMs * (2 ** Math.max(0, failures - 1)), 30000);++ const waitMs = Math.min(Math.max(backoffMs, serverRetryMs), 30000);++ await pause(waitMs, signal);++ }++ }++ return { run, events, post };++}++module.exports = { createAgentsClient };++},++// ── 51: src/tools.js ────────────────────────────────────────────────function (module, exports) {'use strict';/**@@@ -5738,8 +6001,8 @@ function (module, exports) {*/const { OpenVibeError, isOpenVibeError } = __load(3);const { newIdempotencyKey } = __load(5);--const { isUpload, appendFiles } = __load(47);--const { createJobsClient } = __load(46);++const { isUpload, appendFiles } = __load(49);++const { createJobsClient } = __load(48);const enc = encodeURIComponent;const ID_RE = /^[a-z][a-z0-9-]{0,39}$/;@@@ -6001,7 +6264,7 @@ function createToolsClient(client, { baseUrl, service = 'tools', audience = 'opemodule.exports = { createToolsClient, ToolRunError, isToolRunError };},--// ── 49: src/projects.js ─────────────────────────────────────────────++// ── 52: src/projects.js ─────────────────────────────────────────────function (module, exports) {'use strict';/**@@@ -6117,7 +6380,7 @@ function createProjectsClient(client, { baseUrl } = {}) {module.exports = { createProjectsClient };},--// ── 50: src/geo.js ──────────────────────────────────────────────────++// ── 53: src/geo.js ──────────────────────────────────────────────────function (module, exports) {'use strict';/**@@@ -6181,7 +6444,7 @@ function createGeoClient(client, { baseUrl, fetch: fetchImpl = null, samples = 3module.exports = { createGeoClient };},--// ── 51: src/history.js ──────────────────────────────────────────────++// ── 54: src/history.js ──────────────────────────────────────────────function (module, exports) {'use strict';@@@ -6209,7 +6472,7 @@ function createHistoryClient(client, { baseUrl } = {}) {module.exports = { createHistoryClient };},--// ── 52: src/user-card.js ────────────────────────────────────────────++// ── 55: src/user-card.js ────────────────────────────────────────────function (module, exports) {'use strict';/** One public Network aggregate. Hidden/unavailable fields stay absent; no credentials are needed. */@@@ -6225,6 +6488,53 @@ function createUserCardClient(client, { baseUrl } = {}) {}module.exports = { createUserCardClient };},++// ── 56: src/player.js ───────────────────────────────────────────────++function (module, exports) {++'use strict';++/** Browser facade for Shared's portable live player. */++function host() { return typeof window === 'undefined' ? null : window.OVOverlay || null; }++function current() { return host()?.current() || null; }++function handoff(url) { return host()?.handoffUrl(url) || url; }++function on(listener) {++ if (typeof listener !== 'function') throw new TypeError('listener must be a function');++ if (typeof window === 'undefined') return () => {};++ const receive = (event) => listener(event.detail);++ window.addEventListener('openvibe:player', receive);++ return () => window.removeEventListener('openvibe:player', receive);++}++module.exports = { current, handoff, on };++},++// ── 57: src/agent-sheet.js ──────────────────────────────────────────++function (module, exports) {++'use strict';++/** Owner-only agent sheets through Network's fixed cross-site relay. The caller supplies their own Bearer. */++const ACTOR = /^agt_[0-9A-HJKMNP-TV-Z]{26}$/;++const MEMORY = /^mem_[0-9A-HJKMNP-TV-Z]{26}$/;++const CODES = /^[a-z][a-z0-9-]{0,63}$/;++++function createAgentSheetClient(client, { baseUrl } = {}) {++ if (!client || typeof client.json !== 'function') throw new TypeError('createAgentSheetClient: pass an SDK core client');++ const id = (value, pattern, kind) => {++ if (!pattern.test(String(value || ''))) throw new TypeError(`invalid ${kind} id`);++ return String(value);++ };++ const path = (agent) => `/api/frame/agents/${id(agent, ACTOR, 'Actor agent')}`;++ const call = (opts) => client.json({ service: 'network', audience: 'openvibe.network', baseUrl, ...opts });++ return {++ sheet: (agent, { signal } = {}) => call({ path: `${path(agent)}/sheet`, signal }),++ codesSheet: (agent, { signal } = {}) => call({ path: `/api/frame/codes-agents/${id(agent, CODES, 'coding agent')}/sheet`, signal }),++ pause: (agent, { signal } = {}) => call({ method: 'PATCH', path: path(agent), json: { status: 'paused' }, signal, idempotent: true }),++ resume: (agent, { signal } = {}) => call({ method: 'PATCH', path: path(agent), json: { status: 'active' }, signal, idempotent: true }),++ memory: {++ add: (agent, text, { signal } = {}) => call({ method: 'POST', path: `${path(agent)}/memory`, json: { text }, signal, idempotent: false }),++ update: (agent, note, text, { signal } = {}) => call({ method: 'PATCH', path: `${path(agent)}/memory/${id(note, MEMORY, 'memory')}`, json: { text }, signal, idempotent: true }),++ remove: (agent, note, { signal } = {}) => call({ method: 'DELETE', path: `${path(agent)}/memory/${id(note, MEMORY, 'memory')}`, signal, idempotent: true }),++ },++ };++}++++module.exports = { createAgentSheetClient };++},];const __cache = new Array(__defs.length);function __load(i) {@@@ -6241,9 +6551,10 @@ export const {parseTraceparent, startSpan, contextFromHeaders, ulid, newEventId, newIdempotencyKey,isActingSubjectId, satisfiesRange, compareVersions, CONTRACTS_RANGE, DEFAULT_NETWORK, SDK_VERSION,auth, locker, registry, trust, levels, modules,-- realtime, media, cam, mediaUpload, ui, community,-- homeFeed, bot, search, bench, meme, vision,-- themes, design, redact, chat, ai, credentials,-- jobs, tools, projects, geo, history, userCard,++ realtime, media, composer, cam, mediaUpload, ui,++ community, homeFeed, feed, bot, search, bench,++ meme, vision, themes, design, redact, chat,++ ai, credentials, jobs, agents, tools, projects,++ geo, history, userCard, player, agentSheet,} = sdk;export default sdk;@@@ -9,6 +9,16 @@"createActorClient"]},++ "agentSheet": {++ "object": [++ "createAgentSheetClient"++ ]++ },++ "agents": {++ "object": [++ "createAgentsClient"++ ]++ },"ai": {"object": ["CLASSES",@@@ -101,6 +111,11 @@"verifyTelegram"]},++ "codes": {++ "object": [++ "createCodesClient"++ ]++ },"commerce": {"object": ["CommerceError",@@@ -116,6 +131,14 @@]},"compareVersions": "function",++ "composer": {++ "object": [++ "ATTACHMENT_MAX",++ "MEDIA_ORIGIN",++ "NETWORK_ORIGIN",++ "createComposerClient"++ ]++ },"contextFromHeaders": "function","coupons": {"object": [@@@ -165,6 +188,11 @@"verifyDeliveryV2"]},++ "feed": {++ "object": [++ "createFeedClient"++ ]++ },"food": {"object": ["createFoodClient"@@@ -323,6 +351,13 @@"createPlayClient"]},++ "player": {++ "object": [++ "current",++ "handoff",++ "on"++ ]++ },"projects": {"object": ["createProjectsClient"@@@ -516,6 +551,12 @@"openvibe-sdk/actor": {"createActorClient": "function"},++ "openvibe-sdk/agent-sheet": {++ "createAgentSheetClient": "function"++ },++ "openvibe-sdk/agents": {++ "createAgentsClient": "function"++ },"openvibe-sdk/ai": {"CLASSES": "array","OPS": "array",@@@ -636,6 +677,9 @@"verifySlack": "function","verifyTelegram": "function"},++ "openvibe-sdk/codes": {++ "createCodesClient": "function"++ },"openvibe-sdk/commerce": {"CommerceError": "class","createCommerceClient": "function",@@@ -646,6 +690,12 @@"actingHeaders": "function","createCommunityClient": "function"},++ "openvibe-sdk/composer": {++ "ATTACHMENT_MAX": "number",++ "MEDIA_ORIGIN": "string",++ "NETWORK_ORIGIN": "string",++ "createComposerClient": "function"++ },"openvibe-sdk/compute": {"STRATEGIES": "array","TIERS": "array",@@@ -791,12 +841,16 @@"NETWORK_BASE_URL": "string","RETRY_STATUS": "object","SIGNATURE_TOLERANCE_SEC": "number",++ "createCatalogSearchClient": "function","createExchangeClient": "function","verifyWebhook": "function"},"openvibe-sdk/explicit": {"isExplicit": "function"},++ "openvibe-sdk/feed": {++ "createFeedClient": "function"++ },"openvibe-sdk/food": {"createFoodClient": "function"},@@@ -1031,6 +1085,11 @@"openvibe-sdk/play": {"createPlayClient": "function"},++ "openvibe-sdk/player": {++ "current": "function",++ "handoff": "function",++ "on": "function"++ },"openvibe-sdk/projects": {"createProjectsClient": "function"},@@@ -1176,6 +1235,7 @@"refresh"]},++ "HANDOFF_PARAM": "string","claimsToUser": "function","createSsoClient": "function","decodeJwtPayload": "function",@@@ -1183,6 +1243,7 @@"fedcmNonceMatches": "function","parseCookies": "function","sanitizeNext": "function",++ "stripHandoffParam": "function","withParam": "function"},"openvibe-sdk/storage": {nnew file mode 100644@@@ -0,0 +1,7 @@++// Generated by scripts/esm.js from src/agent-sheet.js. Do not edit.++import cjs from '../src/agent-sheet.js';++++export const {++ createAgentSheetClient,++} = cjs;++export default cjs;nnew file mode 100644@@@ -0,0 +1,7 @@++// Generated by scripts/esm.js from src/agents.js. Do not edit.++import cjs from '../src/agents.js';++++export const {++ createAgentsClient,++} = cjs;++export default cjs;nnew file mode 100644@@@ -0,0 +1,7 @@++// Generated by scripts/esm.js from src/codes.js. Do not edit.++import cjs from '../src/codes.js';++++export const {++ createCodesClient,++} = cjs;++export default cjs;nnew file mode 100644@@@ -0,0 +1,7 @@++// Generated by scripts/esm.js from src/composer.js. Do not edit.++import cjs from '../src/composer.js';++++export const {++ createComposerClient, MEDIA_ORIGIN, NETWORK_ORIGIN, ATTACHMENT_MAX,++} = cjs;++export default cjs;@@@ -2,7 +2,7 @@import cjs from '../src/exchange.js';export const {-- createExchangeClient, ExchangeError, verifyWebhook, SIGNATURE_TOLERANCE_SEC, EXCHANGE_BASE_URL, NETWORK_BASE_URL,-- DEFAULT_PATHS, RETRY_STATUS,++ createCatalogSearchClient, createExchangeClient, ExchangeError, verifyWebhook, SIGNATURE_TOLERANCE_SEC, EXCHANGE_BASE_URL,++ NETWORK_BASE_URL, DEFAULT_PATHS, RETRY_STATUS,} = cjs;export default cjs;nnew file mode 100644@@@ -0,0 +1,7 @@++// Generated by scripts/esm.js from src/feed.js. Do not edit.++import cjs from '../src/feed.js';++++export const {++ createFeedClient,++} = cjs;++export default cjs;@@@ -7,14 +7,15 @@ export const {isActingSubjectId, satisfiesRange, compareVersions, CONTRACTS_RANGE, DEFAULT_NETWORK, SDK_VERSION,report, share, locker, auth, registry, identity,trust, levels, modules, events, realtime, media,-- cam, mediaUpload, ui, community, homeFeed, bot,-- search, bench, meme, vision, themes, design,-- redact, chat, chatApps, ai, credentials, jobs,-- tools, projects, resources, vip, quest, live,-- inventory, tips, blog, reviews, wiki, coupons,-- deals, shoes, trade, space, host, news,-- food, actor, run, video, pics, work,-- zone, git, play, download, openre, geo,-- history, usage, commerce, moneyReasons, telemetry, userCard,++ composer, cam, mediaUpload, ui, community, homeFeed,++ feed, bot, search, bench, meme, vision,++ themes, design, redact, chat, chatApps, ai,++ credentials, jobs, agents, tools, projects, resources,++ vip, quest, live, inventory, tips, blog,++ reviews, wiki, coupons, deals, shoes, trade,++ space, host, news, food, actor, codes,++ run, video, pics, work, zone, git,++ play, download, openre, geo, history, usage,++ commerce, moneyReasons, telemetry, userCard, player, agentSheet,} = cjs;export default cjs;nnew file mode 100644@@@ -0,0 +1,7 @@++// Generated by scripts/esm.js from src/player.js. Do not edit.++import cjs from '../src/player.js';++++export const {++ current, handoff, on,++} = cjs;++export default cjs;@@@ -2,7 +2,7 @@import cjs from '../src/sso/index.js';export const {-- createSsoClient, sanitizeNext, withParam, decodeJwtPayload, fedcmNonceMatches, claimsToUser,-- parseCookies, failurePage, COOKIES,++ createSsoClient, sanitizeNext, withParam, stripHandoffParam, decodeJwtPayload, fedcmNonceMatches,++ claimsToUser, parseCookies, failurePage, HANDOFF_PARAM, COOKIES,} = cjs;export default cjs;@@@ -21,11 +21,13 @@ module.exports = {events: require('./src/events'),realtime: require('./src/realtime'),media: require('./src/media'),++ composer: require('./src/composer'),cam: require('./src/cam'),mediaUpload: require('./src/media-upload'),ui: require('./src/ui'),community: require('./src/community'),homeFeed: require('./src/home-feed'),++ feed: require('./src/feed'),bot: require('./src/bot'),search: require('./src/search'),bench: require('./src/bench'),@@@ -39,6 +41,7 @@ module.exports = {ai: require('./src/ai'),credentials: require('./src/credentials'),jobs: require('./src/jobs'),++ agents: require('./src/agents'),tools: require('./src/tools'),projects: require('./src/projects'),resources: require('./src/resources'),@@@ -59,6 +62,7 @@ module.exports = {news: require('./src/news'),food: require('./src/food'),actor: require('./src/actor'),++ codes: require('./src/codes'),run: require('./src/run'),video: require('./src/video'),pics: require('./src/pics'),@@@ -75,4 +79,6 @@ module.exports = {moneyReasons: require('./src/money-reasons'),telemetry: require('./src/telemetry'),userCard: require('./src/user-card'),++ player: require('./src/player'),++ agentSheet: require('./src/agent-sheet'),};@@@ -1,12 +1,12 @@{"name": "openvibe-sdk",-- "version": "0.147.0",++ "version": "0.148.0","lockfileVersion": 3,"requires": true,"packages": {"": {"name": "openvibe-sdk",-- "version": "0.147.0",++ "version": "0.148.0","license": "MIT","bin": {"openvibe-download": "bin/openvibe-download.js"@@@ -14,7 +14,7 @@"devDependencies": {"@electric-sql/pglite": "^0.5.8","iovalkey": "^0.4.0",-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.399.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.406.0","pg": "^8.23.0"},"engines": {@@@ -198,9 +198,9 @@"license": "MIT"},"node_modules/openvibe-contracts": {-- "version": "0.399.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.399.0",-- "integrity": "sha512-7QqSrMH2BT6ZOJu+xxusjEPICaVKGrr+3ij3tg0leE3sy4jfPwvWVTwaXZSCPM1feoP7heUDnSO4/9QBmjguJw==",++ "version": "0.406.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.406.0",++ "integrity": "sha512-V/Tjss2UWvzI551bw3ACb/RqWg0lChxGITxfCVZ8qou2Docg0pBMvIjkeQoib56yq3r6UZGB6DGl+pA+qfjOiA==","dev": true,"license": "AGPL-3.0-only","dependencies": {@@@ -1,6 +1,6 @@{"name": "openvibe-sdk",-- "version": "0.147.0",++ "version": "0.148.0","description": "Supported browser and server clients for the OpenVibe platform: auth (incl. developer apps), registry discovery, identity, user modules, events, realtime, media, community, Bot robots, search, chat, AI runs, Tools (registry, run API and jobs), VIP memberships and developer projects, the async PostgreSQL data layer (db) and the Valkey cache, limits store, queue and pub/sub, with deadlines, safe retries, problem errors and trace propagation.","main": "index.js","types": "types/index.d.ts",@@@ -84,6 +84,11 @@"import": "./esm/media.mjs","default": "./src/media.js"},++ "./composer": {++ "types": "./types/composer.d.ts",++ "import": "./esm/composer.mjs",++ "default": "./src/composer.js"++ },"./media-upload": {"types": "./types/media-upload.d.ts","import": "./esm/media-upload.mjs",@@@ -288,6 +293,11 @@"import": "./esm/jobs.mjs","default": "./src/jobs.js"},++ "./agents": {++ "types": "./types/agents.d.ts",++ "import": "./esm/agents.mjs",++ "default": "./src/agents.js"++ },"./tools": {"types": "./types/tools.d.ts","import": "./esm/tools.mjs",@@@ -315,6 +325,12 @@"import": "./esm/home-feed.mjs","default": "./src/home-feed.js"},++ "./feed": {++ "types": "./types/feed.d.ts",++ "browser": "./src/feed.js",++ "import": "./esm/feed.mjs",++ "default": "./src/feed.js"++ },"./geo": {"types": "./types/geo.d.ts","import": "./esm/geo.mjs",@@@ -594,6 +610,24 @@"types": "./types/user-card.d.ts","import": "./esm/user-card.mjs","default": "./src/user-card.js"++ },++ "./player": {++ "types": "./types/player.d.ts",++ "browser": "./src/player.js",++ "import": "./esm/player.mjs",++ "default": "./src/player.js"++ },++ "./codes": {++ "types": "./types/codes.d.ts",++ "browser": null,++ "import": "./esm/codes.mjs",++ "default": "./src/codes.js"++ },++ "./agent-sheet": {++ "types": "./types/agent-sheet.d.ts",++ "browser": "./src/agent-sheet.js",++ "import": "./esm/agent-sheet.mjs",++ "default": "./src/agent-sheet.js"}},"bin": {@@@ -639,7 +673,7 @@"devDependencies": {"@electric-sql/pglite": "^0.5.8","iovalkey": "^0.4.0",-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.399.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.406.0","pg": "^8.23.0"},"peerDependencies": {@@@ -13,6 +13,7 @@ const path = require('path');const ROOT = path.join(__dirname, '..');const ENTRIES = {'user-card.mjs': '../src/user-card.js',++ 'agent-sheet.mjs': '../src/agent-sheet.js','wallet.mjs': '../src/wallet.js','compute.mjs': '../src/compute/index.js','share.mjs': '../src/share.js',@@@ -36,6 +37,7 @@ const ENTRIES = {'cam.mjs': '../src/cam/index.js','realtime.mjs': '../src/realtime.js','media.mjs': '../src/media.js',++ 'composer.mjs': '../src/composer.js','media-upload.mjs': '../src/media-upload.js','ui.mjs': '../src/ui.js','community.mjs': '../src/community.js',@@@ -71,6 +73,7 @@ const ENTRIES = {'chat-apps.mjs': '../src/chat-apps.js','ai.mjs': '../src/ai.js','jobs.mjs': '../src/jobs.js',++ 'agents.mjs': '../src/agents.js','tools.mjs': '../src/tools.js','projects.mjs': '../src/projects.js','geo.mjs': '../src/geo.js',@@@ -80,6 +83,7 @@ const ENTRIES = {'download.mjs': '../src/download.js','quest.mjs': '../src/quest.js','live.mjs': '../src/live.js',++ 'player.mjs': '../src/player.js','streams.mjs': '../src/streams.js','reports.mjs': '../src/reports.js','inventory.mjs': '../src/inventory.js',@@@ -101,6 +105,7 @@ const ENTRIES = {'front-page.mjs': '../src/front-page.js','food.mjs': '../src/food.js','actor.mjs': '../src/actor.js',++ 'codes.mjs': '../src/codes.js','run.mjs': '../src/run.js','video.mjs': '../src/video.js','pics.mjs': '../src/pics.js',@@@ -111,6 +116,7 @@ const ENTRIES = {'frame.mjs': '../src/frame.js','menu.mjs': '../src/menu/index.js','home-feed.mjs': '../src/home-feed.js',++ 'feed.mjs': '../src/feed.js','service.mjs': '../src/service/index.js','testing.mjs': '../src/testing/index.js',};@@@ -109,6 +109,7 @@ function createActorClient(client, { baseUrl } = {}) {/** A personal agent: name, instructions, mode, budget_per_run_usd, daily_runs_max, schedule, … */create: (agent, { signal } = {}) => once({ method: 'POST', path: '/api/v1/me/agents', json: agent, signal }),get: (agent, { signal } = {}) => call({ path: agentPath(agent), signal }),++ sheet: (agent, { signal } = {}) => call({ path: agentPath(agent), query: { view: 'sheet' }, signal }),update: (agent, patch, { signal } = {}) => call({ method: 'PATCH', path: agentPath(agent), json: patch, idempotent: true, signal }),remove: (agent, { signal } = {}) => call({ method: 'DELETE', path: agentPath(agent), signal }),/** Run it now with an input → the task it started (follow it with tasks.events). */nnew file mode 100644@@@ -0,0 +1,28 @@++'use strict';++/** Owner-only agent sheets through Network's fixed cross-site relay. The caller supplies their own Bearer. */++const ACTOR = /^agt_[0-9A-HJKMNP-TV-Z]{26}$/;++const MEMORY = /^mem_[0-9A-HJKMNP-TV-Z]{26}$/;++const CODES = /^[a-z][a-z0-9-]{0,63}$/;++++function createAgentSheetClient(client, { baseUrl } = {}) {++ if (!client || typeof client.json !== 'function') throw new TypeError('createAgentSheetClient: pass an SDK core client');++ const id = (value, pattern, kind) => {++ if (!pattern.test(String(value || ''))) throw new TypeError(`invalid ${kind} id`);++ return String(value);++ };++ const path = (agent) => `/api/frame/agents/${id(agent, ACTOR, 'Actor agent')}`;++ const call = (opts) => client.json({ service: 'network', audience: 'openvibe.network', baseUrl, ...opts });++ return {++ sheet: (agent, { signal } = {}) => call({ path: `${path(agent)}/sheet`, signal }),++ codesSheet: (agent, { signal } = {}) => call({ path: `/api/frame/codes-agents/${id(agent, CODES, 'coding agent')}/sheet`, signal }),++ pause: (agent, { signal } = {}) => call({ method: 'PATCH', path: path(agent), json: { status: 'paused' }, signal, idempotent: true }),++ resume: (agent, { signal } = {}) => call({ method: 'PATCH', path: path(agent), json: { status: 'active' }, signal, idempotent: true }),++ memory: {++ add: (agent, text, { signal } = {}) => call({ method: 'POST', path: `${path(agent)}/memory`, json: { text }, signal, idempotent: false }),++ update: (agent, note, text, { signal } = {}) => call({ method: 'PATCH', path: `${path(agent)}/memory/${id(note, MEMORY, 'memory')}`, json: { text }, signal, idempotent: true }),++ remove: (agent, note, { signal } = {}) => call({ method: 'DELETE', path: `${path(agent)}/memory/${id(note, MEMORY, 'memory')}`, signal, idempotent: true }),++ },++ };++}++++module.exports = { createAgentSheetClient };nnew file mode 100644@@@ -0,0 +1,52 @@++'use strict';++/** Agent run snapshots, AG-UI event streams and approval responses. */++const { OpenVibeError, isOpenVibeError } = require('./core/errors');++const { parseSSE } = require('./realtime');++const enc = encodeURIComponent;++const pause = (ms, signal) => new Promise((resolve) => {++ const t = setTimeout(resolve, ms);++ if (signal) signal.addEventListener('abort', () => { clearTimeout(t); resolve(); }, { once: true });++});++function createAgentsClient(client, { baseUrl, service = 'network', audience = 'openvibe.network' } = {}) {++ const call = (opts) => client.request({ service, baseUrl, audience, ...opts });++ async function run(id, { signal } = {}) {++ if (!id) throw new TypeError('agents.run: id is required');++ try { return (await call({ path: `/api/v1/runs/${enc(id)}`, signal })).data; }++ catch (err) { if (isOpenVibeError(err) && err.status === 404) return null; throw err; }++ }++ async function post(id, approval, { signal } = {}) {++ if (!id) throw new TypeError('agents.post: id is required');++ if (!approval || typeof approval !== 'object' || Array.isArray(approval)) throw new TypeError('agents.post: approval must be an object');++ return (await call({ method: 'POST', path: `/api/v1/runs/${enc(id)}/approvals`, json: approval, signal })).data;++ }++ async function* events(id, { lastEventId = 0, signal, maxReconnects = 20, reconnectDelayMs = 1000 } = {}) {++ if (!id) throw new TypeError('agents.events: id is required');++ let last = String(lastEventId || '0'), failures = 0, serverRetryMs = 0;++ while (!(signal && signal.aborted)) {++ let delivered = false;++ try {++ const res = await call({ path: `/api/v1/runs/${enc(id)}/events`, responseType: 'response', retries: 0, signal,++ headers: { Accept: 'text/event-stream', 'Cache-Control': 'no-cache', 'Last-Event-ID': last } });++ if (res.status === 204) return;++ for await (const msg of parseSSE(res.data.body, { onRetry: (ms) => { serverRetryMs = ms; } })) {++ if (msg.id !== undefined && msg.id !== '') { if (/^\d+$/.test(msg.id) && Number(msg.id) <= Number(last)) continue; last = msg.id; }++ let data; try { data = JSON.parse(msg.data); } catch { data = null; }++ delivered = true; failures = 0;++ yield { id: msg.id ?? null, event: msg.event || (data && data.type) || null, data };++ if (data && ['RUN_FINISHED', 'RUN_ERROR'].includes(data.type)) return;++ }++ } catch (err) {++ if (signal && signal.aborted) return;++ if (isOpenVibeError(err) && err.status >= 400 && err.status < 500) throw err;++ if (!isOpenVibeError(err) && !['TypeError', 'AbortError'].includes(err && err.name)) throw err;++ }++ failures = delivered ? 0 : failures + 1;++ if (failures > maxReconnects) throw new OpenVibeError({ code: 'sdk.network_error', retryable: true, message: `agents: event stream ${id} failed ${failures} times in a row` });++ const backoffMs = Math.min(reconnectDelayMs * (2 ** Math.max(0, failures - 1)), 30000);++ const waitMs = Math.min(Math.max(backoffMs, serverRetryMs), 30000);++ await pause(waitMs, signal);++ }++ }++ return { run, events, post };++}++module.exports = { createAgentsClient };@@@ -72,6 +72,14 @@ function createChatClient(client, defaults = {}) {history,/** The typed address previews on a message (chat.embed@1): [] when it has none. */embeds: (message) => readEmbeds(message),++ /** Compacted day, week or month memory for a person (chat.person-memory-result@1). */++ people: {++ memory: (username, { channel, level = 'day' } = {}, o = {}) => call({ path: `/api/v1/people/${enc(String(username))}/memory`, query: { channel, level } }, o),++ /** Erase the signed-in person's memory and opt out. */++ forget: (username, o = {}) => call({ method: 'DELETE', path: `/api/v1/people/${enc(String(username))}/memory`, ...once }, o),++ /** Set the signed-in person's opt-out preference. */++ preferences: (username, optedOut, o = {}) => call({ method: 'PUT', path: `/api/v1/people/${enc(String(username))}/memory/preferences`, json: { opted_out: !!optedOut }, ...once }, o),++ },/** Vibe's model comparison API, authenticated as the person. */ask: (input, o = {}) => call({ method: 'POST', path: '/api/vibe/ask', json: input, ...once }, o),pick: (messageId, model, o = {}) => call({ method: 'POST', path: '/api/vibe/pick', json: { message_id: messageId, model }, ...once }, o),nnew file mode 100644@@@ -0,0 +1,56 @@++'use strict';++/**++ * openvibe-sdk/codes — OpenVibe.Codes for apps: the person's coding tasks as the Dock's Code tab lists, starts and++ * stops them. Routes at https://openvibe.codes/api/v1; the person's own Network access token.++ *++ * const codes = createCodesClient(createClient({ tokenProvider }));++ * const { tasks } = await codes.tasks.list({ limit: 8 }); // running first, codes.task-summary@1++ * const { id, url } = await codes.tasks.create({ text: 'Add a test.', mode: 'repo', repo: 'octocat/Hello-World' });++ * await codes.tasks.stop(id); // { ok: true }++ *++ * A task is its requester's own: another person's id is 404. Creates are never retried on their own (a retry would++ * start a second task); neither is a stop (a task already ended answers 409). The person token rides every call.++ */++const enc = encodeURIComponent;++++const TASK = /^tsk_[0-9A-HJKMNP-TV-Z]{26}$/;++const MODES = ['create', 'openvibe', 'repo'];++++function createCodesClient(client, { baseUrl } = {}) {++ if (!client || typeof client.json !== 'function' || typeof client.request !== 'function') throw new TypeError('createCodesClient: pass an openvibe-sdk/core client');++ const at = { service: 'codes', baseUrl, audience: 'openvibe.codes' };++ const call = (opts) => client.json({ ...at, ...opts });++ const once = (opts) => call({ ...opts, idempotencyKey: false });++ const id = (v) => {++ if (!TASK.test(String(v || ''))) throw new TypeError('codes: a task id is tsk_…');++ return enc(String(v));++ };++++ return {++ tasks: {++ /** The person's latest tasks, running first (codes.task-summary@1). */++ list: ({ limit = 8, signal } = {}) => {++ const n = Number(limit);++ if (!Number.isInteger(n) || n < 1 || n > 50) throw new TypeError('codes.tasks.list: limit is 1-50');++ return call({ path: '/api/v1/me/tasks', query: { limit: n }, signal });++ },++ /** Start one the Dock's way: the words, where it starts, and what it carries. → { id, url }. */++ create: ({ text, mode, repo, attachments } = {}, { signal } = {}) => {++ if (typeof text !== 'string' || !text.trim()) throw new TypeError('codes.tasks.create: text is the instruction in plain words');++ if (!MODES.includes(mode)) throw new TypeError('codes.tasks.create: mode is create, openvibe or repo');++ if (repo != null && (typeof repo !== 'string' || !repo.trim())) throw new TypeError('codes.tasks.create: repo is a repository address');++ if (attachments != null && (!Array.isArray(attachments) || attachments.some((a) => typeof a !== 'string'))) {++ throw new TypeError('codes.tasks.create: attachments are composer attachment ids');++ }++ return once({++ method: 'POST', path: '/api/v1/tasks',++ json: { text, mode, ...(repo ? { repo } : {}), ...(attachments ? { attachments } : {}) }, signal,++ });++ },++ /** Stop the running attempt; nothing carries it on. → { ok: true }. */++ stop: (taskId, { signal } = {}) => once({ method: 'POST', path: `/api/v1/tasks/${id(taskId)}/stop`, signal }),++ },++ };++}++++module.exports = { createCodesClient };@@@ -113,6 +113,8 @@ function createCommunityClient(client, defaults = {}) {query: { kind: query.kind, site: query.site, q: query.q, cursor: query.cursor, limit: query.limit,following: query.following === true || query.following === 1 ? '1' : undefined },}, o);++ // Named form used by new integrations; the callable form remains for existing SDK consumers.++ feed.list = feed;const posts = {/*** A status post in the Feed at once: { text, pictures?: [{ media_id, alt }] } (at most four, alt required,nnew file mode 100644@@@ -0,0 +1,140 @@++'use strict';++/**++ * openvibe-sdk/composer: the shared composer's client, the same on every OpenVibe site.++ *++ * attachments.upload(file, { onProgress, signal, filename, contentType, xhr })++ * POST /api/v1/attachments on Media (multipart `file`), through XMLHttpRequest so upload progress is real.++ * -> composer.attachment@1++ * attachments.remove(id)++ * DELETE /api/v1/attachments/<id>; true when it went, false when it was already gone.++ * composer.prefs.get() / composer.prefs.put(prefs, rev)++ * GET/PUT /api/v1/me/composer on Network (composer.prefs@1). A PUT sends the revision it read; a stale one is++ * 409 composer.rev_conflict and err.problem.current is the other tab's document.++ * mentions.search(q, kinds)++ * GET /api/v1/mentions on Network (composer.mention@1): people the caller follows first, then public people,++ * repositories from OpenVibe Git, first-party sites. Signed-in only; at most 8.++ *++ * Browser-safe. The composer page calls Network directly for prefs and mentions (every site's connect-src allows it)++ * and Media for the bytes (the site's CSP must allow OPENVIBE_MEDIA_ORIGIN).++ */++const { OpenVibeError, isOpenVibeError } = require('./core/errors');++++const MEDIA_ORIGIN = 'https://openvibe.media';++const NETWORK_ORIGIN = 'https://openvibe.network';++const ATTACHMENT_MAX = 300 * 1024 * 1024;++const enc = encodeURIComponent;++++function toBlob(file, contentType) {++ if (typeof Blob !== 'undefined' && file instanceof Blob) return file;++ if (typeof file === 'string' || file instanceof ArrayBuffer || ArrayBuffer.isView(file)) {++ return new Blob([file], contentType ? { type: contentType } : undefined);++ }++ throw new TypeError('attachments.upload: pass a File/Blob, ArrayBuffer, typed array (Buffer) or string');++}++++function abortError() {++ return new OpenVibeError({ code: 'sdk.aborted', message: 'the upload was aborted' });++}++++function createComposerClient(client, { mediaBaseUrl, networkBaseUrl } = {}) {++ if (!client || typeof client.json !== 'function') throw new TypeError('createComposerClient: an SDK client is required');++ const net = (opts) => client.json({ service: 'network', audience: 'openvibe.network', ...(networkBaseUrl ? { baseUrl: networkBaseUrl } : {}), ...opts });++++ /** The Media token for an XHR upload (the XHR cannot borrow the fetch client's request loop). */++ async function mediaToken() {++ const o = client.options || {};++ if (o.token != null) return o.token;++ if (typeof o.getToken === 'function') return (await o.getToken({ service: 'media', audience: 'openvibe.media' })) || null;++ return null;++ }++++ const attachments = {++ /**++ * One file -> composer.attachment@1. Progress is the browser's upload progress (XHR), not a guess.++ * `xhr` injects an XMLHttpRequest implementation (tests, a non-browser shell); otherwise the global one.++ */++ upload(file, { onProgress, signal, filename, contentType, xhr } = {}) {++ return new Promise((resolve, reject) => {++ const XHR = xhr || (typeof XMLHttpRequest !== 'undefined' ? XMLHttpRequest : null);++ if (!XHR) return reject(new OpenVibeError({ code: 'sdk.xhr_unavailable', message: 'attachments.upload needs XMLHttpRequest (pass { xhr })' }));++ let blob;++ try { blob = toBlob(file, contentType); } catch (err) { return reject(err); }++ if (blob.size > ATTACHMENT_MAX) {++ return reject(new OpenVibeError({ code: 'media.attachment.too_large', status: 413, message: `a file may be at most ${ATTACHMENT_MAX / (1024 * 1024)} MB` }));++ }++ const url = client.url({ service: 'media', ...(mediaBaseUrl ? { baseUrl: mediaBaseUrl } : {}), path: '/api/v1/attachments' });++ Promise.resolve(url).then(async (target) => {++ if (signal && signal.aborted) return reject(abortError());++ const fd = new FormData();++ fd.append('file', blob, filename || (file && file.name) || 'file');++ const req = new XHR();++ let settled = false;++ const finish = (fn, v) => { if (settled) return; settled = true; cleanup(); fn(v); };++ const offAbort = signal && typeof signal.addEventListener === 'function' ? () => signal.removeEventListener('abort', onAbort) : () => {};++ function onAbort() { try { req.abort(); } catch { /* */ } finish(reject, abortError()); }++ const cleanup = () => { offAbort(); };++ req.open('POST', target);++ req.setRequestHeader('Accept', 'application/json');++ const token = await mediaToken();++ if (token) req.setRequestHeader('Authorization', `Bearer ${token}`);++ if (req.upload && typeof onProgress === 'function') {++ req.upload.onprogress = (e) => onProgress({ loaded: e.loaded || 0, total: e.total || blob.size, lengthComputable: e.lengthComputable !== false });++ }++ req.onload = () => {++ let body = null;++ try { body = req.responseText ? JSON.parse(req.responseText) : null; } catch { body = req.responseText || null; }++ if (req.status >= 200 && req.status < 300) return finish(resolve, body);++ const err = OpenVibeError.fromResponse({ status: req.status, body, method: 'POST', url: target });++ if (body && body.current) err.current = body.current;++ finish(reject, err);++ };++ req.onerror = () => finish(reject, new OpenVibeError({ code: 'sdk.network_error', message: 'the upload could not reach Media', method: 'POST', url: target }));++ req.ontimeout = () => finish(reject, new OpenVibeError({ code: 'sdk.timeout', message: 'the upload timed out', method: 'POST', url: target }));++ if (signal && typeof signal.addEventListener === 'function') signal.addEventListener('abort', onAbort, { once: true });++ try { req.send(fd); } catch (err) { finish(reject, err); }++ }).catch(reject);++ });++ },++++ /** DELETE /api/v1/attachments/<id>: true when it went, false when it was already gone. */++ async remove(id) {++ if (!id) throw new TypeError('attachments.remove: pass the attachment id');++ try {++ await client.json({ service: 'media', ...(mediaBaseUrl ? { baseUrl: mediaBaseUrl } : {}), audience: 'openvibe.media', method: 'DELETE', path: `/api/v1/attachments/${enc(id)}` });++ return true;++ } catch (err) {++ if (isOpenVibeError(err) && err.status === 404) return false;++ throw err;++ }++ },++ };++++ const composer = {++ prefs: {++ /** composer.prefs@1, with its rev. */++ get: () => net({ path: '/api/v1/me/composer' }),++ /**++ * composer.prefs@1 in (rev is the one you read; 0 for a new person), the new document out.++ * A stale rev is 409 composer.rev_conflict with err.problem.current (and err.current).++ */++ put(prefs, rev) {++ if (!prefs || typeof prefs !== 'object') throw new TypeError('composer.prefs.put: pass the prefs object');++ if (!Number.isInteger(rev) || rev < 0) throw new TypeError('composer.prefs.put: pass the revision you read (0 for new)');++ return net({ method: 'PUT', path: '/api/v1/me/composer', json: { ...prefs, rev } });++ },++ },++ };++++ const mentions = {++ /** @ menu entries (composer.mention@1[]). kinds: person | repo | site | file; omitted = all. */++ async search(q, kinds) {++ const list = Array.isArray(kinds) ? kinds : (kinds ? String(kinds).split(',') : []);++ const data = await net({ path: '/api/v1/mentions', query: { q: q || undefined, kinds: list.length ? list.join(',') : undefined } });++ return (data && Array.isArray(data.mentions)) ? data.mentions : [];++ },++ };++++ return { attachments, composer, mentions, MEDIA_ORIGIN, NETWORK_ORIGIN };++}++++module.exports = { createComposerClient, MEDIA_ORIGIN, NETWORK_ORIGIN, ATTACHMENT_MAX };@@@ -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.147.0';++const SDK_VERSION = '0.148.0';module.exports = {createClient,@@@ -47,6 +47,7 @@ const crypto = require('node:crypto');const EXCHANGE_BASE_URL = 'https://api.openvibe.exchange';/** The Network's origin: the user-owned AI offers live there. */const NETWORK_BASE_URL = 'https://api.openvibe.network';++const CATALOG_SEARCH_URL = 'https://openvibe.exchange';/** How far a signed webhook's timestamp may be from this server's clock, in seconds. */const SIGNATURE_TOLERANCE_SEC = 300;@@@ -664,6 +665,7 @@ function createExchangeClient({}module.exports = {++ createCatalogSearchClient,createExchangeClient,ExchangeError,verifyWebhook,@@@ -673,3 +675,18 @@ module.exports = {DEFAULT_PATHS,RETRY_STATUS,};++++/** A public, keyless reader for Exchange's model and provider search. */++function createCatalogSearchClient({ baseUrl = CATALOG_SEARCH_URL, fetchImpl = globalThis.fetch } = {}) {++ return {++ async search(q, filters = {}, { signal } = {}) {++ if (typeof q !== 'string' || q.length > 200) throw new TypeError('q must be a string of at most 200 characters');++ const url = new URL('/api/v1/search', baseUrl);++ for (const [key, value] of Object.entries({ ...filters, q })) if (value != null && value !== '') url.searchParams.set(key, String(value));++ const res = await fetchImpl(url, { headers: { Accept: 'application/json' }, signal });++ const body = await res.json();++ if (!res.ok) throw new ExchangeError(body?.detail || body?.error || `Search answered ${res.status}`, { status: res.status, body });++ return body;++ },++ };++}nnew file mode 100644@@@ -0,0 +1,15 @@++'use strict';++++/** Browser-safe reader for the public, cross-site Feed cards on OpenVibe.Community. */++function createFeedClient(client, { communityBaseUrl } = {}) {++ if (!client || typeof client.json !== 'function') throw new TypeError('createFeedClient: pass an openvibe-sdk/core client');++ return {++ /** Read one page of cards; pass next_cursor back as cursor to continue. */++ list: ({ kind, cursor } = {}, { signal } = {}) => client.json({++ service: 'community', baseUrl: communityBaseUrl, audience: 'openvibe.community',++ path: '/api/v1/feed', query: { kind, cursor }, signal,++ }),++ };++}++++module.exports = { createFeedClient };@@@ -1,10 +1,11 @@'use strict';--/** The menu's portable Home layout, network day counts and personal wall. Pass a user-token core client;-- * contribute() needs an app token with the person's community.pulse.contribute grant. */++/** The menu's portable Home layout, network day counts, what people do now and personal wall. Pass a user-token core++ * client; contribute() needs an app token with the person's community.pulse.contribute grant. */function createHomeFeedClient(client, { networkBaseUrl, communityBaseUrl } = {}) {if (!client || typeof client.json !== 'function') throw new TypeError('createHomeFeedClient: pass an openvibe-sdk/core client');const network = (opts) => client.json({ service: 'network', baseUrl: networkBaseUrl, audience: 'openvibe.network', ...opts });const community = (opts) => client.json({ service: 'community', baseUrl: communityBaseUrl, audience: 'openvibe.community', ...opts });++ const limitQuery = (limit) => (limit == null ? undefined : { limit });return {home: {get: ({ signal } = {}) => network({ path: '/api/v1/me/home-screen', signal }),@@@ -12,6 +13,10 @@ function createHomeFeedClient(client, { networkBaseUrl, communityBaseUrl } = {})},network: {today: ({ signal } = {}) => network({ path: '/api/v1/frame/today', auth: false, signal }),++ // The signed-out front door's list: Report's public aggregate, no credentials sent.++ popular: ({ limit, signal } = {}) => network({ path: '/api/v1/home/popular', query: limitQuery(limit), auth: false, signal }),++ // The signed-in person's own suggestions; Network computes them in the caller's request.++ tryNext: ({ limit, signal } = {}) => network({ path: '/api/v1/me/try-next', query: limitQuery(limit), signal }),},feed: {list: ({ for: audience = 'me', cursor, signal } = {}) => community({ path: '/api/v1/pulse', query: { for: audience, cursor }, signal }),nnew file mode 100644@@@ -0,0 +1,13 @@++'use strict';++/** Browser facade for Shared's portable live player. */++function host() { return typeof window === 'undefined' ? null : window.OVOverlay || null; }++function current() { return host()?.current() || null; }++function handoff(url) { return host()?.handoffUrl(url) || url; }++function on(listener) {++ if (typeof listener !== 'function') throw new TypeError('listener must be a function');++ if (typeof window === 'undefined') return () => {};++ const receive = (event) => listener(event.detail);++ window.addEventListener('openvibe:player', receive);++ return () => window.removeEventListener('openvibe:player', receive);++}++module.exports = { current, handoff, on };@@@ -11,7 +11,8 @@* networkUrl: 'https://openvibe.network', networkInternalUrl: 'http://127.0.0.1:4000',* secureCookies: true,* });-- * app.use('/auth', sso.router(express)); // /login /callback /fedcm /logout /me /refresh++ * app.use('/auth', sso.router(express)); // /login /callback /fedcm /logout /me /refresh /health /handoff++ * app.use(sso.handoff()); // ?ov_handoff= arrival: redeem before the page renders* app.use(sso.optionalAuth()); // req.user, req.token when a valid session is presented* app.get('/private', sso.requireAuth(), handler); // 401 JSON without a session*@@@ -35,6 +36,10 @@* GET /health → { ok, client: 'ok' | 'invalid' | 'unknown' }: does the Network accept this site's OAuth client?* (a token request with a code that cannot exist: invalid_grant means the client is fine,* invalid_client means its id or secret is wrong; cached 10 minutes; 503 only for 'invalid')++ * POST /handoff → { ticket, expires_in }: a one-time 60 s ticket for a link to another OpenVibe site++ * (the caller's session proves the person; body: the destination's client_id + redirect_uri).++ * Mount sso.handoff() early: an arrival carrying ?ov_handoff= redeems for this site's session++ * before the page renders — no extra round trip, never switches a live session, never wakes a guest.** When a sign-in cannot finish (0.73.0): a lost or expired flow (the state cookie is gone or does not match) and a code* the Network no longer accepts (invalid_grant: used twice, or too old) start the sign-in once more by themselves, with@@@ -107,6 +112,24 @@ function withParam(target, key, value) {return `${base}${base.includes('?') ? '&' : '?'}${encodeURIComponent(key)}=${encodeURIComponent(value)}${hash}`;}++/**++ * The cross-site handoff ticket arrives as ?ov_handoff=<ticket> on a plain link (Shared's handoffLinks++ * carries it; the source minted it at its own /auth/handoff). It is single-use and short-lived, so the++ * first thing a redeem does is scrub it from the address bar with a same-site redirect.++ */++const HANDOFF_PARAM = 'ov_handoff';++const TICKET_RE = /^ovh_[A-Za-z0-9_-]{20,200}$/;++++/** Remove the handoff ticket from a path+query URL, keeping everything else. Never throws. */++function stripHandoffParam(target) {++ try {++ const u = new URL(String(target || '/'), 'https://site.invalid');++ u.searchParams.delete(HANDOFF_PARAM);++ const q = u.searchParams.toString();++ return `${u.pathname}${q ? `?${q}` : ''}${u.hash}`;++ } catch { return '/'; }++}++/** A JWT's payload WITHOUT checking its signature: only to pre-check a FedCM nonce before the Network verifies it. */function decodeJwtPayload(token) {if (typeof token !== 'string') return null;@@@ -238,6 +261,32 @@ function createSsoClient(opts = {}) {}const SIGN_IN_AGAIN = { title: 'Sign-in didn\'t finish', text: 'It took too long, or it started in another tab or app. Start it again and it will go through; your account is fine.' };++ /** A JSON POST to the Network: the internal URL first, the public one if it cannot be reached. */++ async function networkPost(path, body, bearer) {++ let lastErr = null;++ for (const base of [...new Set([networkInternalUrl, networkUrl])]) {++ try {++ const res = await fetchImpl(`${base}${path}`, {++ method: 'POST',++ headers: { 'Content-Type': 'application/json', ...(bearer ? { Authorization: `Bearer ${bearer}` } : {}) },++ body: JSON.stringify(body),++ signal: AbortSignal.timeout(10_000),++ });++ const data = await res.json().catch(() => ({}));++ if (!res.ok) {++ throw Object.assign(new Error(data.error_description || data.error || `request failed (${res.status})`), {++ status: res.status, error: data.error || 'invalid_grant', error_description: data.error_description || null,++ });++ }++ return data;++ } catch (err) {++ lastErr = err;++ if (err.status && err.status < 500) throw err; // the request itself is bad: another base will not help++ }++ }++ throw lastErr || new Error('Network unreachable');++ }++/** A token grant at the Network: the internal URL first, the public one if it cannot be reached. */async function tokenGrant(body) {let lastErr = null;@@@ -403,6 +452,31 @@ function createSsoClient(opts = {}) {return res.status(502).json({ error: 'OpenVibe.Network could not be reached' });}},++++ /**++ * Mint a one-time cross-site handoff ticket for a link to another OpenVibe site. The caller's++ * session proves the person (Bearer or cookie); the ticket is bound to the destination the caller++ * names and lives 60 seconds. Shared's handoffLinks calls this same-origin on intent.++ */++ async handoffMint(req, res) {++ const token = extractToken(req);++ if (!await verify(token)) return res.status(401).json({ error: 'Sign in required' });++ const { client_id, redirect_uri } = (req.body || {});++ if (typeof client_id !== 'string' || !client_id || typeof redirect_uri !== 'string' || !redirect_uri) {++ return res.status(400).json({ error: 'invalid_request', error_description: 'client_id and redirect_uri are required' });++ }++ try {++ const data = await networkPost('/api/v1/handoff/tickets', { client_id, redirect_uri }, token);++ if (!data || typeof data.ticket !== 'string' || !TICKET_RE.test(data.ticket)) {++ throw Object.assign(new Error('Network issued no ticket'), { status: 502 });++ }++ return res.json({ ticket: data.ticket, expires_in: data.expires_in || 60 });++ } catch (err) {++ if (err.status && err.status < 500) return res.status(err.status).json({ error: err.error || 'invalid_request', error_description: err.error_description || err.message });++ log.error(`[${site} auth] handoff mint failed: ${err.message}`);++ return res.status(502).json({ error: 'OpenVibe.Network could not be reached' });++ }++ },};let health = null; // { client, at }: the last check, or the last sign-in's verdict@@@ -442,6 +516,7 @@ function createSsoClient(opts = {}) {r.get('/me', wrap(handlers.me));r.post('/refresh', wrap(handlers.refresh));r.get('/health', wrap(handlers.health));++ r.post('/handoff', json, badJson, wrap(handlers.handoffMint));return r;}@@@ -460,10 +535,45 @@ function createSsoClient(opts = {}) {return (req, res, nextFn) => opt(req, res, () => (req.user ? nextFn() : res.status(401).json({ error: 'Sign in required' })));}-- return { router, handlers, optionalAuth, requireAuth, verify, extractToken, authorizeUrl, tokenGrant, checkClient, redirectUri, audience };++ /**++ * Arrival with ?ov_handoff=<ticket>: redeem it for this site's session before the page renders, so a++ * link from another OpenVibe site lands signed in with no extra round trip. Mount early (before your++ * pages): `app.use(sso.handoff())`. Rules, all enforced here, never in the page:++ * - GET navigations only, and only when this browser has NO session here (never switch an account);++ * - never when the browser carries this site's guest hint (a guest stays a guest, no Network call);++ * - any redeem or exchange failure renders the page as a guest; the ticket is always scrubbed from++ * the address bar with a same-site redirect, so a bearer ticket never sits in history.++ */++ function handoff() {++ return (req, res, nextFn) => {++ (async () => {++ const ticket = (req.query || {})[HANDOFF_PARAM];++ if (req.method !== 'GET' || typeof ticket !== 'string' || !TICKET_RE.test(ticket)) return nextFn();++ if (parseCookies(req)[HINT_COOKIE] === 'guest') return nextFn();++ if (await verify(extractToken(req))) return nextFn();++ const target = req.originalUrl || req.url || '/';++ const clean = stripHandoffParam(target);++ try {++ const data = await networkPost('/api/v1/handoff/redeem', {++ ticket, client_id: opts.clientId, client_secret: opts.clientSecret, redirect_uri: redirectUri,++ });++ if (!data || typeof data.code !== 'string' || !data.code) throw new Error('Network issued no code');++ const tokens = await tokenGrant({ grant_type: 'authorization_code', redirect_uri: redirectUri, code: data.code });++ if (!tokens || typeof tokens.access_token !== 'string' || !tokens.access_token) throw new Error('Network issued no session');++ setSession(res, tokens.access_token, tokens.refresh_token);++ } catch (err) {++ log.warn(`[${site} auth] handoff arrival failed: ${err.message}`);++ }++ return res.redirect(clean);++ })().catch(nextFn);++ };++ }++++ return { router, handlers, handoff, optionalAuth, requireAuth, verify, extractToken, authorizeUrl, tokenGrant, checkClient, redirectUri, audience };}module.exports = {-- createSsoClient, sanitizeNext, withParam, decodeJwtPayload, fedcmNonceMatches, claimsToUser, parseCookies, failurePage,++ createSsoClient, sanitizeNext, withParam, stripHandoffParam, decodeJwtPayload, fedcmNonceMatches, claimsToUser, parseCookies, failurePage,++ HANDOFF_PARAM,COOKIES: Object.freeze({ access: ACCESS_COOKIE, refresh: REFRESH_COOKIE, hint: HINT_COOKIE }),};@@@ -1,9 +1,10 @@'use strict';--/** Personal bearer wallet/store facade. Amounts and ownership are enforced by Billing and Inventory. */--function createWalletClient(client, { billingUrl, inventoryUrl } = {}) {++/** Personal bearer wallet/store facade. Amounts and ownership are enforced by their ledger services. */++function createWalletClient(client, { billingUrl, inventoryUrl, networkUrl } = {}) {if (!client || typeof client.json !== 'function') throw new TypeError('createWalletClient requires an SDK core client');-- const call = (service, path, opts = {}) => client.json({ service, baseUrl: service === 'billing' ? billingUrl : inventoryUrl,++ const urls = { billing: billingUrl, inventory: inventoryUrl, network: networkUrl };++ const call = (service, path, opts = {}) => client.json({ service, baseUrl: urls[service],audience: `openvibe.${service}`, path, credentials: 'omit', ...opts });const key = (v) => {if (typeof v !== 'string' || !/^[A-Za-z0-9._:-]{8,128}$/.test(v)) throw new TypeError('An idempotency_key of 8–128 letters, digits, dot, colon, underscore or dash is required');@@@ -11,12 +12,38 @@ function createWalletClient(client, { billingUrl, inventoryUrl } = {}) {};const whole = (v) => { if (!Number.isSafeInteger(v) || v < 1) throw new TypeError('Choose a positive whole amount'); return v; };const itemId = (v) => { if (!/^itd_[0-9A-HJKMNP-TV-Z]{26}$/.test(v || '')) throw new TypeError('An item id is an itd_ id'); return encodeURIComponent(v); };++ const cursor = (v, currency) => {++ if (v == null) return v;++ if (currency === 'vibes') {++ if (typeof v !== 'string' || !/^txn_[0-9A-HJKMNP-TV-Z]{26}$/.test(v)) throw new TypeError('vibes cursor is a txn_ id');++ } else if (!Number.isSafeInteger(Number(v)) || Number(v) <= 0 || !/^[0-9]+$/.test(String(v))) {++ throw new TypeError('cursor is a positive move id');++ }++ return v;++ };++ const streamer = (v) => { if (!Number.isSafeInteger(v) || v <= 0) throw new TypeError('streamer is a positive user id'); return v; };// The BODY key is durable in both services. Retries keep it; the SDK must not add an unrelated header key.const write = (service, path, json, { signal } = {}) => call(service, path, { method: 'POST', json,idempotencyKey: false, idempotent: true, signal });return {wallet: {get: ({ signal } = {}) => call('billing', '/api/v1/me/wallet', { signal }),++ changes: ({ since_vibes, since_opencoins, since_channel_points, streamer: channel, limit, signal } = {}) => {++ if (limit != null && (!Number.isSafeInteger(limit) || limit < 1 || limit > 500)) throw new TypeError('limit is 1 to 500');++ return call('network', '/api/wallet/changes', { query: {++ since_vibes: cursor(since_vibes, 'vibes'), since_opencoins: cursor(since_opencoins, 'opencoins'),++ since_channel_points: cursor(since_channel_points, 'channel-points'),++ streamer: channel == null ? undefined : streamer(channel), limit,++ }, signal });++ },++ markRead: ({ currency, cursor: at, streamer: channel }, { signal } = {}) => {++ if (!['vibes', 'opencoins', 'channel-points'].includes(currency)) throw new TypeError('currency is vibes, opencoins or channel-points');++ if (at == null) throw new TypeError('cursor is required');++ if (currency === 'channel-points' && channel == null) throw new TypeError('streamer is required for channel points');++ return call('network', '/api/wallet/changes/read', { method: 'PUT', json: {++ currency, cursor: cursor(at, currency), ...(currency === 'channel-points' ? { streamer: streamer(channel) } : {}),++ }, signal });++ },topUp: ({ provider, amount_cents, idempotency_key }, options) => write('billing', '/api/v1/me/wallet/topups',{ provider, amount_cents: whole(amount_cents), idempotency_key: key(idempotency_key) }, options),},nnew file mode 100644@@@ -0,0 +1,36 @@++'use strict';++const assert = require('assert');++const { createAgentSheetClient } = require('../src/agent-sheet');++const { createActorClient } = require('../src/actor');++const seen = [];++const client = createAgentSheetClient({ json: async (request) => { seen.push(request); return { ok: true }; } });++const agent = 'agt_00000000000000000000000000';++const memory = 'mem_00000000000000000000000000';++++(async () => {++ await client.sheet(agent);++ await client.codesSheet('codex');++ await client.pause(agent);++ await client.resume(agent);++ await client.memory.add(agent, 'A fact');++ await client.memory.update(agent, memory, 'New fact');++ await client.memory.remove(agent, memory);++ await createActorClient({ json: async (request) => { seen.push(request); return { ok: true }; }, request: async () => ({}) }).me.agents.sheet(agent);++ assert.deepStrictEqual(seen.map((r) => [r.method || 'GET', r.path]), [++ ['GET', `/api/frame/agents/${agent}/sheet`],++ ['GET', '/api/frame/codes-agents/codex/sheet'],++ ['PATCH', `/api/frame/agents/${agent}`],++ ['PATCH', `/api/frame/agents/${agent}`],++ ['POST', `/api/frame/agents/${agent}/memory`],++ ['PATCH', `/api/frame/agents/${agent}/memory/${memory}`],++ ['DELETE', `/api/frame/agents/${agent}/memory/${memory}`],++ ['GET', `/api/v1/me/agents/${agent}`],++ ]);++ assert.ok(seen.slice(0, -1).every((r) => r.service === 'network' && r.audience === 'openvibe.network'));++ assert.deepStrictEqual(seen.at(-1).query, { view: 'sheet' });++ assert.deepStrictEqual([seen[2].json, seen[3].json], [{ status: 'paused' }, { status: 'active' }]);++ assert.throws(() => client.sheet('../other'), TypeError);++ assert.throws(() => client.codesSheet('../other'), TypeError);++ assert.throws(() => client.memory.remove(agent, '../other'), TypeError);++ console.log('agent sheet reader: fixed Network paths and validated IDs');++})().catch((error) => { console.error(error); process.exitCode = 1; });nnew file mode 100644@@@ -0,0 +1,24 @@++'use strict';++const assert = require('node:assert/strict');++const { run, stubServer, send } = require('./helpers');++const { createClient } = require('../src/core');++const { createAgentsClient } = require('../src/agents');++run([++ ['run snapshot, post approval, and resumable AG-UI SSE', async () => {++ let calls = 0; const srv = await stubServer((req,res) => {++ if (req.url.endsWith('/approvals')) return send(res,200,{accepted:true});++ if (req.url.endsWith('/events')) { calls++; assert.equal(req.headers['last-event-id'], calls === 1 ? '4' : '5'); res.writeHead(200,{'Content-Type':'text/event-stream'}); res.write(`id: ${calls===1?'5':'6'}\nevent: ${calls===1?'STEP_STARTED':'RUN_FINISHED'}\ndata: ${JSON.stringify({type:calls===1?'STEP_STARTED':'RUN_FINISHED',timestamp:'2026-11-11T00:00:00Z'})}\n\n`); return res.end(); }++ return send(res,200,{id:'run_a',status:'running'});++ });++ try { const agents=createAgentsClient(createClient({baseUrls:{network:srv.url},token:'t'}));++ assert.equal((await agents.run('run_a')).id,'run_a'); assert.deepEqual(await agents.post('run_a',{id:'approval_1',action:'approve'}),{accepted:true});++ const got=[]; for await (const e of agents.events('run_a',{lastEventId:4,reconnectDelayMs:1})) got.push([e.id,e.event]);++ assert.deepEqual(got,[['5','STEP_STARTED'],['6','RUN_FINISHED']]); assert.equal(srv.requests[3].headers['last-event-id'],'5');++ } finally { await srv.close(); }++ }],++ ['reconnect retries use exponential backoff', async () => {++ let calls=0; const srv=await stubServer((req,res)=>{ calls++; if(calls<4) return send(res,503,{code:'temporarily.unavailable'}); res.writeHead(200,{'Content-Type':'text/event-stream'}); res.end(`id: 1\nevent: RUN_FINISHED\ndata: ${JSON.stringify({type:'RUN_FINISHED',timestamp:1794355200000})}\n\n`); });++ try { const agents=createAgentsClient(createClient({baseUrls:{network:srv.url},token:'t'})); const start=Date.now(); const got=[]; for await(const e of agents.events('run_a',{reconnectDelayMs:10,maxReconnects:5})) got.push(e.id); assert.deepEqual(got,['1']); assert.ok(Date.now()-start>=65,`backoff elapsed ${Date.now()-start}ms`); assert.equal(calls,4); }++ finally { await srv.close(); }++ }],++]);@@@ -117,8 +117,11 @@ run([assert.equal(me.body.user.subject, USER);assert.deepEqual(me.body.user.capabilities, ['media.object.read']);assert.equal(me.body.user.app, `app:${APP}`);-- assert.ok(!JSON.stringify(me.body).includes('eyJ'), 'no token in the answer');-- assert.ok(!JSON.stringify(ok.cookies).includes('eyJ'), 'no token in the cookie');++ // A token is a JWT's shape (three base64url parts, the first "eyJ…"), not the letters "eyJ": a random session id++ // can hold those three letters, and the bare substring failed now and then on the slow macOS runners (2026-11-11).++ const JWT = /eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/;++ assert.ok(!JWT.test(JSON.stringify(me.body)), 'no token in the answer');++ assert.ok(!JWT.test(JSON.stringify(ok.cookies)), 'no token in the cookie');// Signed out: no user.const out = fakeRes();@@@ -76,7 +76,7 @@ run([for (const [re, why] of FORBIDDEN) if (re.test(src)) problems.push(`${path.relative(ROOT, file)}: ${why}`);}assert.deepEqual(problems, []);-- assert.deepEqual(blocked.sort(), ['./account-data', './actor', './app-signin', './assertions', './blog', './cache', './chat-apps', './commerce', './compute', './consent', './cost', './coupons', './db', './deals', './delivery', './design', './download', './events', './exchange', './food', './git', './govern', './host', './identity', './inventory', './limits', './link', './live', './menu', './news', './notifications', './openre', './placement', './play', './pubsub', './quest', './queue', './reports', './resources', './reviews', './run', './runtime', './service', './share', './shoes', './space', './sso', './storage', './streams', './telemetry', './testing', './tips', './trade', './usage', './valkey', './vibes', './video', './vip', './wiki', './work', './zone']);++ assert.deepEqual(blocked.sort(), ['./account-data', './actor', './app-signin', './assertions', './blog', './cache', './chat-apps', './codes', './commerce', './compute', './consent', './cost', './coupons', './db', './deals', './delivery', './design', './download', './events', './exchange', './food', './git', './govern', './host', './identity', './inventory', './limits', './link', './live', './menu', './news', './notifications', './openre', './placement', './play', './pubsub', './quest', './queue', './reports', './resources', './reviews', './run', './runtime', './service', './share', './shoes', './space', './sso', './storage', './streams', './telemetry', './testing', './tips', './trade', './usage', './valkey', './vibes', './video', './vip', './wiki', './work', './zone']);const rel = [...scanned].map((f) => path.relative(ROOT, f).split(path.sep).join('/')); // the same names on Windowsfor (const must of ['browser.js', 'src/core/client.js', 'src/auth/browser.js', 'src/cam/index.js', 'src/realtime.js', 'src/media.js', 'src/community.js', 'src/bot.js', 'src/modules.js', 'src/registry.js', 'src/jobs.js', 'src/tools.js', 'src/core/form.js', 'src/projects.js', 'browser/openvibe-sdk.mjs']) {assert.ok(rel.includes(must), `${must} is scanned`);@@@ -20,6 +20,20 @@ async function chatStub() {}run([++ ['person memory reader and privacy controls use the person API', async () => {++ const srv = await chatStub();++ const chat = createChatClient(createClient({ baseUrls: { chat: srv.url }, token: 'person-token' }));++ await chat.people.memory('alice', { channel: 'streamer', level: 'week' });++ await chat.people.forget('alice');++ await chat.people.preferences('alice', false);++ assert.deepEqual(srv.requests.map(r => [r.method, r.url]), [++ ['GET', '/api/v1/people/alice/memory?channel=streamer&level=week'],++ ['DELETE', '/api/v1/people/alice/memory'],++ ['PUT', '/api/v1/people/alice/memory/preferences'],++ ]);++ assert.deepEqual(JSON.parse(String(srv.requests[2].body)), { opted_out: false });++ await srv.close();++ }],['embeds reads chat.embed@1 and returns the typed shape, dropping what is not one', async () => {const chat = createChatClient(createClient({ baseUrls: { chat: 'http://127.0.0.1:9' }, token: 'person-token' }));const link = { kind: 'link', url: 'https://example.com/a', site_name: 'Example', title: 'A story', description: 'What happened', image: null, width: 640, height: 360 };nnew file mode 100644@@@ -0,0 +1,59 @@++'use strict';++// openvibe-sdk/codes: the Dock's Code tab calls (list, create, stop) through a real core client against a local stub.++const assert = require('assert');++const http = require('http');++const { createClient } = require('../src/core');++const { createCodesClient } = require('../src/codes');++++const TASK = 'tsk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3';++const summary = (over = {}) => ({++ id: TASK, title: 'Add a test', status: 'running', mode: 'repo', repo: 'https://github.com/octocat/Hello-World',++ started_at: '2026-11-10T10:00:00.000Z', updated_at: '2026-11-10T10:04:00.000Z', steps: 2,++ url: `https://openvibe.codes/tasks/${TASK}`, preview_url: null, pr_url: null, ...over,++});++++(async () => {++ const seen = [];++ const server = http.createServer((req, res) => {++ let body = '';++ req.on('data', (c) => { body += c; });++ req.on('end', () => {++ seen.push({ method: req.method, url: req.url, auth: req.headers.authorization || null, body });++ const json = (status, obj) => { res.writeHead(status, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(obj)); };++ if (req.url === '/api/v1/me/tasks?limit=8' && req.method === 'GET') return json(200, { tasks: [summary()] });++ if (req.url === '/api/v1/tasks' && req.method === 'POST') {++ const b = JSON.parse(body);++ if (b.mode === 'create') return json(200, { id: null, url: 'https://openvibe.space/auth/login?next=%2Fspaces%2Fnew' });++ return json(201, { id: TASK, url: `https://openvibe.codes/tasks/${TASK}` });++ }++ if (req.url === `/api/v1/tasks/${TASK}/stop` && req.method === 'POST') return json(200, { ok: true });++ return json(404, { code: 'codes.task_not_found' });++ });++ });++ await new Promise((r) => server.listen(0, '127.0.0.1', r));++ const base = `http://127.0.0.1:${server.address().port}`;++ const codes = createCodesClient(createClient({ baseUrls: { codes: base }, token: 'person-token' }));++ try {++ const listed = await codes.tasks.list({ limit: 8 });++ assert.deepStrictEqual(listed, { tasks: [summary()] });++ const created = await codes.tasks.create({ text: 'Add a test.', mode: 'repo', repo: 'octocat/Hello-World' });++ assert.deepStrictEqual(created, { id: TASK, url: `https://openvibe.codes/tasks/${TASK}` });++ const space = await codes.tasks.create({ text: 'A bakery site.', mode: 'create' });++ assert.strictEqual(space.id, null);++ assert.ok(space.url.startsWith('https://openvibe.space'));++ assert.deepStrictEqual(await codes.tasks.stop(TASK), { ok: true });++++ const posted = JSON.parse(seen.find((s) => s.url === '/api/v1/tasks' && JSON.parse(s.body).mode === 'repo').body);++ assert.deepStrictEqual(posted, { text: 'Add a test.', mode: 'repo', repo: 'octocat/Hello-World' });++ assert.ok(seen.every((s) => s.auth === 'Bearer ' + 'person-token'), 'the person token rides every call');++ assert.throws(() => codes.tasks.list({ limit: 0 }), /limit is 1-50/);++ assert.throws(() => codes.tasks.create({ text: '', mode: 'repo' }), /text is the instruction/);++ assert.throws(() => codes.tasks.create({ text: 'Hi', mode: 'teleport' }), /mode is create/);++ assert.throws(() => codes.tasks.create({ text: 'Hi', mode: 'repo', repo: ' ' }), /repo is a repository/);++ assert.throws(() => codes.tasks.create({ text: 'Hi', mode: 'repo', attachments: [42] }), /attachment ids/);++ assert.throws(() => codes.tasks.stop('nope'), /tsk_/);++ console.log(' ok codes tasks: list, create (repo and create), stop, and every validation');++ } finally {++ await new Promise((r) => server.close(r));++ }++})().catch((err) => { console.error(err); process.exit(1); });@@@ -161,7 +161,7 @@ run([const sdk = require('..');const community = sdk.community.createCommunityClient(sdk.createClient({ baseUrls: { community: srv.url } }));const query = { kind: 'stream', site: 'live', q: 'broadcast', following: 1, limit: 1 };-- const first = await community.feed(query);++ const first = await community.feed.list(query);assert.deepEqual(first, { items: [], next_cursor: 'opaque-cursor' });const second = await community.feed({ ...query, cursor: first.next_cursor });assert.deepEqual(second, { items: [item], next_cursor: null });nnew file mode 100644@@@ -0,0 +1,118 @@++'use strict';++/** The shared composer client: XHR upload with real progress, abort, remove, prefs with the revision, mentions. */++const assert = require('node:assert/strict');++const { stubServer, send, run } = require('./helpers');++const { createClient } = require('../src/core');++const { createComposerClient } = require('../src/composer');++++function fakeXHR({ never = false } = {}) {++ return class FakeXHR {++ constructor() { this.upload = {}; this.headers = {}; this.sent = false; }++ open(method, url) { this.method = method; this.url = url; }++ setRequestHeader(k, v) { this.headers[k.toLowerCase()] = v; }++ send(body) {++ this.sent = true;++ this.body = body;++ if (never) return;++ fetch(this.url, { method: this.method, headers: this.headers, body }).then(async (r) => {++ this.status = r.status;++ this.responseText = await r.text();++ this.upload.onprogress?.({ loaded: 1, total: 1, lengthComputable: true });++ this.onload?.();++ }).catch((err) => this.onerror?.(err));++ }++ abort() { this.aborted = true; }++ };++}++++run([++ ['the ESM entry exports the same client', async () => {++ const esm = await import('../esm/composer.mjs');++ assert.equal(typeof esm.createComposerClient, 'function');++ assert.equal(esm.MEDIA_ORIGIN, 'https://openvibe.media');++ }],++++ ['upload posts one multipart file through XHR, with the token and progress', async () => {++ const srv = await stubServer((req, res) => send(res, 201, {++ id: 'att_01JAB2C3D4E5F6G7H8J9K0MNPQ', kind: 'image', name: 'shot.png', size: 3, mime: 'image/png',++ url: 'https://openvibe.media/a/att_01JAB2C3D4E5F6G7H8J9K0MNPQ/shot.png', thumb: null, w: 1, h: 1,++ expires_at: '2026-11-11T12:00:00.000Z',++ }));++ const composer = createComposerClient(createClient({ baseUrls: { media: srv.url }, token: 'person-token' }));++ const progress = [];++ const out = await composer.attachments.upload(new Blob([new Uint8Array([1, 2, 3])], { type: 'image/png' }), {++ filename: 'shot.png', onProgress: (p) => progress.push(p), xhr: fakeXHR(),++ });++ assert.equal(out.id, 'att_01JAB2C3D4E5F6G7H8J9K0MNPQ');++ assert.equal(out.kind, 'image');++ const r = srv.requests[0];++ assert.equal(r.method, 'POST');++ assert.equal(r.url, '/api/v1/attachments');++ assert.equal(r.headers.authorization, 'Bearer person-token');++ assert.match(r.headers['content-type'], /^multipart\/form-data; boundary=/);++ const text = r.body.toString('latin1');++ assert.match(text, /name="file"; filename="shot.png"/);++ assert.match(text, /content-type: image\/png/i);++ assert.ok(progress.length >= 1 && progress.every((p) => p.loaded <= p.total && p.total === 1), 'real XHR progress');++ await srv.close();++ }],++++ ['a getToken client supplies the upload token; a signal aborts', async () => {++ const composer = createComposerClient(createClient({ baseUrls: { media: 'http://127.0.0.1:9' }, getToken: async ({ service, audience }) => `${service}:${audience}` }));++ assert.equal(composer.MEDIA_ORIGIN, 'https://openvibe.media');++ const ctrl = new AbortController();++ const p = composer.attachments.upload(new Blob(['x']), { signal: ctrl.signal, xhr: fakeXHR({ never: true }) });++ ctrl.abort();++ await assert.rejects(p, (err) => err.code === 'sdk.aborted');++ }],++++ ['remove deletes and answers true, false when already gone', async () => {++ const srv = await stubServer((req, res) => (req.url.endsWith('/missing') ? send(res, 404, { code: 'media.attachment.not_found' }) : res.writeHead(204).end()));++ const composer = createComposerClient(createClient({ baseUrls: { media: srv.url } }), { mediaBaseUrl: srv.url });++ assert.equal(await composer.attachments.remove('att_01JAB2C3D4E5F6G7H8J9K0MNPQ'), true);++ assert.equal(await composer.attachments.remove('missing'), false);++ assert.deepEqual(srv.requests.map((r) => `${r.method} ${r.url}`), ['DELETE /api/v1/attachments/att_01JAB2C3D4E5F6G7H8J9K0MNPQ', 'DELETE /api/v1/attachments/missing']);++ await srv.close();++ }],++++ ['prefs get/put name the revision; a stale rev carries the current copy', async () => {++ const prefs = { rev: 2, send_key: 'enter', tools: [{ id: 'attach', hidden: false }], density: 'comfy', voice: true, snippets: [] };++ const srv = await stubServer((req, res, raw) => {++ if (req.method === 'GET') return send(res, 200, prefs);++ const body = JSON.parse(raw.toString());++ if (body.rev !== 2) return send(res, 409, { status: 409, code: 'composer.rev_conflict', error: 'moved', current: prefs });++ return send(res, 200, { ...body, rev: 3 });++ });++ const composer = createComposerClient(createClient({ baseUrls: { network: srv.url } }), { networkBaseUrl: srv.url });++ assert.deepEqual(await composer.composer.prefs.get(), prefs);++ const saved = await composer.composer.prefs.put({ send_key: 'mod-enter', tools: [], density: 'compact', voice: false, snippets: [] }, 2);++ assert.equal(saved.rev, 3);++ assert.equal(saved.send_key, 'mod-enter');++ const sent = JSON.parse(srv.requests[1].body.toString());++ assert.equal(srv.requests[1].method, 'PUT');++ assert.equal(sent.rev, 2);++ await assert.rejects(composer.composer.prefs.put({ send_key: 'enter', tools: [], density: 'comfy', voice: true, snippets: [] }, 1), (err) => {++ assert.equal(err.status, 409);++ assert.equal(err.code, 'composer.rev_conflict');++ assert.equal(err.problem.current.rev, 2);++ return true;++ });++ assert.throws(() => composer.composer.prefs.put({}, '2'), TypeError);++ await srv.close();++ }],++++ ['mentions searches with q and kinds and returns the list', async () => {++ const srv = await stubServer((req, res) => send(res, 200, { mentions: [++ { kind: 'person', id: 'usr_01JAB2C3D4E5F6G7H8J9K0MNPQ', label: 'Ana', handle: 'ana', avatar: null, url: 'https://openvibe.network/@ana' },++ ] }));++ const composer = createComposerClient(createClient({ baseUrls: { network: srv.url } }));++ const out = await composer.mentions.search('an a', ['person', 'repo']);++ assert.equal(out.length, 1);++ assert.equal(out[0].handle, 'ana');++ const u = new URL(`http://x${srv.requests[0].url}`);++ assert.equal(u.pathname, '/api/v1/mentions');++ assert.equal(u.searchParams.get('q'), 'an a');++ assert.equal(u.searchParams.get('kinds'), 'person,repo');++ await srv.close();++ }],++]);nnew file mode 100644@@@ -0,0 +1,21 @@++'use strict';++const assert = require('node:assert/strict');++const { createCatalogSearchClient, ExchangeError } = require('../src/exchange');++++(async () => {++ let url;++ const client = createCatalogSearchClient({ fetchImpl: async (input, init) => {++ url = new URL(input);++ assert.equal(init.headers.Authorization, undefined);++ return new Response(JSON.stringify({ q: 'fast coder', count: 0, results: [], quality_unavailable: false }), { status: 200 });++ } });++ const result = await client.search('fast coder', { max_price: 1 });++ assert.equal(result.count, 0);++ assert.equal(url.pathname, '/api/v1/search');++ assert.equal(url.searchParams.get('max_price'), '1');++ assert.equal(url.searchParams.get('q'), 'fast coder');++ await assert.rejects(client.search(null), TypeError);++ const failing = createCatalogSearchClient({ fetchImpl: async () => new Response(JSON.stringify({ error: 'catalog_unavailable' }), { status: 503 }) });++ await assert.rejects(failing.search('vision'), (err) => err instanceof ExchangeError && err.status === 503);++ process.stdout.write('Exchange catalog reader passed\n');++})().catch((err) => { process.stderr.write(String(err.stack || err)); process.exitCode = 1; });nnew file mode 100644@@@ -0,0 +1,12 @@++'use strict';++const assert = require('assert');++const { createFeedClient } = require('../src/feed');++++(async () => {++ const calls = [];++ const client = createFeedClient({ json: async (input) => { calls.push(input); return { items: [], next_cursor: null }; } }, { communityBaseUrl: 'https://community.test' });++ const result = await client.list({ kind: 'clip', cursor: 'opaque' });++ assert.deepStrictEqual(result, { items: [], next_cursor: null });++ assert.deepStrictEqual(calls[0], { service: 'community', baseUrl: 'https://community.test', audience: 'openvibe.community', path: '/api/v1/feed', query: { kind: 'clip', cursor: 'opaque' }, signal: undefined });++ console.log('feed.list sends kind and cursor to Community');++})().catch((error) => { console.error(error); process.exitCode = 1; });@@@ -10,19 +10,26 @@ const { createHomeFeedClient } = require('../src/home-feed');await ov.home.get();await ov.home.set(layout);await ov.network.today();++ await ov.network.popular({ limit: 5 });++ await ov.network.tryNext({ limit: 3 });await ov.feed.list({ for: 'me', cursor: 'next' });await ov.feed.post('Hello', { link: 'https://openvibe.live/' });await ov.feed.contribute({ id: 'update-1', text: 'New thing', url: 'https://example.test/' });assert.deepEqual(calls.map((c) => [c.service, c.method || 'GET', c.path]), [['network', 'GET', '/api/v1/me/home-screen'], ['network', 'PUT', '/api/v1/me/home-screen'],-- ['network', 'GET', '/api/v1/frame/today'], ['community', 'GET', '/api/v1/pulse'],++ ['network', 'GET', '/api/v1/frame/today'], ['network', 'GET', '/api/v1/home/popular'],++ ['network', 'GET', '/api/v1/me/try-next'], ['community', 'GET', '/api/v1/pulse'],['community', 'POST', '/api/v1/me/status'], ['community', 'POST', '/api/v1/pulse/items'],]);assert.deepEqual(calls[1].json, layout);assert.equal(calls[2].auth, false);-- assert.deepEqual(calls[3].query, { for: 'me', cursor: 'next' });-- assert.equal(calls[4].idempotencyKey, false);-- assert.equal(calls[5].idempotencyKey, false);++ assert.equal(calls[3].auth, false);++ assert.deepEqual(calls[3].query, { limit: 5 });++ assert.equal(calls[4].auth, undefined);++ assert.deepEqual(calls[4].query, { limit: 3 });++ assert.deepEqual(calls[5].query, { for: 'me', cursor: 'next' });++ assert.equal(calls[6].idempotencyKey, false);++ assert.equal(calls[7].idempotencyKey, false);const esm = await import('../esm/home-feed.mjs');assert.equal(typeof esm.createHomeFeedClient, 'function');const packageEntry = await import('openvibe-sdk/home-feed');@@@ -72,7 +72,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, 103); // user-card adds the 103rd subpath after history.++ assert.equal(subpaths.length, 109); // composer, feed and agents add the 104th-106th after user-card; the 0.148.0 train's codes, player and the rest bring 109.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,32 @@++'use strict';++const assert = require('node:assert/strict');++const { run } = require('./helpers');++const player = require('../src/player');++++run([++ ['player delegates current and handoff to the shared host', async () => {++ const old = global.window;++ try {++ global.window = { OVOverlay: { current: () => ({ id: 'live', href: 'https://openvibe.live/@test' }), handoffUrl: (url) => `${url}#ovo=card` } };++ assert.equal(player.current().id, 'live');++ assert.equal(player.handoff('https://openvibe.games/'), 'https://openvibe.games/#ovo=card');++ } finally { global.window = old; }++ }],++ ['player events can be unsubscribed', async () => {++ const old = global.window;++ const listeners = new Map();++ try {++ global.window = { addEventListener: (name, fn) => listeners.set(name, fn), removeEventListener: (name) => listeners.delete(name) };++ const values = [];++ const off = player.on((event) => values.push(event.reason));++ listeners.get('openvibe:player')({ detail: { reason: 'open' } });++ off();++ assert.deepEqual(values, ['open']);++ assert.equal(listeners.size, 0);++ } finally { global.window = old; }++ }],++ ['player ESM subpath exposes handoff', async () => {++ const esm = await import('../esm/player.mjs');++ assert.equal(typeof esm.handoff, 'function');++ }],++]);@@@ -42,7 +42,9 @@ function skipsIn(output) {function runOne(file) {return new Promise((resolve) => {const started = Date.now();-- const child = spawn(process.execPath, [path.join(__dirname, file)], {++ // PGlite's revocation schema can exhaust the default Windows V8 stack during WASM init.++ const stack = process.platform === 'win32' && file === 'revocations.test.js' ? ['--stack_size=4096'] : [];++ const child = spawn(process.execPath, [...stack, path.join(__dirname, file)], {cwd: path.join(__dirname, '..'),env: { ...process.env, NODE_ENV: 'test' },stdio: ['ignore', 'pipe', 'pipe'],nnew file mode 100644@@@ -0,0 +1,159 @@++'use strict';++/** openvibe-sdk/sso handoff: mint a ticket for a link, redeem an arrival before first paint. */++const assert = require('node:assert/strict');++const crypto = require('node:crypto');++const http = require('node:http');++const { run, waitFor } = require('./helpers');++const { createSsoClient, stripHandoffParam, HANDOFF_PARAM } = require('../src/sso');++++const keys = crypto.generateKeyPairSync('rsa', { modulusLength: 2048 });++const ISS = 'https://openvibe.network';++const now = () => Math.floor(Date.now() / 1000);++const b64 = (o) => Buffer.from(JSON.stringify(o)).toString('base64url');++const sign = (claims) => { const input = `${b64({ alg: 'RS256', typ: 'JWT', kid: 'k1' })}.${b64(claims)}`; return `${input}.${crypto.sign('RSA-SHA256', Buffer.from(input), keys.privateKey).toString('base64url')}`; };++const session = (over = {}) => sign({ sub: 7, id: 7, username: 'ana', iss: ISS, aud: ['openvibe.network', 'openvibe.trade'], iat: now(), exp: now() + 600, ...over });++++const TICKET = `ovh_${'t'.repeat(40)}`;++const USED_TICKET = `ovh_${'u'.repeat(40)}`;++const BAD_CODE_TICKET = `ovh_${'b'.repeat(40)}`;++++// A stub Network: handoff tickets + redeem, the code exchange, the JWKS.++const net = { tickets: [], redeems: [] };++const server = http.createServer((req, res) => {++ let raw = ''; req.on('data', (c) => { raw += c; });++ req.on('end', () => {++ const send = (status, body) => { res.statusCode = status; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(body)); };++ if (req.url === '/api/.well-known/jwks') return send(200, { keys: [{ ...keys.publicKey.export({ format: 'jwk' }), kid: 'k1', use: 'sig', alg: 'RS256' }] });++ const body = raw ? JSON.parse(raw) : {};++ if (req.url === '/api/v1/handoff/tickets') {++ net.tickets.push({ auth: req.headers.authorization, body });++ if (req.headers.authorization !== `Bearer ${net.session}`) return send(401, { error: 'Authentication required' });++ if (body.client_id === 'nope') return send(400, { error: 'invalid_request' });++ return send(200, { ticket: TICKET, expires_at: new Date(Date.now() + 60000).toISOString(), expires_in: 60, client_id: body.client_id });++ }++ if (req.url === '/api/v1/handoff/redeem') {++ net.redeems.push(body);++ if (body.client_secret !== 'secret') return send(401, { error: 'invalid_client' });++ if (body.ticket === TICKET && body.client_id === 'trade') return send(200, { code: 'handoff-code', expires_in: 300 });++ if (body.ticket === BAD_CODE_TICKET) return send(200, { code: 'bad-code', expires_in: 300 });++ return send(400, { error: 'invalid_grant', error_description: 'Unknown or used handoff ticket' });++ }++ if (req.url === '/oauth/token') {++ if (body.client_secret !== 'secret') return send(401, { error: 'invalid_client' });++ if (body.grant_type === 'authorization_code' && body.code === 'handoff-code') {++ return send(200, { access_token: session(), refresh_token: 'rt_handoff', token_type: 'Bearer' });++ }++ return send(400, { error: 'invalid_grant' });++ }++ return send(404, {});++ });++});++++function fakeRes() {++ const r = { cookies: {}, cleared: [], statusCode: 200 };++ r.cookie = (k, v, o) => { r.cookies[k] = { v, o }; return r; };++ r.clearCookie = (k) => { r.cleared.push(k); return r; };++ r.redirect = (u) => { r.statusCode = 302; r.location = u; return r; };++ r.status = (s) => { r.statusCode = s; return r; };++ r.json = (b) => { r.body = b; return r; };++ r.send = (b) => { r.body = b; return r; };++ r.type = (t) => { r.contentType = t; return r; };++ r.set = (k, v) => { (r.headers = r.headers || {})[k] = v; return r; };++ return r;++}++const cookieHeader = (o) => Object.entries(o).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('; ');++const silent = { warn() {}, error() {}, info() {} };++++let sso, base;++run([++ ['handoff param name and strip helper', async () => {++ assert.equal(HANDOFF_PARAM, 'ov_handoff');++ assert.equal(stripHandoffParam('/deals?a=1&ov_handoff=ovh_x#h'), '/deals?a=1#h');++ assert.equal(stripHandoffParam('/deals?ov_handoff=ovh_x'), '/deals');++ assert.equal(stripHandoffParam('/'), '/');++ assert.equal(stripHandoffParam(null), '/');++ }],++++ ['mint: 401 without a session, 400 without a destination', async () => {++ await new Promise((r) => server.listen(0, '127.0.0.1', r));++ base = `http://127.0.0.1:${server.address().port}`;++ net.session = session();++ sso = createSsoClient({ site: 'trade', baseUrl: 'https://openvibe.trade', clientId: 'trade', clientSecret: 'secret', networkUrl: ISS, networkInternalUrl: base, issuer: ISS, log: silent });++ let res = fakeRes();++ await sso.handlers.handoffMint({ headers: {}, body: { client_id: 'news', redirect_uri: 'https://openvibe.news/auth/callback' } }, res);++ assert.equal(res.statusCode, 401, 'no session mints nothing');++ res = fakeRes();++ await sso.handlers.handoffMint({ headers: { cookie: cookieHeader({ ov_token: net.session }) }, body: {} }, res);++ assert.equal(res.statusCode, 400, 'no destination is refused');++ }],++++ ['mint: forwards the person and the destination, returns the ticket', async () => {++ const res = fakeRes();++ await sso.handlers.handoffMint({ headers: { cookie: cookieHeader({ ov_token: net.session }) }, body: { client_id: 'news', redirect_uri: 'https://openvibe.news/auth/callback' } }, res);++ assert.equal(res.statusCode, 200);++ assert.equal(res.body.ticket, TICKET);++ assert.equal(res.body.expires_in, 60);++ assert.equal(net.tickets.at(-1).auth, `Bearer ${net.session}`, 'the person’s own token proves the mint');++ assert.equal(net.tickets.at(-1).body.client_id, 'news');++ }],++++ ['mint: the Network refusing or silent is a 4xx or 502, never a ticket', async () => {++ let res = fakeRes();++ await sso.handlers.handoffMint({ headers: { cookie: cookieHeader({ ov_token: net.session }) }, body: { client_id: 'nope', redirect_uri: 'https://x.example/cb' } }, res);++ assert.equal(res.statusCode, 400, 'the Network’s refusal is passed through');++ const down = createSsoClient({ site: 'trade', baseUrl: 'https://openvibe.trade', clientId: 'trade', clientSecret: 'secret', networkUrl: 'http://127.0.0.1:1', networkInternalUrl: 'http://127.0.0.1:1', issuer: ISS, log: silent, publicKey: keys.publicKey.export({ format: 'pem', type: 'spki' }) });++ res = fakeRes();++ await down.handlers.handoffMint({ headers: { cookie: cookieHeader({ ov_token: net.session }) }, body: { client_id: 'news', redirect_uri: 'https://openvibe.news/auth/callback' } }, res);++ assert.equal(res.statusCode, 502, 'an unreachable Network is a 502');++ }],++++ ['arrival: no ticket, a bad ticket, a guest, or a live session all pass through untouched', async () => {++ const before = net.redeems.length;++ for (const req of [++ { method: 'GET', query: {}, headers: {}, url: '/deals' },++ { method: 'GET', query: { ov_handoff: 'not-a-ticket' }, headers: {}, url: '/deals?ov_handoff=not-a-ticket' },++ { method: 'GET', query: { ov_handoff: TICKET }, headers: { cookie: cookieHeader({ ov_sso_hint: 'guest' }) }, url: '/' },++ { method: 'GET', query: { ov_handoff: TICKET }, headers: { cookie: cookieHeader({ ov_token: net.session }) }, url: '/' },++ { method: 'POST', query: { ov_handoff: TICKET }, headers: {}, url: '/' },++ ]) {++ let nexted = false;++ const res = fakeRes();++ sso.handoff()(req, res, () => { nexted = true; });++ await waitFor(() => nexted || res.location !== undefined, { timeoutMs: 30000 });++ assert.ok(nexted, `passes through: ${req.url} ${JSON.stringify(req.headers)}`);++ assert.equal(res.location, undefined, 'no redirect when passing through');++ }++ assert.equal(net.redeems.length, before, 'no redeem call for any pass-through');++ }],++++ ['arrival: a good ticket signs the browser in and scrubs the address bar', async () => {++ const res = fakeRes();++ let nexted = false;++ sso.handoff()({ method: 'GET', query: { ov_handoff: TICKET }, headers: {}, url: '/deals?a=1&ov_handoff=xx' }, res, () => { nexted = true; });++ await waitFor(() => nexted || res.location !== undefined, { timeoutMs: 30000 });++ assert.equal(nexted, false);++ assert.equal(res.statusCode, 302);++ assert.equal(res.location, '/deals?a=1', 'the ticket leaves the address bar, the rest stays');++ assert.ok(res.cookies.ov_token && res.cookies.ov_token.v.length > 100, 'a session is set');++ assert.equal(res.cookies.ov_sso_hint.v, 'account');++ assert.equal(net.redeems.at(-1).client_id, 'trade', 'redeemed as this site’s own client');++ }],++++ ['arrival: a refused or broken handoff renders as a guest on a clean URL', async () => {++ for (const ticket of [USED_TICKET, BAD_CODE_TICKET]) {++ const res = fakeRes();++ let nexted = false;++ sso.handoff()({ method: 'GET', query: { ov_handoff: ticket }, headers: {}, url: `/?ov_handoff=${ticket}` }, res, () => { nexted = true; });++ await waitFor(() => nexted || res.location !== undefined, { timeoutMs: 30000 });++ assert.equal(nexted, false);++ assert.equal(res.location, '/', 'the ticket is scrubbed even on failure');++ assert.equal(res.cookies.ov_token, undefined, 'no session on failure');++ }++ server.close();++ }],++ ['the ESM subpath exports the public handoff helpers', async () => {++ const esm = await import('../esm/sso.mjs');++ assert.equal(esm.HANDOFF_PARAM, 'ov_handoff');++ assert.equal(esm.stripHandoffParam('/deals?ov_handoff=ovh_x&sort=top#list'), '/deals?sort=top#list');++ }],++]);@@@ -5,14 +5,33 @@ const { createWalletClient } = require('../src/wallet');(async () => {const calls = []; let retry = true;-- const client = createClient({ token: 'person-token', baseUrls: { billing: 'https://billing.test', inventory: 'https://inventory.test' },++ const client = createClient({ token: 'person-token', baseUrls: { billing: 'https://billing.test', inventory: 'https://inventory.test', network: 'https://network.test' },credentials: 'include', retries: 1, retryDelayMs: 1, fetch: async (url, init) => {calls.push({ url, ...init, json: init.body ? JSON.parse(init.body) : null });if (retry && url.endsWith('/buy')) { retry = false; return new Response(JSON.stringify({ detail: 'retry' }), { status: 503 }); }++ if (url.includes('/api/wallet/changes?')) return new Response(JSON.stringify({++ vibes: { net: 12, parts: { earned: 12, spent: 0, received: 0, sent: 0 }, moves: [{ id: 3, delta: 12, part: 'earned' }], cursor: 3 },++ opencoins: { net: -4, parts: { earned: 0, spent: -4, received: 0, sent: 0 }, moves: [{ id: 8, delta: -4, part: 'spent' }], cursor: 8 },++ channel_points: null, state: { vibes: 'open', opencoins: 'open', channel_points: 'needs_streamer' },++ }), { status: 200, headers: { 'Content-Type': 'application/json' } });return new Response(JSON.stringify({ ok: true }), { status: 200, headers: { 'Content-Type': 'application/json' } });} });const ov = createWalletClient(client);await ov.wallet.get();++ const vibesCursor = 'txn_' + '0'.repeat(25) + '1';++ const changes = await ov.wallet.changes({ since_vibes: vibesCursor, since_opencoins: 7 });++ assert.strictEqual(changes.vibes.net, 12);++ assert.strictEqual(changes.opencoins.net, -4);++ assert.strictEqual(changes.state.channel_points, 'needs_streamer');++ assert(calls.at(-1).url.includes(`since_vibes=${vibesCursor}`));++ assert(calls.at(-1).url.includes('since_opencoins=7'));++ assert(!calls.at(-1).url.includes('subject='));++ await ov.wallet.changes({ streamer: 9, since_channel_points: 21 });++ assert(calls.at(-1).url.includes('since_channel_points=21'));++ await ov.wallet.markRead({ currency: 'channel-points', streamer: 9, cursor: 22 });++ assert.deepStrictEqual(calls.at(-1).json, { currency: 'channel-points', streamer: 9, cursor: 22 });++ await ov.wallet.markRead({ currency: 'vibes', cursor: vibesCursor });++ assert.deepStrictEqual(calls.at(-1).json, { currency: 'vibes', cursor: vibesCursor });await ov.wallet.topUp({ provider: 'stripe', amount_cents: 1000, idempotency_key: 'topup-test-01', subject: 'ignored' });await ov.vibes.buy({ provider: 'stripe', vibes: 100, idempotency_key: 'vibes-test-01', amount_cents: 1 });await ov.store.list();@@@ -34,5 +53,8 @@ const { createWalletClient } = require('../src/wallet');assert.throws(() => ov.store.buy(id, { currency: 'USD', idempotency_key: 'safe-key-01' }));assert.throws(() => ov.vibes.buy({ provider: 'stripe', vibes: 1.5, idempotency_key: 'safe-key-01' }));assert.throws(() => ov.wallet.topUp({ provider: 'stripe', amount_cents: 1000 }));++ assert.throws(() => ov.wallet.changes({ streamer: -1 }));++ assert.throws(() => ov.wallet.changes({ since_vibes: 'someone-else' }));++ assert.throws(() => ov.wallet.markRead({ currency: 'channel-points', cursor: 22 }));console.log('wallet facade: routes, personal bearer, no cookies, stable retry keys and no price/subject injection');})().catch((e) => { console.error(e); process.exitCode = 1; });@@@ -44,6 +44,7 @@ export interface ActorClient {list(opts?: Opts): Promise<{ agents: ActorJson[] }>;create(agent: Record<string, unknown>, opts?: Opts): Promise<ActorJson>;get(id: string, opts?: Opts): Promise<ActorJson>;++ sheet(id: string, opts?: Opts): Promise<import('./agent-sheet').AgentSheet>;update(id: string, patch: Record<string, unknown>, opts?: Opts): Promise<ActorJson>;remove(id: string, opts?: Opts): Promise<ActorJson>;run(id: string, req?: { input?: string }, opts?: Opts): Promise<ActorJson>;nnew file mode 100644@@@ -0,0 +1,24 @@++import type { OpenVibeClient } from './core';++export interface AgentSheet {++ agent: { id: string; name: string; face: string | null; state: string; [key: string]: unknown };++ now: Record<string, unknown> | null;++ work: Array<Record<string, unknown>>;++ asks: Array<Record<string, unknown>>;++ asks_empty: string | null;++ computer: Record<string, unknown> | null;++ schedules: Array<Record<string, unknown>>;++ memory: Array<Record<string, unknown>>;++ activity: Array<Record<string, unknown>>;++}++export interface AgentSheetClient {++ sheet(agentId: string, options?: { signal?: AbortSignal }): Promise<AgentSheet>;++ codesSheet(harnessId: string, options?: { signal?: AbortSignal }): Promise<AgentSheet>;++ pause(agentId: string, options?: { signal?: AbortSignal }): Promise<Record<string, unknown>>;++ resume(agentId: string, options?: { signal?: AbortSignal }): Promise<Record<string, unknown>>;++ memory: {++ add(agentId: string, text: string, options?: { signal?: AbortSignal }): Promise<Record<string, unknown>>;++ update(agentId: string, memoryId: string, text: string, options?: { signal?: AbortSignal }): Promise<Record<string, unknown>>;++ remove(agentId: string, memoryId: string, options?: { signal?: AbortSignal }): Promise<unknown>;++ };++}++export function createAgentSheetClient(client: OpenVibeClient, options?: { baseUrl?: string }): AgentSheetClient;nnew file mode 100644@@@ -0,0 +1,8 @@++/** AG-UI 1.0 event stream and agent.run@1 snapshot. */++export interface AgentSource { url: string; title: string; site: string }++export interface AgentUiEvent { type: string; timestamp: string; threadId?: string; runId?: string; metadata?: { ov?: { title?: string; kind?: string; sources?: AgentSource[]; attachments?: unknown[]; cost_usd?: number | null; needs_you?: object | null; product?: string; ref?: string | null } }; [field: string]: unknown }++export interface AgentRun { id: string; product: string; title: string; status: 'queued'|'running'|'waiting'|'needs_you'|'succeeded'|'failed'|'stopped'|'cancelled'; plan: Array<{title:string;status:string}>; steps: Array<Record<string, unknown>>; sources: AgentSource[]; result: Record<string, unknown>|null; started_at: string; ended_at: string|null; cost_usd: number|null }++export interface AgentApproval { id?: string; action?: string; [field: string]: unknown }++export interface AgentEvent { id: string|null; event: string|null; data: AgentUiEvent|null }++export interface AgentsClient { run(id: string, options?: {signal?: AbortSignal}): Promise<AgentRun|null>; events(id: string, options?: {lastEventId?: string|number; signal?: AbortSignal; maxReconnects?: number; reconnectDelayMs?: number}): AsyncIterable<AgentEvent>; post(id: string, approval: AgentApproval, options?: {signal?: AbortSignal}): Promise<unknown> }++export function createAgentsClient(client: unknown, options?: {baseUrl?: string; service?: string; audience?: string}): AgentsClient;@@@ -1,15 +1,19 @@import * as userCard from './user-card';++import * as player from './player';++import * as agentSheet from './agent-sheet';/** browser/openvibe-sdk.mjs: one self-contained ES module with the browser-safe subpaths (same shape as the package's browser entry). */import * as auth from './auth-browser';import * as registry from './registry';import * as modules from './modules';import * as realtime from './realtime';import * as media from './media';++import * as composer from './composer';import * as cam from './cam';import * as mediaUpload from './media-upload';import * as ui from './ui';import * as community from './community';import * as homeFeed from './home-feed';++import * as feed from './feed';import * as bot from './bot';import * as search from './search';import * as bench from './bench';@@@ -22,6 +26,7 @@ import * as chat from './chat';import * as ai from './ai';import * as credentials from './credentials';import * as jobs from './jobs';++import * as agents from './agents';import * as tools from './tools';import * as projects from './projects';import * as geo from './geo';@@@ -33,16 +38,19 @@ import * as core from './core';export const createWalletClient: typeof import('./wallet').createWalletClient;export * from './core';++export * as agentSheet from './agent-sheet';export * as auth from './auth-browser';export * as registry from './registry';export * as modules from './modules';export * as realtime from './realtime';export * as media from './media';++export * as composer from './composer';export * as cam from './cam';export * as mediaUpload from './media-upload';export * as ui from './ui';export * as community from './community';export * as homeFeed from './home-feed';++export * as feed from './feed';export * as bot from './bot';export * as search from './search';export * as bench from './bench';@@@ -55,6 +63,7 @@ export * as chat from './chat';export * as ai from './ai';export * as credentials from './credentials';export * as jobs from './jobs';++export * as agents from './agents';export * as tools from './tools';export * as projects from './projects';export * as geo from './geo';@@@ -64,11 +73,13 @@ export * as trust from './trust';export * as levels from './levels';export * as userCard from './user-card';++export * as player from './player';declare const sdk: typeof core & {createWalletClient: typeof createWalletClient;userCard: typeof userCard;++ player: typeof player;auth: typeof auth; registry: typeof registry; modules: typeof modules; realtime: typeof realtime;-- media: typeof media; cam: typeof cam; mediaUpload: typeof mediaUpload; ui: typeof ui; community: typeof community; homeFeed: typeof homeFeed; bot: typeof bot; search: typeof search; bench: typeof bench; meme: typeof meme; vision: typeof vision; themes: typeof themes; design: typeof design; redact: typeof redact; chat: typeof chat; ai: typeof ai; credentials: typeof credentials; jobs: typeof jobs; tools: typeof tools; projects: typeof projects; geo: typeof geo; history: typeof history; locker: typeof locker; trust: typeof trust; levels: typeof levels;++ media: typeof media; composer: typeof composer; cam: typeof cam; mediaUpload: typeof mediaUpload; ui: typeof ui; community: typeof community; homeFeed: typeof homeFeed; feed: typeof feed; bot: typeof bot; search: typeof search; bench: typeof bench; meme: typeof meme; vision: typeof vision; themes: typeof themes; design: typeof design; redact: typeof redact; chat: typeof chat; ai: typeof ai; credentials: typeof credentials; jobs: typeof jobs; agents: typeof agents; tools: typeof tools; projects: typeof projects; geo: typeof geo; history: typeof history; locker: typeof locker; trust: typeof trust; levels: typeof levels;};export default sdk;@@@ -93,11 +93,30 @@ export interface RoomMemory {can_edit: boolean;me: string | null;}++/** chat.person-memory-result@1. Private fields appear only for the person and channel owner. */++export interface PersonMemoryResult {++ username: string;++ level: 'day' | 'week' | 'month';++ channel: string | null;++ opted_out: boolean;++ enabled: boolean;++ entries: {++ level: 'day' | 'week' | 'month'; period_start: string; channel_user_id: number;++ summary: string; topics: string[];++ highlights: { detail: string; stream_id: number | null; at: string | null }[];++ message_count: number; tone?: string; catchphrases?: string[]; people?: string[];++ }[];++}export interface ChatClient {/** The typed address previews a message carries (chat.embed@1): [] when it has none. */embeds(message?: { embeds?: unknown } | null): ChatEmbed[];history: ChatHistoryHelper;++ people: {++ memory(username: string, query?: { channel?: string; level?: 'day' | 'week' | 'month' }, opts?: Opts): Promise<PersonMemoryResult>;++ forget(username: string, opts?: Opts): Promise<{ deleted: boolean; opted_out: boolean }>;++ preferences(username: string, optedOut: boolean, opts?: Opts): Promise<{ opted_out: boolean }>;++ };/** Ask Vibe using Auto, one catalogue model, or two to three models side by side. */ask(input: { message: string; models?: string[]; slug?: string; fresh?: boolean; system_prompt?: string; temperature?: number }, opts?: Opts): Promise<{ chat: Json; message: Json }>;pick(messageId: number, model: string, opts?: Opts): Promise<{ chat: Json; model: string }>;nnew file mode 100644@@@ -0,0 +1,20 @@++/** openvibe-sdk/codes: OpenVibe.Codes — the person's coding tasks as the Dock's Code tab lists, starts and stops them. */++type Opts = { signal?: AbortSignal };++type CodesJson = Record<string, any>;++export type CodesTaskMode = 'create' | 'openvibe' | 'repo';++export type CodesTaskStatus = 'queued' | 'running' | 'needs-you' | 'done' | 'failed' | 'stopped';++export interface CodesTaskSummary {++ id: string; title: string; status: CodesTaskStatus; mode: CodesTaskMode; repo: string;++ started_at: string; updated_at: string; steps: number; url: string; preview_url: string | null; pr_url: string | null;++}++export interface CodesTaskCreate {++ text: string; mode: CodesTaskMode; repo?: string; attachments?: string[];++}++export interface CodesClient {++ tasks: {++ list(opts?: Opts & { limit?: number }): Promise<{ tasks: CodesTaskSummary[] }>;++ create(req: CodesTaskCreate, opts?: Opts): Promise<{ id: string | null; url: string }>;++ stop(id: string, opts?: Opts): Promise<{ ok: true }>;++ };++}++export declare function createCodesClient(client: CodesJson, opts?: { baseUrl?: string }): { tasks: CodesClient['tasks'] };@@@ -12,7 +12,7 @@ export type CallOptions = ActingOptions & { signal?: AbortSignal };export interface CommentQuery { after?: string | number; sort?: 'old' | 'new'; limit?: number; parent?: string | number; }export type CommentAttachment = AiFile & { url?: string };--export interface Comment { id: number; thread_id: number | string; parent_id: number | null; message: string | null; reply_count?: number | null; replies?: Comment[]; attachments?: CommentAttachment[]; [field: string]: unknown; }++export interface Comment { id: number; thread_id: number | string; parent_id: number | null; reply_to_id: number | null; reply_to: { id: number; display_name: string } | null; message: string | null; reply_count?: number | null; replies?: Comment[]; attachments?: CommentAttachment[]; [field: string]: unknown; }export interface CommentPage { thread: Record<string, unknown>; comments: Comment[]; next_cursor: string | null; viewer: Record<string, boolean>; }export interface CommentsApi {resolve(ref: EntityRef, opts?: CallOptions): Promise<{ thread: Record<string, unknown>; created: boolean }>;@@@ -60,7 +60,7 @@ export interface FeedItem {site: string;site_name: string;actor: { name: string | null; avatar: string | null } | null;-- media: { thumb: string; ratio: number; length?: number } | null;++ media: { thumb: string; preview?: string; ratio: number; length?: number } | null;counts: { views?: number; likes?: number; comments?: number };/** True while on air; false on an ended stream. Absent for other kinds. */live_now?: boolean;@@@ -76,10 +76,14 @@ export interface PostsApi {export interface CommunityClient {pastes: PastesApi;comments: CommentsApi;-- feed(query?: FeedQuery, opts?: CallOptions): Promise<{ items: FeedItem[]; next_cursor: string | null }>;++ feed: FeedReader;posts: PostsApi;as(who: string | ActingOptions): CommunityClient;headers(): Record<string, string>;}++export interface FeedReader {++ (query?: FeedQuery, opts?: CallOptions): Promise<{ items: FeedItem[]; next_cursor: string | null }>;++ list(query?: Pick<FeedQuery, 'kind' | 'cursor'>, opts?: CallOptions): Promise<{ items: FeedItem[]; next_cursor: string | null }>;++}export declare function createCommunityClient(client: OpenVibeClient, defaults?: ActingOptions & { baseUrl?: string }): CommunityClient;export declare function actingHeaders(opts?: ActingOptions): Record<string, string>;nnew file mode 100644@@@ -0,0 +1,75 @@++import type { OpenVibeClient } from './core';++++/** composer.attachment@1: one file a person attached to a message. */++export interface ComposerAttachment {++ id: string;++ kind: 'image' | 'video' | 'audio' | 'file' | 'code';++ name: string;++ size: number;++ mime: string;++ url: string;++ thumb?: string | null;++ w?: number | null;++ h?: number | null;++ expires_at: string;++}++++/** composer.prefs@1: a person's composer settings on every site. */++export interface ComposerPrefs {++ /** The revision that was read; a PUT names it, and a moved one is 409 composer.rev_conflict. */++ rev: number;++ send_key: 'enter' | 'mod-enter';++ tools: Array<{ id: string; hidden: boolean }>;++ density: 'comfy' | 'compact';++ voice: boolean;++ snippets: Array<{ trigger: string; label: string; text: string }>;++}++++/** The document a PUT sends: the fields, with rev = the revision it read (0 for a new person). */++export type ComposerPrefsInput = Omit<ComposerPrefs, 'rev'>;++++/** composer.mention@1: one entry of the composer's @ menu. */++export interface ComposerMention {++ kind: 'person' | 'repo' | 'site' | 'file';++ id: string;++ label: string;++ handle?: string;++ avatar?: string | null;++ url: string;++}++++export type ComposerFile = Blob | ArrayBuffer | ArrayBufferView | string;++++export interface UploadOptions {++ onProgress?: (p: { loaded: number; total: number; lengthComputable: boolean }) => void;++ signal?: AbortSignal;++ filename?: string;++ contentType?: string;++ /** An XMLHttpRequest implementation, for a non-browser shell or a test. */++ xhr?: new () => XMLHttpRequest;++}++++export interface ComposerClient {++ attachments: {++ upload(file: ComposerFile, opts?: UploadOptions): Promise<ComposerAttachment>;++ /** true when it was deleted, false when it was already gone. */++ remove(id: string): Promise<boolean>;++ };++ composer: {++ prefs: {++ get(): Promise<ComposerPrefs>;++ /** A stale rev rejects with an OpenVibeError: status 409, code composer.rev_conflict, err.problem.current. */++ put(prefs: ComposerPrefsInput, rev: number): Promise<ComposerPrefs>;++ };++ };++ mentions: {++ search(q?: string, kinds?: Array<'person' | 'repo' | 'site' | 'file'> | string): Promise<ComposerMention[]>;++ };++ MEDIA_ORIGIN: string;++ NETWORK_ORIGIN: string;++}++++export declare function createComposerClient(client: OpenVibeClient, opts?: { mediaBaseUrl?: string; networkBaseUrl?: string }): ComposerClient;++export declare const MEDIA_ORIGIN: string;++export declare const NETWORK_ORIGIN: string;++export declare const ATTACHMENT_MAX: number;@@@ -1969,9 +1969,21 @@ export interface ServiceManifest {statusApi?: string;};/**-- * The room's launcher group in OpenVibe.Shared rooms.js and OpenVibe.Network frame/sites.js; optional so older manifests retain their meaning.++ * The site's launcher group: the eight current groups are watch, talk, play, read, shop, tools, make and build (OpenVibe.Network frame/sites.js GROUPS), whose order and labels the menu, the footer and the home pages show (OpenVibe.Shared rooms.js repeats the ids for its rooms). The pre-0.400.0 ids streams, games, answers and people stay accepted for older manifests.*/-- neighbourhood?: "streams" | "games" | "answers" | "make" | "shop" | "people" | "build";++ neighbourhood?:++ | "watch"++ | "talk"++ | "play"++ | "read"++ | "shop"++ | "tools"++ | "make"++ | "build"++ | "streams"++ | "games"++ | "answers"++ | "people";/*** This site's own items on the network's footer strip, read with the rest of it at openvibe.network GET /api/frame/online?site=<id>: a label, an icon from the shared glyph set, an https link on an OpenVibe domain (any openvibe.* host), and either a count the site reports or a fixed line. Network checks every field again before it reaches a page.*@@@ -333,8 +333,12 @@ export type ExchangeEarnings = {};export type ExchangeKey = { id: string; name: string; prefix: string; monthly_cap_usd: number; allowed_models: string[]; created_at: string; revoked_at: string | null };--export type ExchangeSearchOptions = { engine?: string; limit?: number; license?: string; commercial?: boolean; format?: string; orientation?: string; size?: string; style?: 'line' | 'solid' | 'any'; set?: string; signal?: AbortSignal };--export type ExchangeSearchResult = { results: Array<{ title: string; url: string; snippet: string; engine: string; published: string | null; thumbnail?: string | null; license?: string | null; author?: string | null; source_page?: string | null; attribution?: string | null }>; receipt: { engine: string; cost_usd: number; time_ms: number } };++export type ExchangeSearchOptions = { engine?: string; limit?: number; license?: string; commercial?: boolean; format?: string; orientation?: string; size?: 'small' | 'medium' | 'large'; color?: 'black_and_white' | 'black' | 'white' | 'yellow' | 'orange' | 'red' | 'purple' | 'magenta' | 'green' | 'teal' | 'blue'; type?: 'icon' | 'photo' | 'illustration' | 'vector'; after?: string; safe_search?: 'strict'; style?: 'line' | 'solid' | 'any'; set?: string; signal?: AbortSignal };++export interface CatalogSearchFilters { provider?: string; capability?: string; min_price?: number; max_price?: number; max_speed_ms?: number; sort?: 'relevance' | 'price' | 'speed' | 'quality'; rating_kind?: string; }++export interface CatalogModelResult { model: string; name: string; family: string | null; capabilities: Record<string, boolean>; provider: string; price: { in: number; out: number; cached?: number; per?: string; currency?: string } | null; speed: { p50_ms?: number; p95_ms?: number; tokens_per_second?: number } | null; quality: { elo: number; games: number; kind: string } | null; offers: number; task_match: string | null; score: number; }++export interface CatalogSearchResponse { q: string; count: number; results: CatalogModelResult[]; quality_unavailable: boolean; }++export declare function createCatalogSearchClient(options?: { baseUrl?: string; fetchImpl?: typeof fetch }): { search(q: string, filters?: CatalogSearchFilters, options?: { signal?: AbortSignal }): Promise<CatalogSearchResponse> };++export type ExchangeSearchResult = { results: Array<{ title: string; url: string; snippet: string; engine: string; published: string | null; indexed_on?: string | null; type?: string | null; thumbnail?: string | null; license?: string | null; license_url?: string | null; author?: string | null; author_url?: string | null; source_page?: string | null; attribution?: string | null; width?: number | null; height?: number | null }>; receipt: { engine: string; cost_usd: number; time_ms: number } };export type ExchangeClient = {keys: {list(opts?: { signal?: AbortSignal }): Promise<{ keys: ExchangeKey[] }>;nnew file mode 100644@@@ -0,0 +1,7 @@++import type { CallOptions } from './core';++import type { FeedItem, FeedKind } from './community';++++export interface FeedClient {++ list(query?: { kind?: FeedKind; cursor?: string }, opts?: CallOptions): Promise<{ items: FeedItem[]; next_cursor: string | null }>;++}++export declare function createFeedClient(client: import('./core').OpenVibeClient, options?: { communityBaseUrl?: string }): FeedClient;@@@ -4,9 +4,45 @@ export interface HomeLayout { home: string[]; folders: Array<{ id: string; name:export interface TodayPoint { day: string; people: number | null; streams: number | null; posts: number | null; new_videos: number | null; }export interface Today { day: string; people_today: number | null; streams_today: number | null; posts_today: number | null; new_videos_today: number | null; series: TodayPoint[]; updated_at: string; }export interface FeedItem { id: number; source: { service: string; type: string; id: string }; title: string; url: string; label: string | null; via?: string; picture_url?: string; actor: Record<string, unknown> | null; occurred_at: string | null; }++/** One destination in the signed-out "things people do now" list and in a person's Try next: a public label, an++ * https address, an icon id and the one-line reason it is there, with the Report numbers behind them when known. */++export interface PopularItem {++ id: string;++ kind: 'site' | 'tool';++ name: string;++ url: string;++ icon: string;++ reason: string;++ score?: number;++ opens_24h?: number | null;++ opens_7d?: number | null;++ opens_prev_7d?: number | null;++ growth_7d_pct?: number | null;++ uses_24h?: number | null;++ uses_7d?: number | null;++}++export interface Popular {++ source: 'report' | 'fallback';++ reason: string;++ generated_at: string | null;++ fetched_at: string | null;++ stale: boolean;++ items: PopularItem[];++ error?: string;++}++export interface TryNext {++ source: 'report' | 'fallback';++ basis: 'co-usage' | 'popular' | 'starter';++ generated_at: string;++ items: PopularItem[];++}export interface HomeFeedClient {home: { get(opts?: { signal?: AbortSignal }): Promise<HomeLayout>; set(layout: HomeLayout, opts?: { signal?: AbortSignal }): Promise<HomeLayout> };-- network: { today(opts?: { signal?: AbortSignal }): Promise<Today> };++ network: {++ today(opts?: { signal?: AbortSignal }): Promise<Today>;++ popular(opts?: { limit?: number; signal?: AbortSignal }): Promise<Popular>;++ tryNext(opts?: { limit?: number; signal?: AbortSignal }): Promise<TryNext>;++ };feed: {list(opts?: { for?: 'me'; cursor?: string; signal?: AbortSignal }): Promise<{ items: FeedItem[]; next_cursor: string | null }>;post(text: string, opts?: { link?: string; signal?: AbortSignal }): Promise<{ id: number; post_id: number; text: string; url: string; created_at: string }>;@@@ -15,6 +15,7 @@ export * as mediaUpload from './media-upload';export * as ui from './ui';export * as community from './community';export * as homeFeed from './home-feed';++export * as feed from './feed';export * as bot from './bot';export * as search from './search';export * as bench from './bench';@@@ -27,6 +28,7 @@ export * as chatApps from './chat-apps';export * as ai from './ai';export * as credentials from './credentials';export * as jobs from './jobs';++export * as agents from './agents';export * as tools from './tools';export * as projects from './projects';export * as resources from './resources';@@@ -48,6 +50,7 @@ export * as host from './host';export * as news from './news';export * as food from './food';export * as actor from './actor';++export * as codes from './codes';export * as run from './run';export * as video from './video';export * as pics from './pics';@@@ -69,5 +72,7 @@ export * as locker from './locker';export const createWalletClient: typeof import('./wallet').createWalletClient;export * as levels from './levels';--++export * as composer from './composer';export * as userCard from './user-card';++export * as player from './player';++export * as agentSheet from './agent-sheet';nnew file mode 100644@@@ -0,0 +1,17 @@++export interface CurrentPlayer {++ id: string;++ src: string;++ title: string;++ href?: string;++ live: boolean;++ sound: boolean;++ playing?: boolean;++ side?: { src: string; title: string };++}++export interface PlayerEvent { reason: 'open' | 'close' | 'declare' | 'handoff' | 'state'; card: CurrentPlayer | null }++/** The active portable live stream, if one exists on this page. */++export function current(): CurrentPlayer | null;++/** Add the active player to an OpenVibe destination URL for same-tab navigation. */++export function handoff(url: string): string;++/** Subscribe to changes. Returns a function that removes the listener. */++export function on(listener: (event: PlayerEvent) => void): () => void;@@@ -41,9 +41,11 @@ export interface SsoClientOptions {export type SsoUser = Omit<UserTokenClaims, 'iat' | 'exp' | 'aud' | 'iss' | 'nbf' | 'jti'> & Record<string, unknown>;export interface SsoClient {-- /** GET /login /callback /logout /me /health, POST /fedcm /refresh; mount at /auth. */++ /** GET /login /callback /logout /me /health, POST /fedcm /refresh /handoff; mount at /auth. */router(express: any): any;-- handlers: { login: Handler; callback: Handler; fedcm: Handler; logout: Handler; me: Handler; refresh: Handler; health: Handler };++ handlers: { login: Handler; callback: Handler; fedcm: Handler; logout: Handler; me: Handler; refresh: Handler; health: Handler; handoffMint: Handler };++ /** Redeems a single-use cross-site ticket before the page renders; never replaces an existing session. */++ handoff(): (req: Req, res: Res, next: Next) => void;/** Sets req.user and req.token when a valid session is presented. */optionalAuth(): (req: Req, res: Res, next: Next) => void;/** 401 JSON without a valid session. */@@@ -64,6 +66,8 @@ export declare function createSsoClient(opts: SsoClientOptions): SsoClient;/** A same-site path, this site's https origin or the Network's; anything else (control characters, backslashes) is '/'. */export declare function sanitizeNext(next: unknown, opts?: { baseUrl?: string; networkUrl?: string }): string;export declare function withParam(target: string, key: string, value: string): string;++/** Remove the one-time handoff ticket from a path+query URL, preserving all other parameters and the fragment. */++export declare function stripHandoffParam(target: string): string;/** The self-contained page a sign-in that cannot finish shows (text is escaped). */export declare function failurePage(o: { siteName: string; title: string; text: string; retryHref: string; backHref: string }): string;/** The payload of a JWT, unverified; null when it does not parse. */@@@ -72,3 +76,4 @@ export declare function fedcmNonceMatches(token: string, nonce: string): booleanexport declare function claimsToUser(claims: Record<string, any> | null): SsoUser | null;export declare function parseCookies(req: { headers?: Record<string, any> }): Record<string, string>;export declare const COOKIES: Readonly<{ access: string; refresh: string; hint: string }>;++export declare const HANDOFF_PARAM: 'ov_handoff';@@@ -10,9 +10,14 @@ export interface Wallet {export interface StoreItem { id: string; name: string; description: string; issuer: string; prices: { coins?: number; vibes?: number }; preview: { emoji?: string; token?: string; media_id?: string }; kind: string; slots: string[]; surfaces: Array<{ surface: string; renderer?: string }>; owned: boolean; available: boolean; }export interface Purchase { id: string; subject: string; item_id: string; name: string; issuer: string; currency: 'coins' | 'vibes'; amount: number; status: 'pending' | 'fulfilled'; instance_id: string | null; payment_id: string | null; }export interface Checkout { intent: Record<string, unknown>; checkout_url: string | null; }++export type BalanceCurrency = 'vibes' | 'opencoins' | 'channel-points';++export type BalancePart = 'earned' | 'spent' | 'received' | 'sent';++export interface BalanceMove { id: string | number; at: string; delta: number; part: BalancePart; label: string; detail?: string; reason?: string | null; reason_key?: string; subject?: { name: string; url: string } | null; streamer_id?: number; }++export interface BalanceChanges { currency: BalanceCurrency; balance: number | Record<string, number> | null; since: string | number | null; moves: BalanceMove[]; parts: Record<BalancePart, number>; net: number; cursor: string | number | null; next_since: string | number | null; read_cursor: string | number | null; streamer?: number; truncated?: boolean; }++export interface WalletChanges { vibes: BalanceChanges | null; opencoins: BalanceChanges | null; channel_points: BalanceChanges | null; state: { vibes: 'open' | 'unavailable' | 'not_authoritative'; opencoins: 'open' | 'unavailable'; channel_points: 'open' | 'unavailable' | 'unlinked' | 'needs_streamer' }; }type Options = { signal?: AbortSignal };--export function createWalletClient(client: OpenVibeClient, options?: { billingUrl?: string; inventoryUrl?: string }): {-- wallet: { get(options?: Options): Promise<Wallet>; topUp(input: { provider: string; amount_cents: number; idempotency_key: string }, options?: Options): Promise<Checkout> };++export function createWalletClient(client: OpenVibeClient, options?: { billingUrl?: string; inventoryUrl?: string; networkUrl?: string }): {++ wallet: { get(options?: Options): Promise<Wallet>; changes(options?: Options & { since_vibes?: string; since_opencoins?: number; since_channel_points?: number; streamer?: number; limit?: number }): Promise<WalletChanges>; markRead(input: { currency: BalanceCurrency; cursor: string | number; streamer?: number }, options?: Options): Promise<{ currency: BalanceCurrency; read_cursor: string | number; streamer?: number }>; topUp(input: { provider: string; amount_cents: number; idempotency_key: string }, options?: Options): Promise<Checkout> };vibes: { buy(input: { provider: string; vibes: number; idempotency_key: string }, options?: Options): Promise<Checkout> };store: { list(options?: Options): Promise<{ items: StoreItem[] }>; buy(id: string, input: { currency: 'coins' | 'vibes'; idempotency_key: string }, options?: Options): Promise<Purchase>; submit(id: string, prices: { coins?: number; vibes?: number }, options?: Options): Promise<{ definition_id: string; issuer: string; prices: { coins?: number; vibes?: number }; status: 'in_review' }> };items: { list(options?: Options & { cursor?: string; limit?: number; kind?: string }): Promise<{ subject: string; instances: import('./inventory').Instance[]; definitions: Record<string, import('./inventory').Definition>; next_cursor: string | null }>; equip(input: { kind: string; slot: string; instance_id: string | null }, options?: Options): Promise<import('./inventory').EquippedSet> };
@@ -1,5 +1,28 @@
@@ -1,5 +1,28 @@
# Changelog
# Changelog
+## 0.148.0 — 2026-11-11
+
+- #208: add `openvibe-sdk/composer`, the shared composer's client: `attachments.upload(file, { onProgress, signal })` (multipart through `XMLHttpRequest`, real browser progress), `attachments.remove(id)`, `composer.prefs.get()/put(prefs, rev)` and `mentions.search(q, kinds)`, typed by `composer.attachment@1`, `composer.prefs@1` and `composer.mention@1`.
+- #210: add `feed.list({ kind, cursor })` for the cross-site Community Feed, keep the existing Community reader callable, and type preview video media.
+- #211: add the `openvibe-sdk/sso` one-time cross-site sign-in handoff: `POST /auth/handoff` mints a 60 s ticket and `sso.handoff()` arrival middleware redeems it before the page renders.
+- #212: add `agents` SDK methods to fetch run snapshots, follow resumable AG-UI event streams and post approvals.
+- #213: add `wallet.changes()` for the owner-scoped Vibes, OpenCoins and channel-points gain and loss window, and
+ `wallet.markRead()` for each currency's own read cursor.
+- #214: add browser-safe `player.current()`, `player.handoff(url)` and `player.on(listener)` for a live stream carried
+ between OpenVibe sites by Shared's floating player.
+- #215: add typed Chat readers for a person's compacted day, week and month memory, plus personal erase and opt-out.
+- #216: add `exchange.createCatalogSearchClient().search(q, filters)` for public model and provider offers (prices,
+ measured speed and Bench quality when available), and type the image search filters and source metadata.
+- #217: add the owner-only agent sheet reader and Actor pause, resume and memory controls through Network's relay.
+- #219: the app sign-in test checks a token's shape, not the letters "eyJ" (a macOS flake).
+- #220: `homeFeed.network.popular({ limit })` (the signed-out front door's five things people do now) and
+ `homeFeed.network.tryNext({ limit })` (the signed-in person's own suggestions).
+- #222: type the exact reply target and its display name on a Community comment; a thread's first page includes two
+ replies and the rest use the parent cursor.
+- #207: add `codes.tasks` for the Dock's Code tab: `list({ limit })`, `create({ text, mode, repo?, attachments? })` and
+ `stop(id)` (`codes.task-summary@1`).
+- openvibe-contracts pinned at v0.406.0.
+
## 0.147.0 — 2026-11-10
## 0.147.0 — 2026-11-10
- #181: add `chat.activity` (the person's feed: mentions, replies, reactions, server invites, accepted friend requests and roles), `rooms.react()` and `reply_to_id` on room and DM sends.
- #181: add `chat.activity` (the person's feed: mentions, replies, reactions, server invites, accepted friend requests and roles), `rooms.react()` and `reply_to_id` on room and DM sends.
@@ -8,12 +8,12 @@
@@ -8,12 +8,12 @@
**Install and test:** `npm ci && npm test` (Node 20 or newer)
**Install and test:** `npm ci && npm test` (Node 20 or newer)
```bash
```bash
-npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.147.0
+npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.148.0
```
```
> Supported browser and server clients for the OpenVibe platform.
> Supported browser and server clients for the OpenVibe platform.
-**Status:** alpha, v0.147.0 (the five 1.0 core entries — `core`, `auth`, `events`, `db` and `media` — are frozen: their exported names change only with a major release; roadmap Wave 2; developer apps from Wave 20; Media objects v2 from Wave 4; the Tools platform API; Search, Chat, AI, Vision and consent clients; sign-in with recovery and a client health check). Every release is tagged (see [CHANGELOG.md](CHANGELOG.md)). Tested against local stub servers and the built-in mock platform only, never against the live platform. Services pin release tags; each repository's `STATUS.json` names the one it uses.
+**Status:** alpha, v0.148.0 (the five 1.0 core entries — `core`, `auth`, `events`, `db` and `media` — are frozen: their exported names change only with a major release; roadmap Wave 2; developer apps from Wave 20; Media objects v2 from Wave 4; the Tools platform API; Search, Chat, AI, Vision and consent clients; sign-in with recovery and a client health check). Every release is tagged (see [CHANGELOG.md](CHANGELOG.md)). Tested against local stub servers and the built-in mock platform only, never against the live platform. Services pin release tags; each repository's `STATUS.json` names the one it uses.
**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30.
**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30.
**License:** MIT ([LICENSE](LICENSE)). This package is a library that apps outside the network embed, so it uses MIT. The OpenVibe services themselves stay AGPL-3.0.
**License:** MIT ([LICENSE](LICENSE)). This package is a library that apps outside the network embed, so it uses MIT. The OpenVibe services themselves stay AGPL-3.0.
@@ -22,7 +22,7 @@ npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags
@@ -22,7 +22,7 @@ npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags
After v0.97.1 is tagged:
After v0.97.1 is tagged:
```bash
```bash
-npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.147.0
+npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.148.0
```
```
It has no runtime dependencies. It needs Node ≥ 20, or any browser with `fetch`, Web Crypto and `TextDecoder`. There is no build step. The package is CommonJS with ESM entry points (`import` works). Each subpath has its own `.d.ts`. For a page with no bundler, `browser/openvibe-sdk.mjs` is one self-contained ES module of the browser-safe subpaths (see [Browser without a bundler](#browser-without-a-bundler)). It does not depend on `openvibe-contracts` at runtime: it copies the contract types it uses (from the temporary Contracts sharing branch, a devDependency the tests check them against).
It has no runtime dependencies. It needs Node ≥ 20, or any browser with `fetch`, Web Crypto and `TextDecoder`. There is no build step. The package is CommonJS with ESM entry points (`import` works). Each subpath has its own `.d.ts`. For a page with no bundler, `browser/openvibe-sdk.mjs` is one self-contained ES module of the browser-safe subpaths (see [Browser without a bundler](#browser-without-a-bundler)). It does not depend on `openvibe-contracts` at runtime: it copies the contract types it uses (from the temporary Contracts sharing branch, a devDependency the tests check them against).
@@ -203,9 +203,11 @@ Until OpenVibe.Tools serves the gateway facade (Tools S6), point the jobs client
@@ -203,9 +203,11 @@ Until OpenVibe.Tools serves the gateway facade (Tools S6), point the jobs client
| `openvibe-sdk/sso` | server | `createSsoClient({site, baseUrl, clientId, clientSecret, networkUrl, networkInternalUrl, secureCookies})`: a product site's sign-in with OpenVibe.Network: `router(express)` (`/login` with state + PKCE S256, `/callback`, `/fedcm`, `/logout`, `/me`, `/refresh`), `optionalAuth()`, `requireAuth()`, `verify(token)` (offline, shared JWKS), `extractToken(req)`; `sanitizeNext`, `COOKIES` |
| `openvibe-sdk/sso` | server | `createSsoClient({site, baseUrl, clientId, clientSecret, networkUrl, networkInternalUrl, secureCookies})`: a product site's sign-in with OpenVibe.Network: `router(express)` (`/login` with state + PKCE S256, `/callback`, `/fedcm`, `/logout`, `/me`, `/refresh`), `optionalAuth()`, `requireAuth()`, `verify(token)` (offline, shared JWKS), `extractToken(req)`; `sanitizeNext`, `COOKIES` |
| `openvibe-sdk/realtime` | both | `subscribe(topics, onEvent, {lastEventId, onGap, …})`, `createRealtimeClient(client)`, `parseSSE(body)` |
| `openvibe-sdk/realtime` | both | `subscribe(topics, onEvent, {lastEventId, onGap, …})`, `createRealtimeClient(client)`, `parseSSE(body)` |
| `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/composer` | both | `createComposerClient(client, {mediaBaseUrl?, networkBaseUrl?})`: `attachments.upload(file, {onProgress, signal})` (multipart to Media through `XMLHttpRequest`, real browser progress), `attachments.remove(id)`, `composer.prefs.get()/put(prefs, rev)` (a stale revision is 409 `composer.rev_conflict`), `mentions.search(q, kinds)` |
| `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`, shared `comments.resolve/get/upload/create/edit/delete/visibility`, `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`, shared `comments.resolve/get/upload/create/edit/delete/visibility`, `as(subject)` |
| `openvibe-sdk/history` | both | Private history reads: cursor pages, title search, day/week summaries, recent agent reads, settings and owner-scoped deletes. |
| `openvibe-sdk/history` | both | Private history reads: cursor pages, title search, day/week summaries, recent agent reads, settings and owner-scoped deletes. |
| `openvibe-sdk/home-feed` | both | `createHomeFeedClient(client)` gives `ov.home.get()/set(layout)`, `ov.network.today()`, `ov.feed.list({ for: 'me', cursor })`, `ov.feed.post(text)`, and `ov.feed.contribute({ id, text, url })`. Personal calls need a user Bearer token; contribution needs an app token granted `community.pulse.contribute`. Today is public and cached by Network for five minutes. |
| `openvibe-sdk/home-feed` | both | `createHomeFeedClient(client)` gives `ov.home.get()/set(layout)`, `ov.network.today()`, `ov.feed.list({ for: 'me', cursor })`, `ov.feed.post(text)`, and `ov.feed.contribute({ id, text, url })`. Personal calls need a user Bearer token; contribution needs an app token granted `community.pulse.contribute`. Today is public and cached by Network for five minutes. |
+| `openvibe-sdk/feed` | both | `createFeedClient(client)` gives `ov.feed.list({ kind, cursor })` for the public cross-site Community Feed. Pass the returned `next_cursor` as `cursor` to read the next page. |
| `openvibe-sdk/bot` | both | `createBotClient(client, {actingSubject?})`: OpenVibe.Bot robots. `robots.list/get/create/update/delete/pairingCode`, `robots.operators.list/add/remove`, `robots.devices`, `robots.streaming.get/set`, `robots.audit`/`iterateAudit`, `robots.estop`/`clearEstop`, `robots.command` (`bot.command@1`), `embedUrl(robotId, mode)` (panel, overlay or compact), `devices.rotate/revoke`, public `profiles.list/get` and `kits.list/get`, `pair` (the agent's one-time code, never retried), `as(subject)`; a service acts for a person with `X-OV-Subject` (capabilities `bot.robot.read`/`manage`/`control`, `bot.device.connect`) |
| `openvibe-sdk/bot` | both | `createBotClient(client, {actingSubject?})`: OpenVibe.Bot robots. `robots.list/get/create/update/delete/pairingCode`, `robots.operators.list/add/remove`, `robots.devices`, `robots.streaming.get/set`, `robots.audit`/`iterateAudit`, `robots.estop`/`clearEstop`, `robots.command` (`bot.command@1`), `embedUrl(robotId, mode)` (panel, overlay or compact), `devices.rotate/revoke`, public `profiles.list/get` and `kits.list/get`, `pair` (the agent's one-time code, never retried), `as(subject)`; a service acts for a person with `X-OV-Subject` (capabilities `bot.robot.read`/`manage`/`control`, `bot.device.connect`) |
| `openvibe-sdk/themes` | both | `createThemesClient(client)`: `catalog({mode, search, sort, limit})` (seasonal themes carry `season`), `get(idOrSlug)` (null when missing), `export(idOrSlug)`, `active()` (`{theme_id, custom_variables, display}`), `set(themeId, {custom})`, `setDisplay({motion, text, seasonal})`, `seasons({date})`, `submissions()`, `submit(theme)`, `import(file)`. In the browser: `followTheme(fn)` calls back now and on every theme, display or season change with `readTheme()` (`{id, mode, tokens, display: {motion, text, textScale, seasonal, calm}, season, seasonMark}`) and returns `stop()`, for a canvas or game to keep up; `applyTheme(vars, {root})`, `seasonOn(date, seasons)`, `onColor(hex)`, `DISPLAY_OPTIONS`, `TOKENS` |
| `openvibe-sdk/themes` | both | `createThemesClient(client)`: `catalog({mode, search, sort, limit})` (seasonal themes carry `season`), `get(idOrSlug)` (null when missing), `export(idOrSlug)`, `active()` (`{theme_id, custom_variables, display}`), `set(themeId, {custom})`, `setDisplay({motion, text, seasonal})`, `seasons({date})`, `submissions()`, `submit(theme)`, `import(file)`. In the browser: `followTheme(fn)` calls back now and on every theme, display or season change with `readTheme()` (`{id, mode, tokens, display: {motion, text, textScale, seasonal, calm}, season, seasonMark}`) and returns `stop()`, for a canvas or game to keep up; `applyTheme(vars, {root})`, `seasonOn(date, seasons)`, `onColor(hex)`, `DISPLAY_OPTIONS`, `TOKENS` |
| `openvibe-sdk/bench` | both | `createBenchClient(client, {actingSubject?})`: `kinds.list/get`, `tasks.create/get`, `rounds.create/get`, `entries.submit`, `judgements.next/cast`, `boards.get`, `ratings.get` (public, at least twenty games), `models.get`, `datasets.get/iterate`, `evals.create/get/run/results`, `mcpTools()`. Audience `openvibe.bench`; service id `bench`. |
| `openvibe-sdk/bench` | both | `createBenchClient(client, {actingSubject?})`: `kinds.list/get`, `tasks.create/get`, `rounds.create/get`, `entries.submit`, `judgements.next/cast`, `boards.get`, `ratings.get` (public, at least twenty games), `models.get`, `datasets.get/iterate`, `evals.create/get/run/results`, `mcpTools()`. Audience `openvibe.bench`; service id `bench`. |
@@ -217,6 +219,7 @@ Until OpenVibe.Tools serves the gateway facade (Tools S6), point the jobs client
@@ -217,6 +219,7 @@ Until OpenVibe.Tools serves the gateway facade (Tools S6), point the jobs client
| `openvibe-sdk/chat` | both | `createChatClient(client)`: `global.send/history/search`, `rooms.*` (list, create, get, update, messages, send, deleteMessage, join, leave, read, members, setMember, react), `dms.*` (list, create, get, messages, send, deleteMessage, read, unread, blocks, block, unblock), `activity.*` (list, unread, readOne, read: what happened to the person in Chat), `embeds(message)` (the typed `chat.embed@1` address previews on a message), `exportMine()`, `vibe.*` (list, start, rename, remove: chats with Vibe), `inboxes.*` (list, create, get, update, remove, addMember, removeMember, conversations, messages, reply, setStatus, read, identity, replies, saveReply, deleteReply, webhooks, addWebhook, testWebhook, removeWebhook: a support inbox's team), `support.*` (list, messages, send: their own conversations with sites' chats), `feeds.*` (mine, create, update, rotateKey, remove, view, onLive). With no account: `createInboxVisitor(client, { inbox, token, user, account })` (visit, send, messages, handoff, rate: your own panel for an inbox, from any site) and `createFeedPublisher(client, { feed, key })` (hello, publish, claudeCode: vibe coding events with the feed's key) |
| `openvibe-sdk/chat` | both | `createChatClient(client)`: `global.send/history/search`, `rooms.*` (list, create, get, update, messages, send, deleteMessage, join, leave, read, members, setMember, react), `dms.*` (list, create, get, messages, send, deleteMessage, read, unread, blocks, block, unblock), `activity.*` (list, unread, readOne, read: what happened to the person in Chat), `embeds(message)` (the typed `chat.embed@1` address previews on a message), `exportMine()`, `vibe.*` (list, start, rename, remove: chats with Vibe), `inboxes.*` (list, create, get, update, remove, addMember, removeMember, conversations, messages, reply, setStatus, read, identity, replies, saveReply, deleteReply, webhooks, addWebhook, testWebhook, removeWebhook: a support inbox's team), `support.*` (list, messages, send: their own conversations with sites' chats), `feeds.*` (mine, create, update, rotateKey, remove, view, onLive). With no account: `createInboxVisitor(client, { inbox, token, user, account })` (visit, send, messages, handoff, rate: your own panel for an inbox, from any site) and `createFeedPublisher(client, { feed, key })` (hello, publish, claudeCode: vibe coding events with the feed's key) |
| `openvibe-sdk/ai` | both | `createAiClient(client)`: `runs.create(workflow, input, {wait, idempotencyKey, latency, deadlineHours, …})`, `runs.get/list/cancel/retry/citations/addCitations/waitFor`, `batches.create/get/list/cancel`, `creations()` (the public catalogue of what AI can make, no token sent), `chat/generate/summarize/classify/extract/enrich/embed`. A run may ask for the class it may wait in (`latency`: `interactive`, `background`, `batch`; `deadlineHours`: 1–72 for a batch run); `waitFor` gives a batch run back pending rather than throwing when the wait is over — the answer comes with the run's completion event or webhook. |
| `openvibe-sdk/ai` | both | `createAiClient(client)`: `runs.create(workflow, input, {wait, idempotencyKey, latency, deadlineHours, …})`, `runs.get/list/cancel/retry/citations/addCitations/waitFor`, `batches.create/get/list/cancel`, `creations()` (the public catalogue of what AI can make, no token sent), `chat/generate/summarize/classify/extract/enrich/embed`. A run may ask for the class it may wait in (`latency`: `interactive`, `background`, `batch`; `deadlineHours`: 1–72 for a batch run); `waitFor` gives a batch run back pending rather than throwing when the wait is over — the answer comes with the run's completion event or webhook. |
| `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/agents` | both | `createAgentsClient(client, {baseUrl?, service?, audience?})`: `run(id)` fetches an agent run snapshot (null when missing), `events(id, {lastEventId, signal})` follows the resumable AG-UI event stream, `post(id, approval)` answers an approval request |
| `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, prefer})` (+ `.wait()`), `jobs`; `ToolRunError`, `isToolRunError()`; `prefer` picks the worker placement (`auto`, `home`, `host`) ([details](docs/tools-routing.md)) |
| `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, prefer})` (+ `.wait()`), `jobs`; `ToolRunError`, `isToolRunError()`; `prefer` picks the worker placement (`auto`, `home`, `host`) ([details](docs/tools-routing.md)) |
| `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` |
| `openvibe-sdk/resources` | server | `parseResourceName`/`resourceName`/`resourceNameOf` (OVRN, via the pinned `contracts.resources`) and `RESOURCE_KINDS` (only the chosen `med`/`wch` prefixes); `createResourceIndex({authorities, token, fetch, pageLimit, concurrency, timeoutMs, maxPages, maxMs})`: `list({project, kind, owner, limit})` -> `{resources, stale}` (`owner`: a subject id, each authority answering only what it owns), fanning out over the authorities' `GET /api/v1/resources`, following each opaque `next_cursor` and merging `common.resource-list-result@1` pages (an authority that fails — or whose cursor chain repeats, outruns `maxPages`, or outruns `maxMs` — is reported in `stale`, never fatal), and `iterate`; `createResourceClient({origin, token})`: `control(request)` POSTs a `common.resource-control-request@1` to `/api/v1/resources/control` — idempotency-keyed, transient failures retried with the same key and body, a thrown error carrying that `idempotencyKey` — and returns the `common.resource-control-result@1` (`done`/`pending`/`refused`/`failed`, a refused/failed one returned even with a non-retryable non-2xx) |
| `openvibe-sdk/resources` | server | `parseResourceName`/`resourceName`/`resourceNameOf` (OVRN, via the pinned `contracts.resources`) and `RESOURCE_KINDS` (only the chosen `med`/`wch` prefixes); `createResourceIndex({authorities, token, fetch, pageLimit, concurrency, timeoutMs, maxPages, maxMs})`: `list({project, kind, owner, limit})` -> `{resources, stale}` (`owner`: a subject id, each authority answering only what it owns), fanning out over the authorities' `GET /api/v1/resources`, following each opaque `next_cursor` and merging `common.resource-list-result@1` pages (an authority that fails — or whose cursor chain repeats, outruns `maxPages`, or outruns `maxMs` — is reported in `stale`, never fatal), and `iterate`; `createResourceClient({origin, token})`: `control(request)` POSTs a `common.resource-control-request@1` to `/api/v1/resources/control` — idempotency-keyed, transient failures retried with the same key and body, a thrown error carrying that `idempotencyKey` — and returns the `common.resource-control-result@1` (`done`/`pending`/`refused`/`failed`, a refused/failed one returned even with a non-retryable non-2xx) |
@@ -3,7 +3,7 @@
@@ -3,7 +3,7 @@
"stage": "alpha",
"stage": "alpha",
"code": true,
"code": true,
"wave": "W2",
"wave": "W2",
- "version": "0.147.0",
+ "version": "0.148.0",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30",
"deployed": false,
"deployed": false,
"deployment": "none: a package released as git tags; services and apps pin a tag",
"deployment": "none: a package released as git tags; services and apps pin a tag",
@@ -50,5 +50,5 @@
@@ -50,5 +50,5 @@
"vibes (openvibe-sdk/vibes): Vibes Pay charges (create with the app reference as the Idempotency-Key, get, refund) and constant-time webhook verification (X-OpenVibe-Signature-V2, ±300 s) for developer apps",
"vibes (openvibe-sdk/vibes): Vibes Pay charges (create with the app reference as the Idempotency-Key, get, refund) and constant-time webhook verification (X-OpenVibe-Signature-V2, ±300 s) for developer apps",
"streams (openvibe-sdk/streams): the Agent Streams plan S1 — start an agent stream on OpenVibe.Live (live.agent-stream@1) with its publish ticket, renew and revoke that ticket (the kill switch), poll state, post the last frame, set the curtain, switch visibility, end it and mint the view ticket; the publish ticket is a bearer of its own on the desk routes"
"streams (openvibe-sdk/streams): the Agent Streams plan S1 — start an agent stream on OpenVibe.Live (live.agent-stream@1) with its publish ticket, renew and revoke that ticket (the kill switch), poll state, post the last frame, set the curtain, switch visibility, end it and mint the view ticket; the publish ticket is a bearer of its own on the desk routes"
],
],
- "updated": "2026-11-10"
+ "updated": "2026-11-11"
}
}
@@ -16,11 +16,13 @@ module.exports = {
@@ -16,11 +16,13 @@ module.exports = {
modules: require('./src/modules'),
modules: require('./src/modules'),
realtime: require('./src/realtime'),
realtime: require('./src/realtime'),
media: require('./src/media'),
media: require('./src/media'),
+ composer: require('./src/composer'),
cam: require('./src/cam'),
cam: require('./src/cam'),
mediaUpload: require('./src/media-upload'),
mediaUpload: require('./src/media-upload'),
ui: require('./src/ui'),
ui: require('./src/ui'),
community: require('./src/community'),
community: require('./src/community'),
homeFeed: require('./src/home-feed'),
homeFeed: require('./src/home-feed'),
+ feed: require('./src/feed'),
bot: require('./src/bot'),
bot: require('./src/bot'),
search: require('./src/search'),
search: require('./src/search'),
bench: require('./src/bench'),
bench: require('./src/bench'),
@@ -33,9 +35,12 @@ module.exports = {
@@ -33,9 +35,12 @@ module.exports = {
ai: require('./src/ai'),
ai: require('./src/ai'),
credentials: require('./src/credentials'),
credentials: require('./src/credentials'),
jobs: require('./src/jobs'),
jobs: require('./src/jobs'),
+ agents: require('./src/agents'),
tools: require('./src/tools'),
tools: require('./src/tools'),
projects: require('./src/projects'),
projects: require('./src/projects'),
geo: require('./src/geo'),
geo: require('./src/geo'),
history: require('./src/history'),
history: require('./src/history'),
userCard: require('./src/user-card'),
userCard: require('./src/user-card'),
+ player: require('./src/player'),
+ agentSheet: require('./src/agent-sheet'),
};
};
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
-// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.147.0, MIT). Do not edit.
+// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.148.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, locker, registry, trust, levels, modules, realtime, media, cam, mediaUpload, ui, community, homeFeed, bot, search, bench, meme, vision, themes, design, redact, chat, ai, credentials, jobs, tools, projects, geo, history, userCard). No server-only code: no service tokens, JWT verification, events, identity.
+// (auth, locker, registry, trust, levels, modules, realtime, media, composer, cam, mediaUpload, ui, community, homeFeed, feed, bot, search, bench, meme, vision, themes, design, redact, chat, ai, credentials, jobs, agents, tools, projects, geo, history, userCard, player, agentSheet). 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 ──────────────────────────────────────────────────
@@ -23,28 +23,33 @@ module.exports = {
@@ -23,28 +23,33 @@ module.exports = {
modules: __load(24),
modules: __load(24),
realtime: __load(25),
realtime: __load(25),
media: __load(26),
media: __load(26),
- cam: __load(28),
- mediaUpload: __load(29),
- ui: __load(30),
- community: __load(31),
- homeFeed: __load(32),
- bot: __load(33),
- search: __load(34),
- bench: __load(35),
- meme: __load(36),
- vision: __load(37),
- themes: __load(38),
- design: __load(39),
- redact: __load(40),
- chat: __load(41),
- ai: __load(43),
- credentials: __load(45),
- jobs: __load(46),
- tools: __load(48),
- projects: __load(49),
- geo: __load(50),
- history: __load(51),
- userCard: __load(52),
+ composer: __load(28),
+ cam: __load(29),
+ mediaUpload: __load(30),
+ ui: __load(31),
+ community: __load(32),
+ homeFeed: __load(33),
+ feed: __load(34),
+ bot: __load(35),
+ search: __load(36),
+ bench: __load(37),
+ meme: __load(38),
+ vision: __load(39),
+ themes: __load(40),
+ design: __load(41),
+ redact: __load(42),
+ chat: __load(43),
+ ai: __load(45),
+ credentials: __load(47),
+ jobs: __load(48),
+ agents: __load(50),
+ tools: __load(51),
+ projects: __load(52),
+ geo: __load(53),
+ history: __load(54),
+ userCard: __load(55),
+ player: __load(56),
+ agentSheet: __load(57),
};
};
},
},
// ── 1: src/core/index.js ───────────────────────────────────────────
// ── 1: src/core/index.js ───────────────────────────────────────────
@@ -59,7 +64,7 @@ const { paginate, offsetPager } = __load(7);
@@ -59,7 +64,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.147.0';
+const SDK_VERSION = '0.148.0';
module.exports = {
module.exports = {
createClient,
createClient,
@@ -711,10 +716,11 @@ module.exports = { paginate, offsetPager };
@@ -711,10 +716,11 @@ module.exports = { paginate, offsetPager };
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
-/** Personal bearer wallet/store facade. Amounts and ownership are enforced by Billing and Inventory. */
-function createWalletClient(client, { billingUrl, inventoryUrl } = {}) {
+/** Personal bearer wallet/store facade. Amounts and ownership are enforced by their ledger services. */
+function createWalletClient(client, { billingUrl, inventoryUrl, networkUrl } = {}) {
if (!client || typeof client.json !== 'function') throw new TypeError('createWalletClient requires an SDK core client');
if (!client || typeof client.json !== 'function') throw new TypeError('createWalletClient requires an SDK core client');
- const call = (service, path, opts = {}) => client.json({ service, baseUrl: service === 'billing' ? billingUrl : inventoryUrl,
+ const urls = { billing: billingUrl, inventory: inventoryUrl, network: networkUrl };
+ const call = (service, path, opts = {}) => client.json({ service, baseUrl: urls[service],
audience: `openvibe.${service}`, path, credentials: 'omit', ...opts });
audience: `openvibe.${service}`, path, credentials: 'omit', ...opts });
const key = (v) => {
const key = (v) => {
if (typeof v !== 'string' || !/^[A-Za-z0-9._:-]{8,128}$/.test(v)) throw new TypeError('An idempotency_key of 8–128 letters, digits, dot, colon, underscore or dash is required');
if (typeof v !== 'string' || !/^[A-Za-z0-9._:-]{8,128}$/.test(v)) throw new TypeError('An idempotency_key of 8–128 letters, digits, dot, colon, underscore or dash is required');
@@ -722,12 +728,38 @@ function createWalletClient(client, { billingUrl, inventoryUrl } = {}) {
@@ -722,12 +728,38 @@ function createWalletClient(client, { billingUrl, inventoryUrl } = {}) {
};
};
const whole = (v) => { if (!Number.isSafeInteger(v) || v < 1) throw new TypeError('Choose a positive whole amount'); return v; };
const whole = (v) => { if (!Number.isSafeInteger(v) || v < 1) throw new TypeError('Choose a positive whole amount'); return v; };
const itemId = (v) => { if (!/^itd_[0-9A-HJKMNP-TV-Z]{26}$/.test(v || '')) throw new TypeError('An item id is an itd_ id'); return encodeURIComponent(v); };
const itemId = (v) => { if (!/^itd_[0-9A-HJKMNP-TV-Z]{26}$/.test(v || '')) throw new TypeError('An item id is an itd_ id'); return encodeURIComponent(v); };
+ const cursor = (v, currency) => {
+ if (v == null) return v;
+ if (currency === 'vibes') {
+ if (typeof v !== 'string' || !/^txn_[0-9A-HJKMNP-TV-Z]{26}$/.test(v)) throw new TypeError('vibes cursor is a txn_ id');
+ } else if (!Number.isSafeInteger(Number(v)) || Number(v) <= 0 || !/^[0-9]+$/.test(String(v))) {
+ throw new TypeError('cursor is a positive move id');
+ }
+ return v;
+ };
+ const streamer = (v) => { if (!Number.isSafeInteger(v) || v <= 0) throw new TypeError('streamer is a positive user id'); return v; };
// The BODY key is durable in both services. Retries keep it; the SDK must not add an unrelated header key.
// The BODY key is durable in both services. Retries keep it; the SDK must not add an unrelated header key.
const write = (service, path, json, { signal } = {}) => call(service, path, { method: 'POST', json,
const write = (service, path, json, { signal } = {}) => call(service, path, { method: 'POST', json,
idempotencyKey: false, idempotent: true, signal });
idempotencyKey: false, idempotent: true, signal });
return {
return {
wallet: {
wallet: {
get: ({ signal } = {}) => call('billing', '/api/v1/me/wallet', { signal }),
get: ({ signal } = {}) => call('billing', '/api/v1/me/wallet', { signal }),
+ changes: ({ since_vibes, since_opencoins, since_channel_points, streamer: channel, limit, signal } = {}) => {
+ if (limit != null && (!Number.isSafeInteger(limit) || limit < 1 || limit > 500)) throw new TypeError('limit is 1 to 500');
+ return call('network', '/api/wallet/changes', { query: {
+ since_vibes: cursor(since_vibes, 'vibes'), since_opencoins: cursor(since_opencoins, 'opencoins'),
+ since_channel_points: cursor(since_channel_points, 'channel-points'),
+ streamer: channel == null ? undefined : streamer(channel), limit,
+ }, signal });
+ },
+ markRead: ({ currency, cursor: at, streamer: channel }, { signal } = {}) => {
+ if (!['vibes', 'opencoins', 'channel-points'].includes(currency)) throw new TypeError('currency is vibes, opencoins or channel-points');
+ if (at == null) throw new TypeError('cursor is required');
+ if (currency === 'channel-points' && channel == null) throw new TypeError('streamer is required for channel points');
+ return call('network', '/api/wallet/changes/read', { method: 'PUT', json: {
+ currency, cursor: cursor(at, currency), ...(currency === 'channel-points' ? { streamer: streamer(channel) } : {}),
+ }, signal });
+ },
topUp: ({ provider, amount_cents, idempotency_key }, options) => write('billing', '/api/v1/me/wallet/topups',
topUp: ({ provider, amount_cents, idempotency_key }, options) => write('billing', '/api/v1/me/wallet/topups',
{ provider, amount_cents: whole(amount_cents), idempotency_key: key(idempotency_key) }, options),
{ provider, amount_cents: whole(amount_cents), idempotency_key: key(idempotency_key) }, options),
},
},
@@ -3306,7 +3338,150 @@ function createObjectsClient({
@@ -3306,7 +3338,150 @@ function createObjectsClient({
module.exports = { createObjectsClient };
module.exports = { createObjectsClient };
},
},
-// ── 28: src/cam/index.js ────────────────────────────────────────────
+// ── 28: src/composer.js ─────────────────────────────────────────────
+function (module, exports) {
+'use strict';
+/**
+ * openvibe-sdk/composer: the shared composer's client, the same on every OpenVibe site.
+ *
+ * attachments.upload(file, { onProgress, signal, filename, contentType, xhr })
+ * POST /api/v1/attachments on Media (multipart `file`), through XMLHttpRequest so upload progress is real.
+ * -> composer.attachment@1
+ * attachments.remove(id)
+ * DELETE /api/v1/attachments/<id>; true when it went, false when it was already gone.
+ * composer.prefs.get() / composer.prefs.put(prefs, rev)
+ * GET/PUT /api/v1/me/composer on Network (composer.prefs@1). A PUT sends the revision it read; a stale one is
+ * 409 composer.rev_conflict and err.problem.current is the other tab's document.
+ * mentions.search(q, kinds)
+ * GET /api/v1/mentions on Network (composer.mention@1): people the caller follows first, then public people,
+ * repositories from OpenVibe Git, first-party sites. Signed-in only; at most 8.
+ *
+ * Browser-safe. The composer page calls Network directly for prefs and mentions (every site's connect-src allows it)
+ * and Media for the bytes (the site's CSP must allow OPENVIBE_MEDIA_ORIGIN).
+ */
+const { OpenVibeError, isOpenVibeError } = __load(3);
+
+const MEDIA_ORIGIN = 'https://openvibe.media';
+const NETWORK_ORIGIN = 'https://openvibe.network';
+const ATTACHMENT_MAX = 300 * 1024 * 1024;
+const enc = encodeURIComponent;
+
+function toBlob(file, contentType) {
+ if (typeof Blob !== 'undefined' && file instanceof Blob) return file;
+ if (typeof file === 'string' || file instanceof ArrayBuffer || ArrayBuffer.isView(file)) {
+ return new Blob([file], contentType ? { type: contentType } : undefined);
+ }
+ throw new TypeError('attachments.upload: pass a File/Blob, ArrayBuffer, typed array (Buffer) or string');
+}
+
+function abortError() {
+ return new OpenVibeError({ code: 'sdk.aborted', message: 'the upload was aborted' });
+}
+
+function createComposerClient(client, { mediaBaseUrl, networkBaseUrl } = {}) {
+ if (!client || typeof client.json !== 'function') throw new TypeError('createComposerClient: an SDK client is required');
+ const net = (opts) => client.json({ service: 'network', audience: 'openvibe.network', ...(networkBaseUrl ? { baseUrl: networkBaseUrl } : {}), ...opts });
+
+ /** The Media token for an XHR upload (the XHR cannot borrow the fetch client's request loop). */
+ async function mediaToken() {
+ const o = client.options || {};
+ if (o.token != null) return o.token;
+ if (typeof o.getToken === 'function') return (await o.getToken({ service: 'media', audience: 'openvibe.media' })) || null;
+ return null;
+ }
+
+ const attachments = {
+ /**
+ * One file -> composer.attachment@1. Progress is the browser's upload progress (XHR), not a guess.
+ * `xhr` injects an XMLHttpRequest implementation (tests, a non-browser shell); otherwise the global one.
+ */
+ upload(file, { onProgress, signal, filename, contentType, xhr } = {}) {
+ return new Promise((resolve, reject) => {
+ const XHR = xhr || (typeof XMLHttpRequest !== 'undefined' ? XMLHttpRequest : null);
+ if (!XHR) return reject(new OpenVibeError({ code: 'sdk.xhr_unavailable', message: 'attachments.upload needs XMLHttpRequest (pass { xhr })' }));
+ let blob;
+ try { blob = toBlob(file, contentType); } catch (err) { return reject(err); }
+ if (blob.size > ATTACHMENT_MAX) {
+ return reject(new OpenVibeError({ code: 'media.attachment.too_large', status: 413, message: `a file may be at most ${ATTACHMENT_MAX / (1024 * 1024)} MB` }));
+ }
+ const url = client.url({ service: 'media', ...(mediaBaseUrl ? { baseUrl: mediaBaseUrl } : {}), path: '/api/v1/attachments' });
+ Promise.resolve(url).then(async (target) => {
+ if (signal && signal.aborted) return reject(abortError());
+ const fd = new FormData();
+ fd.append('file', blob, filename || (file && file.name) || 'file');
+ const req = new XHR();
+ let settled = false;
+ const finish = (fn, v) => { if (settled) return; settled = true; cleanup(); fn(v); };
+ const offAbort = signal && typeof signal.addEventListener === 'function' ? () => signal.removeEventListener('abort', onAbort) : () => {};
+ function onAbort() { try { req.abort(); } catch { /* */ } finish(reject, abortError()); }
+ const cleanup = () => { offAbort(); };
+ req.open('POST', target);
+ req.setRequestHeader('Accept', 'application/json');
+ const token = await mediaToken();
+ if (token) req.setRequestHeader('Authorization', `Bearer ${token}`);
+ if (req.upload && typeof onProgress === 'function') {
+ req.upload.onprogress = (e) => onProgress({ loaded: e.loaded || 0, total: e.total || blob.size, lengthComputable: e.lengthComputable !== false });
+ }
+ req.onload = () => {
+ let body = null;
+ try { body = req.responseText ? JSON.parse(req.responseText) : null; } catch { body = req.responseText || null; }
+ if (req.status >= 200 && req.status < 300) return finish(resolve, body);
+ const err = OpenVibeError.fromResponse({ status: req.status, body, method: 'POST', url: target });
+ if (body && body.current) err.current = body.current;
+ finish(reject, err);
+ };
+ req.onerror = () => finish(reject, new OpenVibeError({ code: 'sdk.network_error', message: 'the upload could not reach Media', method: 'POST', url: target }));
+ req.ontimeout = () => finish(reject, new OpenVibeError({ code: 'sdk.timeout', message: 'the upload timed out', method: 'POST', url: target }));
+ if (signal && typeof signal.addEventListener === 'function') signal.addEventListener('abort', onAbort, { once: true });
+ try { req.send(fd); } catch (err) { finish(reject, err); }
+ }).catch(reject);
+ });
+ },
+
+ /** DELETE /api/v1/attachments/<id>: true when it went, false when it was already gone. */
+ async remove(id) {
+ if (!id) throw new TypeError('attachments.remove: pass the attachment id');
+ try {
+ await client.json({ service: 'media', ...(mediaBaseUrl ? { baseUrl: mediaBaseUrl } : {}), audience: 'openvibe.media', method: 'DELETE', path: `/api/v1/attachments/${enc(id)}` });
+ return true;
+ } catch (err) {
+ if (isOpenVibeError(err) && err.status === 404) return false;
+ throw err;
+ }
+ },
+ };
+
+ const composer = {
+ prefs: {
+ /** composer.prefs@1, with its rev. */
+ get: () => net({ path: '/api/v1/me/composer' }),
+ /**
+ * composer.prefs@1 in (rev is the one you read; 0 for a new person), the new document out.
+ * A stale rev is 409 composer.rev_conflict with err.problem.current (and err.current).
+ */
+ put(prefs, rev) {
+ if (!prefs || typeof prefs !== 'object') throw new TypeError('composer.prefs.put: pass the prefs object');
+ if (!Number.isInteger(rev) || rev < 0) throw new TypeError('composer.prefs.put: pass the revision you read (0 for new)');
+ return net({ method: 'PUT', path: '/api/v1/me/composer', json: { ...prefs, rev } });
+ },
+ },
+ };
+
+ const mentions = {
+ /** @ menu entries (composer.mention@1[]). kinds: person | repo | site | file; omitted = all. */
+ async search(q, kinds) {
+ const list = Array.isArray(kinds) ? kinds : (kinds ? String(kinds).split(',') : []);
+ const data = await net({ path: '/api/v1/mentions', query: { q: q || undefined, kinds: list.length ? list.join(',') : undefined } });
+ return (data && Array.isArray(data.mentions)) ? data.mentions : [];
+ },
+ };
+
+ return { attachments, composer, mentions, MEDIA_ORIGIN, NETWORK_ORIGIN };
+}
+
+module.exports = { createComposerClient, MEDIA_ORIGIN, NETWORK_ORIGIN, ATTACHMENT_MAX };
+},
+// ── 29: src/cam/index.js ────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
/**
/**
@@ -3416,7 +3591,7 @@ function defineOvCamera(options = {}) {
@@ -3416,7 +3591,7 @@ function defineOvCamera(options = {}) {
module.exports = { defineOvCamera, OV_CAMERA_SRC, CAM_KIT_SRC, CAM_KIT_SRI, loadCamKit, open, settings };
module.exports = { defineOvCamera, OV_CAMERA_SRC, CAM_KIT_SRC, CAM_KIT_SRI, loadCamKit, open, settings };
},
},
-// ── 29: src/media-upload.js ─────────────────────────────────────────
+// ── 30: src/media-upload.js ─────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
const { createObjectsClient } = __load(26);
const { createObjectsClient } = __load(26);
@@ -3436,7 +3611,7 @@ function createUploadClient(config) {
@@ -3436,7 +3611,7 @@ function createUploadClient(config) {
module.exports = { createUploadClient };
module.exports = { createUploadClient };
},
},
-// ── 30: src/ui.js ───────────────────────────────────────────────────
+// ── 31: src/ui.js ───────────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
@@ -3449,7 +3624,7 @@ function progress(el, options) {
@@ -3449,7 +3624,7 @@ function progress(el, options) {
module.exports = { progress };
module.exports = { progress };
},
},
-// ── 31: src/community.js ────────────────────────────────────────────
+// ── 32: src/community.js ────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
/**
/**
@@ -3566,6 +3741,8 @@ function createCommunityClient(client, defaults = {}) {
@@ -3566,6 +3741,8 @@ function createCommunityClient(client, defaults = {}) {
query: { kind: query.kind, site: query.site, q: query.q, cursor: query.cursor, limit: query.limit,
query: { kind: query.kind, site: query.site, q: query.q, cursor: query.cursor, limit: query.limit,
following: query.following === true || query.following === 1 ? '1' : undefined },
following: query.following === true || query.following === 1 ? '1' : undefined },
}, o);
}, o);
+ // Named form used by new integrations; the callable form remains for existing SDK consumers.
+ feed.list = feed;
const posts = {
const posts = {
/**
/**
* A status post in the Feed at once: { text, pictures?: [{ media_id, alt }] } (at most four, alt required,
* A status post in the Feed at once: { text, pictures?: [{ media_id, alt }] } (at most four, alt required,
@@ -3610,15 +3787,16 @@ function createCommunityClient(client, defaults = {}) {
@@ -3610,15 +3787,16 @@ function createCommunityClient(client, defaults = {}) {
module.exports = { createCommunityClient, actingHeaders };
module.exports = { createCommunityClient, actingHeaders };
},
},
-// ── 32: src/home-feed.js ────────────────────────────────────────────
+// ── 33: src/home-feed.js ────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
-/** The menu's portable Home layout, network day counts and personal wall. Pass a user-token core client;
- * contribute() needs an app token with the person's community.pulse.contribute grant. */
+/** The menu's portable Home layout, network day counts, what people do now and personal wall. Pass a user-token core
+ * client; contribute() needs an app token with the person's community.pulse.contribute grant. */
function createHomeFeedClient(client, { networkBaseUrl, communityBaseUrl } = {}) {
function createHomeFeedClient(client, { networkBaseUrl, communityBaseUrl } = {}) {
if (!client || typeof client.json !== 'function') throw new TypeError('createHomeFeedClient: pass an openvibe-sdk/core client');
if (!client || typeof client.json !== 'function') throw new TypeError('createHomeFeedClient: pass an openvibe-sdk/core client');
const network = (opts) => client.json({ service: 'network', baseUrl: networkBaseUrl, audience: 'openvibe.network', ...opts });
const network = (opts) => client.json({ service: 'network', baseUrl: networkBaseUrl, audience: 'openvibe.network', ...opts });
const community = (opts) => client.json({ service: 'community', baseUrl: communityBaseUrl, audience: 'openvibe.community', ...opts });
const community = (opts) => client.json({ service: 'community', baseUrl: communityBaseUrl, audience: 'openvibe.community', ...opts });
+ const limitQuery = (limit) => (limit == null ? undefined : { limit });
return {
return {
home: {
home: {
get: ({ signal } = {}) => network({ path: '/api/v1/me/home-screen', signal }),
get: ({ signal } = {}) => network({ path: '/api/v1/me/home-screen', signal }),
@@ -3626,6 +3804,10 @@ function createHomeFeedClient(client, { networkBaseUrl, communityBaseUrl } = {})
@@ -3626,6 +3804,10 @@ function createHomeFeedClient(client, { networkBaseUrl, communityBaseUrl } = {})
},
},
network: {
network: {
today: ({ signal } = {}) => network({ path: '/api/v1/frame/today', auth: false, signal }),
today: ({ signal } = {}) => network({ path: '/api/v1/frame/today', auth: false, signal }),
+ // The signed-out front door's list: Report's public aggregate, no credentials sent.
+ popular: ({ limit, signal } = {}) => network({ path: '/api/v1/home/popular', query: limitQuery(limit), auth: false, signal }),
+ // The signed-in person's own suggestions; Network computes them in the caller's request.
+ tryNext: ({ limit, signal } = {}) => network({ path: '/api/v1/me/try-next', query: limitQuery(limit), signal }),
},
},
feed: {
feed: {
list: ({ for: audience = 'me', cursor, signal } = {}) => community({ path: '/api/v1/pulse', query: { for: audience, cursor }, signal }),
list: ({ for: audience = 'me', cursor, signal } = {}) => community({ path: '/api/v1/pulse', query: { for: audience, cursor }, signal }),
@@ -3637,7 +3819,25 @@ function createHomeFeedClient(client, { networkBaseUrl, communityBaseUrl } = {})
@@ -3637,7 +3819,25 @@ function createHomeFeedClient(client, { networkBaseUrl, communityBaseUrl } = {})
module.exports = { createHomeFeedClient };
module.exports = { createHomeFeedClient };
},
},
-// ── 33: src/bot.js ──────────────────────────────────────────────────
+// ── 34: src/feed.js ─────────────────────────────────────────────────
+function (module, exports) {
+'use strict';
+
+/** Browser-safe reader for the public, cross-site Feed cards on OpenVibe.Community. */
+function createFeedClient(client, { communityBaseUrl } = {}) {
+ if (!client || typeof client.json !== 'function') throw new TypeError('createFeedClient: pass an openvibe-sdk/core client');
+ return {
+ /** Read one page of cards; pass next_cursor back as cursor to continue. */
+ list: ({ kind, cursor } = {}, { signal } = {}) => client.json({
+ service: 'community', baseUrl: communityBaseUrl, audience: 'openvibe.community',
+ path: '/api/v1/feed', query: { kind, cursor }, signal,
+ }),
+ };
+}
+
+module.exports = { createFeedClient };
+},
+// ── 35: src/bot.js ──────────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
/**
/**
@@ -3878,7 +4078,7 @@ function createBotClient(client, defaults = {}) {
@@ -3878,7 +4078,7 @@ function createBotClient(client, defaults = {}) {
module.exports = { createBotClient, actingHeaders };
module.exports = { createBotClient, actingHeaders };
},
},
-// ── 34: src/search.js ───────────────────────────────────────────────
+// ── 36: src/search.js ───────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
/**
/**
@@ -4024,7 +4224,7 @@ function createSearchClient(client, defaults = {}) {
@@ -4024,7 +4224,7 @@ function createSearchClient(client, defaults = {}) {
module.exports = { createSearchClient, queryOf, fabricOf, codeOf };
module.exports = { createSearchClient, queryOf, fabricOf, codeOf };
},
},
-// ── 35: src/bench.js ────────────────────────────────────────────────
+// ── 37: src/bench.js ────────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
/** openvibe-sdk/bench: OpenVibe.Bench's JSON API and streamed open dataset. Browser-safe. */
/** openvibe-sdk/bench: OpenVibe.Bench's JSON API and streamed open dataset. Browser-safe. */
@@ -4132,7 +4332,7 @@ function createBenchClient(client, defaults = {}) {
@@ -4132,7 +4332,7 @@ function createBenchClient(client, defaults = {}) {
module.exports = { createBenchClient };
module.exports = { createBenchClient };
},
},
-// ── 36: src/meme.js ─────────────────────────────────────────────────
+// ── 38: src/meme.js ─────────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
/**
/**
@@ -4215,7 +4415,7 @@ function createMemeClient(client, defaults = {}) {
@@ -4215,7 +4415,7 @@ function createMemeClient(client, defaults = {}) {
module.exports = { createMemeClient };
module.exports = { createMemeClient };
},
},
-// ── 37: src/vision.js ───────────────────────────────────────────────
+// ── 39: src/vision.js ───────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
/**
/**
@@ -4373,7 +4573,7 @@ function pick(o, keys) {
@@ -4373,7 +4573,7 @@ function pick(o, keys) {
module.exports = { createVisionClient, DEFAULT_AUDIENCE };
module.exports = { createVisionClient, DEFAULT_AUDIENCE };
},
},
-// ── 38: src/themes.js ───────────────────────────────────────────────
+// ── 40: src/themes.js ───────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
/**
/**
@@ -4523,7 +4723,7 @@ function followTheme(fn, opts = {}) {
@@ -4523,7 +4723,7 @@ function followTheme(fn, opts = {}) {
module.exports = { createThemesClient, followTheme, readTheme, applyTheme, seasonOn, onColor, DISPLAY_OPTIONS, TOKENS };
module.exports = { createThemesClient, followTheme, readTheme, applyTheme, seasonOn, onColor, DISPLAY_OPTIONS, TOKENS };
},
},
-// ── 39: src/design.js ───────────────────────────────────────────────
+// ── 41: src/design.js ───────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
@@ -4590,7 +4790,7 @@ function createDesignClient({ accessToken = null, baseUrl = DEFAULT_DESIGN_URL,
@@ -4590,7 +4790,7 @@ function createDesignClient({ accessToken = null, baseUrl = DEFAULT_DESIGN_URL,
module.exports = { createDesignClient, DEFAULT_DESIGN_URL };
module.exports = { createDesignClient, DEFAULT_DESIGN_URL };
},
},
-// ── 40: src/redact.js ───────────────────────────────────────────────
+// ── 42: src/redact.js ───────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
/**
/**
@@ -4837,7 +5037,7 @@ function onlySecret(text) {
@@ -4837,7 +5037,7 @@ function onlySecret(text) {
module.exports = { redact, onlySecret };
module.exports = { redact, onlySecret };
},
},
-// ── 41: src/chat.js ─────────────────────────────────────────────────
+// ── 43: src/chat.js ─────────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
/**
/**
@@ -4870,7 +5070,7 @@ function (module, exports) {
@@ -4870,7 +5070,7 @@ function (module, exports) {
const { isOpenVibeError } = __load(3);
const { isOpenVibeError } = __load(3);
const enc = encodeURIComponent;
const enc = encodeURIComponent;
-const history = __load(42);
+const history = __load(44);
/** The kinds chat.embed@1 names; anything else is not a preview this SDK returns. */
/** The kinds chat.embed@1 names; anything else is not a preview this SDK returns. */
const EMBED_KINDS = new Set(['link', 'video', 'image', 'live', 'picture', 'design', 'post', 'profile', 'game']);
const EMBED_KINDS = new Set(['link', 'video', 'image', 'live', 'picture', 'design', 'post', 'profile', 'game']);
@@ -4913,6 +5113,14 @@ function createChatClient(client, defaults = {}) {
@@ -4913,6 +5113,14 @@ function createChatClient(client, defaults = {}) {
history,
history,
/** The typed address previews on a message (chat.embed@1): [] when it has none. */
/** The typed address previews on a message (chat.embed@1): [] when it has none. */
embeds: (message) => readEmbeds(message),
embeds: (message) => readEmbeds(message),
+ /** Compacted day, week or month memory for a person (chat.person-memory-result@1). */
+ people: {
+ memory: (username, { channel, level = 'day' } = {}, o = {}) => call({ path: `/api/v1/people/${enc(String(username))}/memory`, query: { channel, level } }, o),
+ /** Erase the signed-in person's memory and opt out. */
+ forget: (username, o = {}) => call({ method: 'DELETE', path: `/api/v1/people/${enc(String(username))}/memory`, ...once }, o),
+ /** Set the signed-in person's opt-out preference. */
+ preferences: (username, optedOut, o = {}) => call({ method: 'PUT', path: `/api/v1/people/${enc(String(username))}/memory/preferences`, json: { opted_out: !!optedOut }, ...once }, o),
+ },
/** Vibe's model comparison API, authenticated as the person. */
/** Vibe's model comparison API, authenticated as the person. */
ask: (input, o = {}) => call({ method: 'POST', path: '/api/vibe/ask', json: input, ...once }, o),
ask: (input, o = {}) => call({ method: 'POST', path: '/api/vibe/ask', json: input, ...once }, o),
pick: (messageId, model, o = {}) => call({ method: 'POST', path: '/api/vibe/pick', json: { message_id: messageId, model }, ...once }, o),
pick: (messageId, model, o = {}) => call({ method: 'POST', path: '/api/vibe/pick', json: { message_id: messageId, model }, ...once }, o),
@@ -5151,7 +5359,7 @@ function createFeedPublisher(client, { feed, key, baseUrl } = {}) {
@@ -5151,7 +5359,7 @@ function createFeedPublisher(client, { feed, key, baseUrl } = {}) {
module.exports = { createChatClient, createInboxVisitor, createFeedPublisher, history, readEmbeds };
module.exports = { createChatClient, createInboxVisitor, createFeedPublisher, history, readEmbeds };
},
},
-// ── 42: src/chat-history.js ─────────────────────────────────────────
+// ── 44: src/chat-history.js ─────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
@@ -5230,7 +5438,7 @@ function attach({ feed, status = null, fetchPage, render, hasMore = true, thresh
@@ -5230,7 +5438,7 @@ function attach({ feed, status = null, fetchPage, render, hasMore = true, thresh
module.exports = { pages, attach };
module.exports = { pages, attach };
},
},
-// ── 43: src/ai.js ───────────────────────────────────────────────────
+// ── 45: src/ai.js ───────────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
/**
/**
@@ -5408,7 +5616,7 @@ function createAiClient(client, defaults = {}) {
@@ -5408,7 +5616,7 @@ function createAiClient(client, defaults = {}) {
remove(id) { requireFiles('remove'); return fileObjects.delete(id); },
remove(id) { requireFiles('remove'); return fileObjects.delete(id); },
};
};
- const metrics = __load(44).createMetricsClient(call);
+ const metrics = __load(46).createMetricsClient(call);
const api = { tasks, creations, choices, run, admin, runs, batches, files, metrics, TERMINAL, CLASSES };
const api = { tasks, creations, choices, run, admin, runs, batches, files, metrics, TERMINAL, CLASSES };
// Direct operations: a run of workflow ai.<op> with the input as the body, waited for by AI.
// Direct operations: a run of workflow ai.<op> with the input as the body, waited for by AI.
for (const op of OPS) {
for (const op of OPS) {
@@ -5421,7 +5629,7 @@ function createAiClient(client, defaults = {}) {
@@ -5421,7 +5629,7 @@ function createAiClient(client, defaults = {}) {
module.exports = { createAiClient, TERMINAL, OPS, CLASSES };
module.exports = { createAiClient, TERMINAL, OPS, CLASSES };
},
},
-// ── 44: src/ai-metrics.js ───────────────────────────────────────────
+// ── 46: src/ai-metrics.js ───────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
// Public measured reads: no bearer token, prompt, person or site identifiers.
// Public measured reads: no bearer token, prompt, person or site identifiers.
@@ -5450,7 +5658,7 @@ function createMetricsClient(call) {
@@ -5450,7 +5658,7 @@ function createMetricsClient(call) {
}
}
module.exports = { createMetricsClient };
module.exports = { createMetricsClient };
},
},
-// ── 45: src/credentials.js ──────────────────────────────────────────
+// ── 47: src/credentials.js ──────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
@@ -5468,7 +5676,7 @@ function createCredentialsClient(client, defaults = {}) {
@@ -5468,7 +5676,7 @@ function createCredentialsClient(client, defaults = {}) {
module.exports = { createCredentialsClient };
module.exports = { createCredentialsClient };
},
},
-// ── 46: src/jobs.js ─────────────────────────────────────────────────
+// ── 48: src/jobs.js ─────────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
/**
/**
@@ -5506,7 +5714,7 @@ function (module, exports) {
@@ -5506,7 +5714,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(47);
+const { appendFiles } = __load(49);
const { parseSSE } = __load(25);
const { parseSSE } = __load(25);
const TERMINAL = new Set(['succeeded', 'failed', 'cancelled']);
const TERMINAL = new Set(['succeeded', 'failed', 'cancelled']);
@@ -5670,7 +5878,7 @@ function createJobsClient(client, { baseUrl, service = 'tools', audience = 'open
@@ -5670,7 +5878,7 @@ function createJobsClient(client, { baseUrl, service = 'tools', audience = 'open
module.exports = { createJobsClient, isTerminal, TERMINAL_STATES: [...TERMINAL] };
module.exports = { createJobsClient, isTerminal, TERMINAL_STATES: [...TERMINAL] };
},
},
-// ── 47: src/core/form.js ────────────────────────────────────────────
+// ── 49: src/core/form.js ────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
/**
/**
@@ -5704,7 +5912,62 @@ function appendFiles(form, list, where) {
@@ -5704,7 +5912,62 @@ function appendFiles(form, list, where) {
module.exports = { isBlob, isUpload, appendFiles };
module.exports = { isBlob, isUpload, appendFiles };
},
},
-// ── 48: src/tools.js ────────────────────────────────────────────────
+// ── 50: src/agents.js ───────────────────────────────────────────────
+function (module, exports) {
+'use strict';
+/** Agent run snapshots, AG-UI event streams and approval responses. */
+const { OpenVibeError, isOpenVibeError } = __load(3);
+const { parseSSE } = __load(25);
+const enc = encodeURIComponent;
+const pause = (ms, signal) => new Promise((resolve) => {
+ const t = setTimeout(resolve, ms);
+ if (signal) signal.addEventListener('abort', () => { clearTimeout(t); resolve(); }, { once: true });
+});
+function createAgentsClient(client, { baseUrl, service = 'network', audience = 'openvibe.network' } = {}) {
+ const call = (opts) => client.request({ service, baseUrl, audience, ...opts });
+ async function run(id, { signal } = {}) {
+ if (!id) throw new TypeError('agents.run: id is required');
+ try { return (await call({ path: `/api/v1/runs/${enc(id)}`, signal })).data; }
+ catch (err) { if (isOpenVibeError(err) && err.status === 404) return null; throw err; }
+ }
+ async function post(id, approval, { signal } = {}) {
+ if (!id) throw new TypeError('agents.post: id is required');
+ if (!approval || typeof approval !== 'object' || Array.isArray(approval)) throw new TypeError('agents.post: approval must be an object');
+ return (await call({ method: 'POST', path: `/api/v1/runs/${enc(id)}/approvals`, json: approval, signal })).data;
+ }
+ async function* events(id, { lastEventId = 0, signal, maxReconnects = 20, reconnectDelayMs = 1000 } = {}) {
+ if (!id) throw new TypeError('agents.events: id is required');
+ let last = String(lastEventId || '0'), failures = 0, serverRetryMs = 0;
+ while (!(signal && signal.aborted)) {
+ let delivered = false;
+ try {
+ const res = await call({ path: `/api/v1/runs/${enc(id)}/events`, responseType: 'response', retries: 0, signal,
+ headers: { Accept: 'text/event-stream', 'Cache-Control': 'no-cache', 'Last-Event-ID': last } });
+ if (res.status === 204) return;
+ for await (const msg of parseSSE(res.data.body, { onRetry: (ms) => { serverRetryMs = ms; } })) {
+ if (msg.id !== undefined && msg.id !== '') { if (/^\d+$/.test(msg.id) && Number(msg.id) <= Number(last)) continue; last = msg.id; }
+ let data; try { data = JSON.parse(msg.data); } catch { data = null; }
+ delivered = true; failures = 0;
+ yield { id: msg.id ?? null, event: msg.event || (data && data.type) || null, data };
+ if (data && ['RUN_FINISHED', 'RUN_ERROR'].includes(data.type)) return;
+ }
+ } catch (err) {
+ if (signal && signal.aborted) return;
+ if (isOpenVibeError(err) && err.status >= 400 && err.status < 500) throw err;
+ if (!isOpenVibeError(err) && !['TypeError', 'AbortError'].includes(err && err.name)) throw err;
+ }
+ failures = delivered ? 0 : failures + 1;
+ if (failures > maxReconnects) throw new OpenVibeError({ code: 'sdk.network_error', retryable: true, message: `agents: event stream ${id} failed ${failures} times in a row` });
+ const backoffMs = Math.min(reconnectDelayMs * (2 ** Math.max(0, failures - 1)), 30000);
+ const waitMs = Math.min(Math.max(backoffMs, serverRetryMs), 30000);
+ await pause(waitMs, signal);
+ }
+ }
+ return { run, events, post };
+}
+module.exports = { createAgentsClient };
+},
+// ── 51: src/tools.js ────────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
/**
/**
@@ -5738,8 +6001,8 @@ function (module, exports) {
@@ -5738,8 +6001,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(47);
-const { createJobsClient } = __load(46);
+const { isUpload, appendFiles } = __load(49);
+const { createJobsClient } = __load(48);
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}$/;
@@ -6001,7 +6264,7 @@ function createToolsClient(client, { baseUrl, service = 'tools', audience = 'ope
@@ -6001,7 +6264,7 @@ function createToolsClient(client, { baseUrl, service = 'tools', audience = 'ope
module.exports = { createToolsClient, ToolRunError, isToolRunError };
module.exports = { createToolsClient, ToolRunError, isToolRunError };
},
},
-// ── 49: src/projects.js ─────────────────────────────────────────────
+// ── 52: src/projects.js ─────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
/**
/**
@@ -6117,7 +6380,7 @@ function createProjectsClient(client, { baseUrl } = {}) {
@@ -6117,7 +6380,7 @@ function createProjectsClient(client, { baseUrl } = {}) {
module.exports = { createProjectsClient };
module.exports = { createProjectsClient };
},
},
-// ── 50: src/geo.js ──────────────────────────────────────────────────
+// ── 53: src/geo.js ──────────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
/**
/**
@@ -6181,7 +6444,7 @@ function createGeoClient(client, { baseUrl, fetch: fetchImpl = null, samples = 3
@@ -6181,7 +6444,7 @@ function createGeoClient(client, { baseUrl, fetch: fetchImpl = null, samples = 3
module.exports = { createGeoClient };
module.exports = { createGeoClient };
},
},
-// ── 51: src/history.js ──────────────────────────────────────────────
+// ── 54: src/history.js ──────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
@@ -6209,7 +6472,7 @@ function createHistoryClient(client, { baseUrl } = {}) {
@@ -6209,7 +6472,7 @@ function createHistoryClient(client, { baseUrl } = {}) {
module.exports = { createHistoryClient };
module.exports = { createHistoryClient };
},
},
-// ── 52: src/user-card.js ────────────────────────────────────────────
+// ── 55: src/user-card.js ────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
/** One public Network aggregate. Hidden/unavailable fields stay absent; no credentials are needed. */
/** One public Network aggregate. Hidden/unavailable fields stay absent; no credentials are needed. */
@@ -6225,6 +6488,53 @@ function createUserCardClient(client, { baseUrl } = {}) {
@@ -6225,6 +6488,53 @@ function createUserCardClient(client, { baseUrl } = {}) {
}
}
module.exports = { createUserCardClient };
module.exports = { createUserCardClient };
},
},
+// ── 56: src/player.js ───────────────────────────────────────────────
+function (module, exports) {
+'use strict';
+/** Browser facade for Shared's portable live player. */
+function host() { return typeof window === 'undefined' ? null : window.OVOverlay || null; }
+function current() { return host()?.current() || null; }
+function handoff(url) { return host()?.handoffUrl(url) || url; }
+function on(listener) {
+ if (typeof listener !== 'function') throw new TypeError('listener must be a function');
+ if (typeof window === 'undefined') return () => {};
+ const receive = (event) => listener(event.detail);
+ window.addEventListener('openvibe:player', receive);
+ return () => window.removeEventListener('openvibe:player', receive);
+}
+module.exports = { current, handoff, on };
+},
+// ── 57: src/agent-sheet.js ──────────────────────────────────────────
+function (module, exports) {
+'use strict';
+/** Owner-only agent sheets through Network's fixed cross-site relay. The caller supplies their own Bearer. */
+const ACTOR = /^agt_[0-9A-HJKMNP-TV-Z]{26}$/;
+const MEMORY = /^mem_[0-9A-HJKMNP-TV-Z]{26}$/;
+const CODES = /^[a-z][a-z0-9-]{0,63}$/;
+
+function createAgentSheetClient(client, { baseUrl } = {}) {
+ if (!client || typeof client.json !== 'function') throw new TypeError('createAgentSheetClient: pass an SDK core client');
+ const id = (value, pattern, kind) => {
+ if (!pattern.test(String(value || ''))) throw new TypeError(`invalid ${kind} id`);
+ return String(value);
+ };
+ const path = (agent) => `/api/frame/agents/${id(agent, ACTOR, 'Actor agent')}`;
+ const call = (opts) => client.json({ service: 'network', audience: 'openvibe.network', baseUrl, ...opts });
+ return {
+ sheet: (agent, { signal } = {}) => call({ path: `${path(agent)}/sheet`, signal }),
+ codesSheet: (agent, { signal } = {}) => call({ path: `/api/frame/codes-agents/${id(agent, CODES, 'coding agent')}/sheet`, signal }),
+ pause: (agent, { signal } = {}) => call({ method: 'PATCH', path: path(agent), json: { status: 'paused' }, signal, idempotent: true }),
+ resume: (agent, { signal } = {}) => call({ method: 'PATCH', path: path(agent), json: { status: 'active' }, signal, idempotent: true }),
+ memory: {
+ add: (agent, text, { signal } = {}) => call({ method: 'POST', path: `${path(agent)}/memory`, json: { text }, signal, idempotent: false }),
+ update: (agent, note, text, { signal } = {}) => call({ method: 'PATCH', path: `${path(agent)}/memory/${id(note, MEMORY, 'memory')}`, json: { text }, signal, idempotent: true }),
+ remove: (agent, note, { signal } = {}) => call({ method: 'DELETE', path: `${path(agent)}/memory/${id(note, MEMORY, 'memory')}`, signal, idempotent: true }),
+ },
+ };
+}
+
+module.exports = { createAgentSheetClient };
+},
];
];
const __cache = new Array(__defs.length);
const __cache = new Array(__defs.length);
function __load(i) {
function __load(i) {
@@ -6241,9 +6551,10 @@ export const {
@@ -6241,9 +6551,10 @@ export const {
parseTraceparent, startSpan, contextFromHeaders, ulid, newEventId, newIdempotencyKey,
parseTraceparent, startSpan, contextFromHeaders, ulid, newEventId, newIdempotencyKey,
isActingSubjectId, satisfiesRange, compareVersions, CONTRACTS_RANGE, DEFAULT_NETWORK, SDK_VERSION,
isActingSubjectId, satisfiesRange, compareVersions, CONTRACTS_RANGE, DEFAULT_NETWORK, SDK_VERSION,
auth, locker, registry, trust, levels, modules,
auth, locker, registry, trust, levels, modules,
- realtime, media, cam, mediaUpload, ui, community,
- homeFeed, bot, search, bench, meme, vision,
- themes, design, redact, chat, ai, credentials,
- jobs, tools, projects, geo, history, userCard,
+ realtime, media, composer, cam, mediaUpload, ui,
+ community, homeFeed, feed, bot, search, bench,
+ meme, vision, themes, design, redact, chat,
+ ai, credentials, jobs, agents, tools, projects,
+ geo, history, userCard, player, agentSheet,
} = sdk;
} = sdk;
export default sdk;
export default sdk;
@@ -9,6 +9,16 @@
@@ -9,6 +9,16 @@
"createActorClient"
"createActorClient"
]
]
},
},
+ "agentSheet": {
+ "object": [
+ "createAgentSheetClient"
+ ]
+ },
+ "agents": {
+ "object": [
+ "createAgentsClient"
+ ]
+ },
"ai": {
"ai": {
"object": [
"object": [
"CLASSES",
"CLASSES",
@@ -101,6 +111,11 @@
@@ -101,6 +111,11 @@
"verifyTelegram"
"verifyTelegram"
]
]
},
},
+ "codes": {
+ "object": [
+ "createCodesClient"
+ ]
+ },
"commerce": {
"commerce": {
"object": [
"object": [
"CommerceError",
"CommerceError",
@@ -116,6 +131,14 @@
@@ -116,6 +131,14 @@
]
]
},
},
"compareVersions": "function",
"compareVersions": "function",
+ "composer": {
+ "object": [
+ "ATTACHMENT_MAX",
+ "MEDIA_ORIGIN",
+ "NETWORK_ORIGIN",
+ "createComposerClient"
+ ]
+ },
"contextFromHeaders": "function",
"contextFromHeaders": "function",
"coupons": {
"coupons": {
"object": [
"object": [
@@ -165,6 +188,11 @@
@@ -165,6 +188,11 @@
"verifyDeliveryV2"
"verifyDeliveryV2"
]
]
},
},
+ "feed": {
+ "object": [
+ "createFeedClient"
+ ]
+ },
"food": {
"food": {
"object": [
"object": [
"createFoodClient"
"createFoodClient"
@@ -323,6 +351,13 @@
@@ -323,6 +351,13 @@
"createPlayClient"
"createPlayClient"
]
]
},
},
+ "player": {
+ "object": [
+ "current",
+ "handoff",
+ "on"
+ ]
+ },
"projects": {
"projects": {
"object": [
"object": [
"createProjectsClient"
"createProjectsClient"
@@ -516,6 +551,12 @@
@@ -516,6 +551,12 @@
"openvibe-sdk/actor": {
"openvibe-sdk/actor": {
"createActorClient": "function"
"createActorClient": "function"
},
},
+ "openvibe-sdk/agent-sheet": {
+ "createAgentSheetClient": "function"
+ },
+ "openvibe-sdk/agents": {
+ "createAgentsClient": "function"
+ },
"openvibe-sdk/ai": {
"openvibe-sdk/ai": {
"CLASSES": "array",
"CLASSES": "array",
"OPS": "array",
"OPS": "array",
@@ -636,6 +677,9 @@
@@ -636,6 +677,9 @@
"verifySlack": "function",
"verifySlack": "function",
"verifyTelegram": "function"
"verifyTelegram": "function"
},
},
+ "openvibe-sdk/codes": {
+ "createCodesClient": "function"
+ },
"openvibe-sdk/commerce": {
"openvibe-sdk/commerce": {
"CommerceError": "class",
"CommerceError": "class",
"createCommerceClient": "function",
"createCommerceClient": "function",
@@ -646,6 +690,12 @@
@@ -646,6 +690,12 @@
"actingHeaders": "function",
"actingHeaders": "function",
"createCommunityClient": "function"
"createCommunityClient": "function"
},
},
+ "openvibe-sdk/composer": {
+ "ATTACHMENT_MAX": "number",
+ "MEDIA_ORIGIN": "string",
+ "NETWORK_ORIGIN": "string",
+ "createComposerClient": "function"
+ },
"openvibe-sdk/compute": {
"openvibe-sdk/compute": {
"STRATEGIES": "array",
"STRATEGIES": "array",
"TIERS": "array",
"TIERS": "array",
@@ -791,12 +841,16 @@
@@ -791,12 +841,16 @@
"NETWORK_BASE_URL": "string",
"NETWORK_BASE_URL": "string",
"RETRY_STATUS": "object",
"RETRY_STATUS": "object",
"SIGNATURE_TOLERANCE_SEC": "number",
"SIGNATURE_TOLERANCE_SEC": "number",
+ "createCatalogSearchClient": "function",
"createExchangeClient": "function",
"createExchangeClient": "function",
"verifyWebhook": "function"
"verifyWebhook": "function"
},
},
"openvibe-sdk/explicit": {
"openvibe-sdk/explicit": {
"isExplicit": "function"
"isExplicit": "function"
},
},
+ "openvibe-sdk/feed": {
+ "createFeedClient": "function"
+ },
"openvibe-sdk/food": {
"openvibe-sdk/food": {
"createFoodClient": "function"
"createFoodClient": "function"
},
},
@@ -1031,6 +1085,11 @@
@@ -1031,6 +1085,11 @@
"openvibe-sdk/play": {
"openvibe-sdk/play": {
"createPlayClient": "function"
"createPlayClient": "function"
},
},
+ "openvibe-sdk/player": {
+ "current": "function",
+ "handoff": "function",
+ "on": "function"
+ },
"openvibe-sdk/projects": {
"openvibe-sdk/projects": {
"createProjectsClient": "function"
"createProjectsClient": "function"
},
},
@@ -1176,6 +1235,7 @@
@@ -1176,6 +1235,7 @@
"refresh"
"refresh"
]
]
},
},
+ "HANDOFF_PARAM": "string",
"claimsToUser": "function",
"claimsToUser": "function",
"createSsoClient": "function",
"createSsoClient": "function",
"decodeJwtPayload": "function",
"decodeJwtPayload": "function",
@@ -1183,6 +1243,7 @@
@@ -1183,6 +1243,7 @@
"fedcmNonceMatches": "function",
"fedcmNonceMatches": "function",
"parseCookies": "function",
"parseCookies": "function",
"sanitizeNext": "function",
"sanitizeNext": "function",
+ "stripHandoffParam": "function",
"withParam": "function"
"withParam": "function"
},
},
"openvibe-sdk/storage": {
"openvibe-sdk/storage": {
new file mode 100644
new file mode 100644
@@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
+// Generated by scripts/esm.js from src/agent-sheet.js. Do not edit.
+import cjs from '../src/agent-sheet.js';
+
+export const {
+ createAgentSheetClient,
+} = 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/agents.js. Do not edit.
+import cjs from '../src/agents.js';
+
+export const {
+ createAgentsClient,
+} = 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/codes.js. Do not edit.
+import cjs from '../src/codes.js';
+
+export const {
+ createCodesClient,
+} = 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/composer.js. Do not edit.
+import cjs from '../src/composer.js';
+
+export const {
+ createComposerClient, MEDIA_ORIGIN, NETWORK_ORIGIN, ATTACHMENT_MAX,
+} = cjs;
+export default cjs;
@@ -2,7 +2,7 @@
@@ -2,7 +2,7 @@
import cjs from '../src/exchange.js';
import cjs from '../src/exchange.js';
export const {
export const {
- createExchangeClient, ExchangeError, verifyWebhook, SIGNATURE_TOLERANCE_SEC, EXCHANGE_BASE_URL, NETWORK_BASE_URL,
- DEFAULT_PATHS, RETRY_STATUS,
+ createCatalogSearchClient, createExchangeClient, ExchangeError, verifyWebhook, SIGNATURE_TOLERANCE_SEC, EXCHANGE_BASE_URL,
+ NETWORK_BASE_URL, DEFAULT_PATHS, RETRY_STATUS,
} = cjs;
} = cjs;
export default 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/feed.js. Do not edit.
+import cjs from '../src/feed.js';
+
+export const {
+ createFeedClient,
+} = cjs;
+export default cjs;
@@ -7,14 +7,15 @@ export const {
@@ -7,14 +7,15 @@ export const {
isActingSubjectId, satisfiesRange, compareVersions, CONTRACTS_RANGE, DEFAULT_NETWORK, SDK_VERSION,
isActingSubjectId, satisfiesRange, compareVersions, CONTRACTS_RANGE, DEFAULT_NETWORK, SDK_VERSION,
report, share, locker, auth, registry, identity,
report, share, locker, auth, registry, identity,
trust, levels, modules, events, realtime, media,
trust, levels, modules, events, realtime, media,
- cam, mediaUpload, ui, community, homeFeed, bot,
- search, bench, meme, vision, themes, design,
- redact, chat, chatApps, ai, credentials, jobs,
- tools, projects, resources, vip, quest, live,
- inventory, tips, blog, reviews, wiki, coupons,
- deals, shoes, trade, space, host, news,
- food, actor, run, video, pics, work,
- zone, git, play, download, openre, geo,
- history, usage, commerce, moneyReasons, telemetry, userCard,
+ composer, cam, mediaUpload, ui, community, homeFeed,
+ feed, bot, search, bench, meme, vision,
+ themes, design, redact, chat, chatApps, ai,
+ credentials, jobs, agents, tools, projects, resources,
+ vip, quest, live, inventory, tips, blog,
+ reviews, wiki, coupons, deals, shoes, trade,
+ space, host, news, food, actor, codes,
+ run, video, pics, work, zone, git,
+ play, download, openre, geo, history, usage,
+ commerce, moneyReasons, telemetry, userCard, player, agentSheet,
} = cjs;
} = cjs;
export default 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/player.js. Do not edit.
+import cjs from '../src/player.js';
+
+export const {
+ current, handoff, on,
+} = cjs;
+export default cjs;
@@ -2,7 +2,7 @@
@@ -2,7 +2,7 @@
import cjs from '../src/sso/index.js';
import cjs from '../src/sso/index.js';
export const {
export const {
- createSsoClient, sanitizeNext, withParam, decodeJwtPayload, fedcmNonceMatches, claimsToUser,
- parseCookies, failurePage, COOKIES,
+ createSsoClient, sanitizeNext, withParam, stripHandoffParam, decodeJwtPayload, fedcmNonceMatches,
+ claimsToUser, parseCookies, failurePage, HANDOFF_PARAM, COOKIES,
} = cjs;
} = cjs;
export default cjs;
export default cjs;
@@ -21,11 +21,13 @@ module.exports = {
@@ -21,11 +21,13 @@ module.exports = {
events: require('./src/events'),
events: require('./src/events'),
realtime: require('./src/realtime'),
realtime: require('./src/realtime'),
media: require('./src/media'),
media: require('./src/media'),
+ composer: require('./src/composer'),
cam: require('./src/cam'),
cam: require('./src/cam'),
mediaUpload: require('./src/media-upload'),
mediaUpload: require('./src/media-upload'),
ui: require('./src/ui'),
ui: require('./src/ui'),
community: require('./src/community'),
community: require('./src/community'),
homeFeed: require('./src/home-feed'),
homeFeed: require('./src/home-feed'),
+ feed: require('./src/feed'),
bot: require('./src/bot'),
bot: require('./src/bot'),
search: require('./src/search'),
search: require('./src/search'),
bench: require('./src/bench'),
bench: require('./src/bench'),
@@ -39,6 +41,7 @@ module.exports = {
@@ -39,6 +41,7 @@ module.exports = {
ai: require('./src/ai'),
ai: require('./src/ai'),
credentials: require('./src/credentials'),
credentials: require('./src/credentials'),
jobs: require('./src/jobs'),
jobs: require('./src/jobs'),
+ agents: require('./src/agents'),
tools: require('./src/tools'),
tools: require('./src/tools'),
projects: require('./src/projects'),
projects: require('./src/projects'),
resources: require('./src/resources'),
resources: require('./src/resources'),
@@ -59,6 +62,7 @@ module.exports = {
@@ -59,6 +62,7 @@ module.exports = {
news: require('./src/news'),
news: require('./src/news'),
food: require('./src/food'),
food: require('./src/food'),
actor: require('./src/actor'),
actor: require('./src/actor'),
+ codes: require('./src/codes'),
run: require('./src/run'),
run: require('./src/run'),
video: require('./src/video'),
video: require('./src/video'),
pics: require('./src/pics'),
pics: require('./src/pics'),
@@ -75,4 +79,6 @@ module.exports = {
@@ -75,4 +79,6 @@ module.exports = {
moneyReasons: require('./src/money-reasons'),
moneyReasons: require('./src/money-reasons'),
telemetry: require('./src/telemetry'),
telemetry: require('./src/telemetry'),
userCard: require('./src/user-card'),
userCard: require('./src/user-card'),
+ player: require('./src/player'),
+ agentSheet: require('./src/agent-sheet'),
};
};
@@ -1,12 +1,12 @@
@@ -1,12 +1,12 @@
{
{
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.147.0",
+ "version": "0.148.0",
"lockfileVersion": 3,
"lockfileVersion": 3,
"requires": true,
"requires": true,
"packages": {
"packages": {
"": {
"": {
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.147.0",
+ "version": "0.148.0",
"license": "MIT",
"license": "MIT",
"bin": {
"bin": {
"openvibe-download": "bin/openvibe-download.js"
"openvibe-download": "bin/openvibe-download.js"
@@ -14,7 +14,7 @@
@@ -14,7 +14,7 @@
"devDependencies": {
"devDependencies": {
"@electric-sql/pglite": "^0.5.8",
"@electric-sql/pglite": "^0.5.8",
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.399.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.406.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
},
},
"engines": {
"engines": {
@@ -198,9 +198,9 @@
@@ -198,9 +198,9 @@
"license": "MIT"
"license": "MIT"
},
},
"node_modules/openvibe-contracts": {
"node_modules/openvibe-contracts": {
- "version": "0.399.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.399.0",
- "integrity": "sha512-7QqSrMH2BT6ZOJu+xxusjEPICaVKGrr+3ij3tg0leE3sy4jfPwvWVTwaXZSCPM1feoP7heUDnSO4/9QBmjguJw==",
+ "version": "0.406.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.406.0",
+ "integrity": "sha512-V/Tjss2UWvzI551bw3ACb/RqWg0lChxGITxfCVZ8qou2Docg0pBMvIjkeQoib56yq3r6UZGB6DGl+pA+qfjOiA==",
"dev": true,
"dev": true,
"license": "AGPL-3.0-only",
"license": "AGPL-3.0-only",
"dependencies": {
"dependencies": {
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.147.0",
+ "version": "0.148.0",
"description": "Supported browser and server clients for the OpenVibe platform: auth (incl. developer apps), registry discovery, identity, user modules, events, realtime, media, community, Bot robots, search, chat, AI runs, Tools (registry, run API and jobs), VIP memberships and developer projects, the async PostgreSQL data layer (db) and the Valkey cache, limits store, queue and pub/sub, with deadlines, safe retries, problem errors and trace propagation.",
"description": "Supported browser and server clients for the OpenVibe platform: auth (incl. developer apps), registry discovery, identity, user modules, events, realtime, media, community, Bot robots, search, chat, AI runs, Tools (registry, run API and jobs), VIP memberships and developer projects, the async PostgreSQL data layer (db) and the Valkey cache, limits store, queue and pub/sub, 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",
@@ -84,6 +84,11 @@
@@ -84,6 +84,11 @@
"import": "./esm/media.mjs",
"import": "./esm/media.mjs",
"default": "./src/media.js"
"default": "./src/media.js"
},
},
+ "./composer": {
+ "types": "./types/composer.d.ts",
+ "import": "./esm/composer.mjs",
+ "default": "./src/composer.js"
+ },
"./media-upload": {
"./media-upload": {
"types": "./types/media-upload.d.ts",
"types": "./types/media-upload.d.ts",
"import": "./esm/media-upload.mjs",
"import": "./esm/media-upload.mjs",
@@ -288,6 +293,11 @@
@@ -288,6 +293,11 @@
"import": "./esm/jobs.mjs",
"import": "./esm/jobs.mjs",
"default": "./src/jobs.js"
"default": "./src/jobs.js"
},
},
+ "./agents": {
+ "types": "./types/agents.d.ts",
+ "import": "./esm/agents.mjs",
+ "default": "./src/agents.js"
+ },
"./tools": {
"./tools": {
"types": "./types/tools.d.ts",
"types": "./types/tools.d.ts",
"import": "./esm/tools.mjs",
"import": "./esm/tools.mjs",
@@ -315,6 +325,12 @@
@@ -315,6 +325,12 @@
"import": "./esm/home-feed.mjs",
"import": "./esm/home-feed.mjs",
"default": "./src/home-feed.js"
"default": "./src/home-feed.js"
},
},
+ "./feed": {
+ "types": "./types/feed.d.ts",
+ "browser": "./src/feed.js",
+ "import": "./esm/feed.mjs",
+ "default": "./src/feed.js"
+ },
"./geo": {
"./geo": {
"types": "./types/geo.d.ts",
"types": "./types/geo.d.ts",
"import": "./esm/geo.mjs",
"import": "./esm/geo.mjs",
@@ -594,6 +610,24 @@
@@ -594,6 +610,24 @@
"types": "./types/user-card.d.ts",
"types": "./types/user-card.d.ts",
"import": "./esm/user-card.mjs",
"import": "./esm/user-card.mjs",
"default": "./src/user-card.js"
"default": "./src/user-card.js"
+ },
+ "./player": {
+ "types": "./types/player.d.ts",
+ "browser": "./src/player.js",
+ "import": "./esm/player.mjs",
+ "default": "./src/player.js"
+ },
+ "./codes": {
+ "types": "./types/codes.d.ts",
+ "browser": null,
+ "import": "./esm/codes.mjs",
+ "default": "./src/codes.js"
+ },
+ "./agent-sheet": {
+ "types": "./types/agent-sheet.d.ts",
+ "browser": "./src/agent-sheet.js",
+ "import": "./esm/agent-sheet.mjs",
+ "default": "./src/agent-sheet.js"
}
}
},
},
"bin": {
"bin": {
@@ -639,7 +673,7 @@
@@ -639,7 +673,7 @@
"devDependencies": {
"devDependencies": {
"@electric-sql/pglite": "^0.5.8",
"@electric-sql/pglite": "^0.5.8",
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.399.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.406.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
},
},
"peerDependencies": {
"peerDependencies": {
@@ -13,6 +13,7 @@ const path = require('path');
@@ -13,6 +13,7 @@ const path = require('path');
const ROOT = path.join(__dirname, '..');
const ROOT = path.join(__dirname, '..');
const ENTRIES = {
const ENTRIES = {
'user-card.mjs': '../src/user-card.js',
'user-card.mjs': '../src/user-card.js',
+ 'agent-sheet.mjs': '../src/agent-sheet.js',
'wallet.mjs': '../src/wallet.js',
'wallet.mjs': '../src/wallet.js',
'compute.mjs': '../src/compute/index.js',
'compute.mjs': '../src/compute/index.js',
'share.mjs': '../src/share.js',
'share.mjs': '../src/share.js',
@@ -36,6 +37,7 @@ const ENTRIES = {
@@ -36,6 +37,7 @@ const ENTRIES = {
'cam.mjs': '../src/cam/index.js',
'cam.mjs': '../src/cam/index.js',
'realtime.mjs': '../src/realtime.js',
'realtime.mjs': '../src/realtime.js',
'media.mjs': '../src/media.js',
'media.mjs': '../src/media.js',
+ 'composer.mjs': '../src/composer.js',
'media-upload.mjs': '../src/media-upload.js',
'media-upload.mjs': '../src/media-upload.js',
'ui.mjs': '../src/ui.js',
'ui.mjs': '../src/ui.js',
'community.mjs': '../src/community.js',
'community.mjs': '../src/community.js',
@@ -71,6 +73,7 @@ const ENTRIES = {
@@ -71,6 +73,7 @@ const ENTRIES = {
'chat-apps.mjs': '../src/chat-apps.js',
'chat-apps.mjs': '../src/chat-apps.js',
'ai.mjs': '../src/ai.js',
'ai.mjs': '../src/ai.js',
'jobs.mjs': '../src/jobs.js',
'jobs.mjs': '../src/jobs.js',
+ 'agents.mjs': '../src/agents.js',
'tools.mjs': '../src/tools.js',
'tools.mjs': '../src/tools.js',
'projects.mjs': '../src/projects.js',
'projects.mjs': '../src/projects.js',
'geo.mjs': '../src/geo.js',
'geo.mjs': '../src/geo.js',
@@ -80,6 +83,7 @@ const ENTRIES = {
@@ -80,6 +83,7 @@ const ENTRIES = {
'download.mjs': '../src/download.js',
'download.mjs': '../src/download.js',
'quest.mjs': '../src/quest.js',
'quest.mjs': '../src/quest.js',
'live.mjs': '../src/live.js',
'live.mjs': '../src/live.js',
+ 'player.mjs': '../src/player.js',
'streams.mjs': '../src/streams.js',
'streams.mjs': '../src/streams.js',
'reports.mjs': '../src/reports.js',
'reports.mjs': '../src/reports.js',
'inventory.mjs': '../src/inventory.js',
'inventory.mjs': '../src/inventory.js',
@@ -101,6 +105,7 @@ const ENTRIES = {
@@ -101,6 +105,7 @@ const ENTRIES = {
'front-page.mjs': '../src/front-page.js',
'front-page.mjs': '../src/front-page.js',
'food.mjs': '../src/food.js',
'food.mjs': '../src/food.js',
'actor.mjs': '../src/actor.js',
'actor.mjs': '../src/actor.js',
+ 'codes.mjs': '../src/codes.js',
'run.mjs': '../src/run.js',
'run.mjs': '../src/run.js',
'video.mjs': '../src/video.js',
'video.mjs': '../src/video.js',
'pics.mjs': '../src/pics.js',
'pics.mjs': '../src/pics.js',
@@ -111,6 +116,7 @@ const ENTRIES = {
@@ -111,6 +116,7 @@ const ENTRIES = {
'frame.mjs': '../src/frame.js',
'frame.mjs': '../src/frame.js',
'menu.mjs': '../src/menu/index.js',
'menu.mjs': '../src/menu/index.js',
'home-feed.mjs': '../src/home-feed.js',
'home-feed.mjs': '../src/home-feed.js',
+ 'feed.mjs': '../src/feed.js',
'service.mjs': '../src/service/index.js',
'service.mjs': '../src/service/index.js',
'testing.mjs': '../src/testing/index.js',
'testing.mjs': '../src/testing/index.js',
};
};
@@ -109,6 +109,7 @@ function createActorClient(client, { baseUrl } = {}) {
@@ -109,6 +109,7 @@ function createActorClient(client, { baseUrl } = {}) {
/** A personal agent: name, instructions, mode, budget_per_run_usd, daily_runs_max, schedule, … */
/** A personal agent: name, instructions, mode, budget_per_run_usd, daily_runs_max, schedule, … */
create: (agent, { signal } = {}) => once({ method: 'POST', path: '/api/v1/me/agents', json: agent, signal }),
create: (agent, { signal } = {}) => once({ method: 'POST', path: '/api/v1/me/agents', json: agent, signal }),
get: (agent, { signal } = {}) => call({ path: agentPath(agent), signal }),
get: (agent, { signal } = {}) => call({ path: agentPath(agent), signal }),
+ sheet: (agent, { signal } = {}) => call({ path: agentPath(agent), query: { view: 'sheet' }, signal }),
update: (agent, patch, { signal } = {}) => call({ method: 'PATCH', path: agentPath(agent), json: patch, idempotent: true, signal }),
update: (agent, patch, { signal } = {}) => call({ method: 'PATCH', path: agentPath(agent), json: patch, idempotent: true, signal }),
remove: (agent, { signal } = {}) => call({ method: 'DELETE', path: agentPath(agent), signal }),
remove: (agent, { signal } = {}) => call({ method: 'DELETE', path: agentPath(agent), signal }),
/** Run it now with an input → the task it started (follow it with tasks.events). */
/** Run it now with an input → the task it started (follow it with tasks.events). */
new file mode 100644
new file mode 100644
@@ -0,0 +1,28 @@
@@ -0,0 +1,28 @@
+'use strict';
+/** Owner-only agent sheets through Network's fixed cross-site relay. The caller supplies their own Bearer. */
+const ACTOR = /^agt_[0-9A-HJKMNP-TV-Z]{26}$/;
+const MEMORY = /^mem_[0-9A-HJKMNP-TV-Z]{26}$/;
+const CODES = /^[a-z][a-z0-9-]{0,63}$/;
+
+function createAgentSheetClient(client, { baseUrl } = {}) {
+ if (!client || typeof client.json !== 'function') throw new TypeError('createAgentSheetClient: pass an SDK core client');
+ const id = (value, pattern, kind) => {
+ if (!pattern.test(String(value || ''))) throw new TypeError(`invalid ${kind} id`);
+ return String(value);
+ };
+ const path = (agent) => `/api/frame/agents/${id(agent, ACTOR, 'Actor agent')}`;
+ const call = (opts) => client.json({ service: 'network', audience: 'openvibe.network', baseUrl, ...opts });
+ return {
+ sheet: (agent, { signal } = {}) => call({ path: `${path(agent)}/sheet`, signal }),
+ codesSheet: (agent, { signal } = {}) => call({ path: `/api/frame/codes-agents/${id(agent, CODES, 'coding agent')}/sheet`, signal }),
+ pause: (agent, { signal } = {}) => call({ method: 'PATCH', path: path(agent), json: { status: 'paused' }, signal, idempotent: true }),
+ resume: (agent, { signal } = {}) => call({ method: 'PATCH', path: path(agent), json: { status: 'active' }, signal, idempotent: true }),
+ memory: {
+ add: (agent, text, { signal } = {}) => call({ method: 'POST', path: `${path(agent)}/memory`, json: { text }, signal, idempotent: false }),
+ update: (agent, note, text, { signal } = {}) => call({ method: 'PATCH', path: `${path(agent)}/memory/${id(note, MEMORY, 'memory')}`, json: { text }, signal, idempotent: true }),
+ remove: (agent, note, { signal } = {}) => call({ method: 'DELETE', path: `${path(agent)}/memory/${id(note, MEMORY, 'memory')}`, signal, idempotent: true }),
+ },
+ };
+}
+
+module.exports = { createAgentSheetClient };
new file mode 100644
new file mode 100644
@@ -0,0 +1,52 @@
@@ -0,0 +1,52 @@
+'use strict';
+/** Agent run snapshots, AG-UI event streams and approval responses. */
+const { OpenVibeError, isOpenVibeError } = require('./core/errors');
+const { parseSSE } = require('./realtime');
+const enc = encodeURIComponent;
+const pause = (ms, signal) => new Promise((resolve) => {
+ const t = setTimeout(resolve, ms);
+ if (signal) signal.addEventListener('abort', () => { clearTimeout(t); resolve(); }, { once: true });
+});
+function createAgentsClient(client, { baseUrl, service = 'network', audience = 'openvibe.network' } = {}) {
+ const call = (opts) => client.request({ service, baseUrl, audience, ...opts });
+ async function run(id, { signal } = {}) {
+ if (!id) throw new TypeError('agents.run: id is required');
+ try { return (await call({ path: `/api/v1/runs/${enc(id)}`, signal })).data; }
+ catch (err) { if (isOpenVibeError(err) && err.status === 404) return null; throw err; }
+ }
+ async function post(id, approval, { signal } = {}) {
+ if (!id) throw new TypeError('agents.post: id is required');
+ if (!approval || typeof approval !== 'object' || Array.isArray(approval)) throw new TypeError('agents.post: approval must be an object');
+ return (await call({ method: 'POST', path: `/api/v1/runs/${enc(id)}/approvals`, json: approval, signal })).data;
+ }
+ async function* events(id, { lastEventId = 0, signal, maxReconnects = 20, reconnectDelayMs = 1000 } = {}) {
+ if (!id) throw new TypeError('agents.events: id is required');
+ let last = String(lastEventId || '0'), failures = 0, serverRetryMs = 0;
+ while (!(signal && signal.aborted)) {
+ let delivered = false;
+ try {
+ const res = await call({ path: `/api/v1/runs/${enc(id)}/events`, responseType: 'response', retries: 0, signal,
+ headers: { Accept: 'text/event-stream', 'Cache-Control': 'no-cache', 'Last-Event-ID': last } });
+ if (res.status === 204) return;
+ for await (const msg of parseSSE(res.data.body, { onRetry: (ms) => { serverRetryMs = ms; } })) {
+ if (msg.id !== undefined && msg.id !== '') { if (/^\d+$/.test(msg.id) && Number(msg.id) <= Number(last)) continue; last = msg.id; }
+ let data; try { data = JSON.parse(msg.data); } catch { data = null; }
+ delivered = true; failures = 0;
+ yield { id: msg.id ?? null, event: msg.event || (data && data.type) || null, data };
+ if (data && ['RUN_FINISHED', 'RUN_ERROR'].includes(data.type)) return;
+ }
+ } catch (err) {
+ if (signal && signal.aborted) return;
+ if (isOpenVibeError(err) && err.status >= 400 && err.status < 500) throw err;
+ if (!isOpenVibeError(err) && !['TypeError', 'AbortError'].includes(err && err.name)) throw err;
+ }
+ failures = delivered ? 0 : failures + 1;
+ if (failures > maxReconnects) throw new OpenVibeError({ code: 'sdk.network_error', retryable: true, message: `agents: event stream ${id} failed ${failures} times in a row` });
+ const backoffMs = Math.min(reconnectDelayMs * (2 ** Math.max(0, failures - 1)), 30000);
+ const waitMs = Math.min(Math.max(backoffMs, serverRetryMs), 30000);
+ await pause(waitMs, signal);
+ }
+ }
+ return { run, events, post };
+}
+module.exports = { createAgentsClient };
@@ -72,6 +72,14 @@ function createChatClient(client, defaults = {}) {
@@ -72,6 +72,14 @@ function createChatClient(client, defaults = {}) {
history,
history,
/** The typed address previews on a message (chat.embed@1): [] when it has none. */
/** The typed address previews on a message (chat.embed@1): [] when it has none. */
embeds: (message) => readEmbeds(message),
embeds: (message) => readEmbeds(message),
+ /** Compacted day, week or month memory for a person (chat.person-memory-result@1). */
+ people: {
+ memory: (username, { channel, level = 'day' } = {}, o = {}) => call({ path: `/api/v1/people/${enc(String(username))}/memory`, query: { channel, level } }, o),
+ /** Erase the signed-in person's memory and opt out. */
+ forget: (username, o = {}) => call({ method: 'DELETE', path: `/api/v1/people/${enc(String(username))}/memory`, ...once }, o),
+ /** Set the signed-in person's opt-out preference. */
+ preferences: (username, optedOut, o = {}) => call({ method: 'PUT', path: `/api/v1/people/${enc(String(username))}/memory/preferences`, json: { opted_out: !!optedOut }, ...once }, o),
+ },
/** Vibe's model comparison API, authenticated as the person. */
/** Vibe's model comparison API, authenticated as the person. */
ask: (input, o = {}) => call({ method: 'POST', path: '/api/vibe/ask', json: input, ...once }, o),
ask: (input, o = {}) => call({ method: 'POST', path: '/api/vibe/ask', json: input, ...once }, o),
pick: (messageId, model, o = {}) => call({ method: 'POST', path: '/api/vibe/pick', json: { message_id: messageId, model }, ...once }, o),
pick: (messageId, model, o = {}) => call({ method: 'POST', path: '/api/vibe/pick', json: { message_id: messageId, model }, ...once }, o),
new file mode 100644
new file mode 100644
@@ -0,0 +1,56 @@
@@ -0,0 +1,56 @@
+'use strict';
+/**
+ * openvibe-sdk/codes — OpenVibe.Codes for apps: the person's coding tasks as the Dock's Code tab lists, starts and
+ * stops them. Routes at https://openvibe.codes/api/v1; the person's own Network access token.
+ *
+ * const codes = createCodesClient(createClient({ tokenProvider }));
+ * const { tasks } = await codes.tasks.list({ limit: 8 }); // running first, codes.task-summary@1
+ * const { id, url } = await codes.tasks.create({ text: 'Add a test.', mode: 'repo', repo: 'octocat/Hello-World' });
+ * await codes.tasks.stop(id); // { ok: true }
+ *
+ * A task is its requester's own: another person's id is 404. Creates are never retried on their own (a retry would
+ * start a second task); neither is a stop (a task already ended answers 409). The person token rides every call.
+ */
+const enc = encodeURIComponent;
+
+const TASK = /^tsk_[0-9A-HJKMNP-TV-Z]{26}$/;
+const MODES = ['create', 'openvibe', 'repo'];
+
+function createCodesClient(client, { baseUrl } = {}) {
+ if (!client || typeof client.json !== 'function' || typeof client.request !== 'function') throw new TypeError('createCodesClient: pass an openvibe-sdk/core client');
+ const at = { service: 'codes', baseUrl, audience: 'openvibe.codes' };
+ const call = (opts) => client.json({ ...at, ...opts });
+ const once = (opts) => call({ ...opts, idempotencyKey: false });
+ const id = (v) => {
+ if (!TASK.test(String(v || ''))) throw new TypeError('codes: a task id is tsk_…');
+ return enc(String(v));
+ };
+
+ return {
+ tasks: {
+ /** The person's latest tasks, running first (codes.task-summary@1). */
+ list: ({ limit = 8, signal } = {}) => {
+ const n = Number(limit);
+ if (!Number.isInteger(n) || n < 1 || n > 50) throw new TypeError('codes.tasks.list: limit is 1-50');
+ return call({ path: '/api/v1/me/tasks', query: { limit: n }, signal });
+ },
+ /** Start one the Dock's way: the words, where it starts, and what it carries. → { id, url }. */
+ create: ({ text, mode, repo, attachments } = {}, { signal } = {}) => {
+ if (typeof text !== 'string' || !text.trim()) throw new TypeError('codes.tasks.create: text is the instruction in plain words');
+ if (!MODES.includes(mode)) throw new TypeError('codes.tasks.create: mode is create, openvibe or repo');
+ if (repo != null && (typeof repo !== 'string' || !repo.trim())) throw new TypeError('codes.tasks.create: repo is a repository address');
+ if (attachments != null && (!Array.isArray(attachments) || attachments.some((a) => typeof a !== 'string'))) {
+ throw new TypeError('codes.tasks.create: attachments are composer attachment ids');
+ }
+ return once({
+ method: 'POST', path: '/api/v1/tasks',
+ json: { text, mode, ...(repo ? { repo } : {}), ...(attachments ? { attachments } : {}) }, signal,
+ });
+ },
+ /** Stop the running attempt; nothing carries it on. → { ok: true }. */
+ stop: (taskId, { signal } = {}) => once({ method: 'POST', path: `/api/v1/tasks/${id(taskId)}/stop`, signal }),
+ },
+ };
+}
+
+module.exports = { createCodesClient };
@@ -113,6 +113,8 @@ function createCommunityClient(client, defaults = {}) {
@@ -113,6 +113,8 @@ function createCommunityClient(client, defaults = {}) {
query: { kind: query.kind, site: query.site, q: query.q, cursor: query.cursor, limit: query.limit,
query: { kind: query.kind, site: query.site, q: query.q, cursor: query.cursor, limit: query.limit,
following: query.following === true || query.following === 1 ? '1' : undefined },
following: query.following === true || query.following === 1 ? '1' : undefined },
}, o);
}, o);
+ // Named form used by new integrations; the callable form remains for existing SDK consumers.
+ feed.list = feed;
const posts = {
const posts = {
/**
/**
* A status post in the Feed at once: { text, pictures?: [{ media_id, alt }] } (at most four, alt required,
* A status post in the Feed at once: { text, pictures?: [{ media_id, alt }] } (at most four, alt required,
new file mode 100644
new file mode 100644
@@ -0,0 +1,140 @@
@@ -0,0 +1,140 @@
+'use strict';
+/**
+ * openvibe-sdk/composer: the shared composer's client, the same on every OpenVibe site.
+ *
+ * attachments.upload(file, { onProgress, signal, filename, contentType, xhr })
+ * POST /api/v1/attachments on Media (multipart `file`), through XMLHttpRequest so upload progress is real.
+ * -> composer.attachment@1
+ * attachments.remove(id)
+ * DELETE /api/v1/attachments/<id>; true when it went, false when it was already gone.
+ * composer.prefs.get() / composer.prefs.put(prefs, rev)
+ * GET/PUT /api/v1/me/composer on Network (composer.prefs@1). A PUT sends the revision it read; a stale one is
+ * 409 composer.rev_conflict and err.problem.current is the other tab's document.
+ * mentions.search(q, kinds)
+ * GET /api/v1/mentions on Network (composer.mention@1): people the caller follows first, then public people,
+ * repositories from OpenVibe Git, first-party sites. Signed-in only; at most 8.
+ *
+ * Browser-safe. The composer page calls Network directly for prefs and mentions (every site's connect-src allows it)
+ * and Media for the bytes (the site's CSP must allow OPENVIBE_MEDIA_ORIGIN).
+ */
+const { OpenVibeError, isOpenVibeError } = require('./core/errors');
+
+const MEDIA_ORIGIN = 'https://openvibe.media';
+const NETWORK_ORIGIN = 'https://openvibe.network';
+const ATTACHMENT_MAX = 300 * 1024 * 1024;
+const enc = encodeURIComponent;
+
+function toBlob(file, contentType) {
+ if (typeof Blob !== 'undefined' && file instanceof Blob) return file;
+ if (typeof file === 'string' || file instanceof ArrayBuffer || ArrayBuffer.isView(file)) {
+ return new Blob([file], contentType ? { type: contentType } : undefined);
+ }
+ throw new TypeError('attachments.upload: pass a File/Blob, ArrayBuffer, typed array (Buffer) or string');
+}
+
+function abortError() {
+ return new OpenVibeError({ code: 'sdk.aborted', message: 'the upload was aborted' });
+}
+
+function createComposerClient(client, { mediaBaseUrl, networkBaseUrl } = {}) {
+ if (!client || typeof client.json !== 'function') throw new TypeError('createComposerClient: an SDK client is required');
+ const net = (opts) => client.json({ service: 'network', audience: 'openvibe.network', ...(networkBaseUrl ? { baseUrl: networkBaseUrl } : {}), ...opts });
+
+ /** The Media token for an XHR upload (the XHR cannot borrow the fetch client's request loop). */
+ async function mediaToken() {
+ const o = client.options || {};
+ if (o.token != null) return o.token;
+ if (typeof o.getToken === 'function') return (await o.getToken({ service: 'media', audience: 'openvibe.media' })) || null;
+ return null;
+ }
+
+ const attachments = {
+ /**
+ * One file -> composer.attachment@1. Progress is the browser's upload progress (XHR), not a guess.
+ * `xhr` injects an XMLHttpRequest implementation (tests, a non-browser shell); otherwise the global one.
+ */
+ upload(file, { onProgress, signal, filename, contentType, xhr } = {}) {
+ return new Promise((resolve, reject) => {
+ const XHR = xhr || (typeof XMLHttpRequest !== 'undefined' ? XMLHttpRequest : null);
+ if (!XHR) return reject(new OpenVibeError({ code: 'sdk.xhr_unavailable', message: 'attachments.upload needs XMLHttpRequest (pass { xhr })' }));
+ let blob;
+ try { blob = toBlob(file, contentType); } catch (err) { return reject(err); }
+ if (blob.size > ATTACHMENT_MAX) {
+ return reject(new OpenVibeError({ code: 'media.attachment.too_large', status: 413, message: `a file may be at most ${ATTACHMENT_MAX / (1024 * 1024)} MB` }));
+ }
+ const url = client.url({ service: 'media', ...(mediaBaseUrl ? { baseUrl: mediaBaseUrl } : {}), path: '/api/v1/attachments' });
+ Promise.resolve(url).then(async (target) => {
+ if (signal && signal.aborted) return reject(abortError());
+ const fd = new FormData();
+ fd.append('file', blob, filename || (file && file.name) || 'file');
+ const req = new XHR();
+ let settled = false;
+ const finish = (fn, v) => { if (settled) return; settled = true; cleanup(); fn(v); };
+ const offAbort = signal && typeof signal.addEventListener === 'function' ? () => signal.removeEventListener('abort', onAbort) : () => {};
+ function onAbort() { try { req.abort(); } catch { /* */ } finish(reject, abortError()); }
+ const cleanup = () => { offAbort(); };
+ req.open('POST', target);
+ req.setRequestHeader('Accept', 'application/json');
+ const token = await mediaToken();
+ if (token) req.setRequestHeader('Authorization', `Bearer ${token}`);
+ if (req.upload && typeof onProgress === 'function') {
+ req.upload.onprogress = (e) => onProgress({ loaded: e.loaded || 0, total: e.total || blob.size, lengthComputable: e.lengthComputable !== false });
+ }
+ req.onload = () => {
+ let body = null;
+ try { body = req.responseText ? JSON.parse(req.responseText) : null; } catch { body = req.responseText || null; }
+ if (req.status >= 200 && req.status < 300) return finish(resolve, body);
+ const err = OpenVibeError.fromResponse({ status: req.status, body, method: 'POST', url: target });
+ if (body && body.current) err.current = body.current;
+ finish(reject, err);
+ };
+ req.onerror = () => finish(reject, new OpenVibeError({ code: 'sdk.network_error', message: 'the upload could not reach Media', method: 'POST', url: target }));
+ req.ontimeout = () => finish(reject, new OpenVibeError({ code: 'sdk.timeout', message: 'the upload timed out', method: 'POST', url: target }));
+ if (signal && typeof signal.addEventListener === 'function') signal.addEventListener('abort', onAbort, { once: true });
+ try { req.send(fd); } catch (err) { finish(reject, err); }
+ }).catch(reject);
+ });
+ },
+
+ /** DELETE /api/v1/attachments/<id>: true when it went, false when it was already gone. */
+ async remove(id) {
+ if (!id) throw new TypeError('attachments.remove: pass the attachment id');
+ try {
+ await client.json({ service: 'media', ...(mediaBaseUrl ? { baseUrl: mediaBaseUrl } : {}), audience: 'openvibe.media', method: 'DELETE', path: `/api/v1/attachments/${enc(id)}` });
+ return true;
+ } catch (err) {
+ if (isOpenVibeError(err) && err.status === 404) return false;
+ throw err;
+ }
+ },
+ };
+
+ const composer = {
+ prefs: {
+ /** composer.prefs@1, with its rev. */
+ get: () => net({ path: '/api/v1/me/composer' }),
+ /**
+ * composer.prefs@1 in (rev is the one you read; 0 for a new person), the new document out.
+ * A stale rev is 409 composer.rev_conflict with err.problem.current (and err.current).
+ */
+ put(prefs, rev) {
+ if (!prefs || typeof prefs !== 'object') throw new TypeError('composer.prefs.put: pass the prefs object');
+ if (!Number.isInteger(rev) || rev < 0) throw new TypeError('composer.prefs.put: pass the revision you read (0 for new)');
+ return net({ method: 'PUT', path: '/api/v1/me/composer', json: { ...prefs, rev } });
+ },
+ },
+ };
+
+ const mentions = {
+ /** @ menu entries (composer.mention@1[]). kinds: person | repo | site | file; omitted = all. */
+ async search(q, kinds) {
+ const list = Array.isArray(kinds) ? kinds : (kinds ? String(kinds).split(',') : []);
+ const data = await net({ path: '/api/v1/mentions', query: { q: q || undefined, kinds: list.length ? list.join(',') : undefined } });
+ return (data && Array.isArray(data.mentions)) ? data.mentions : [];
+ },
+ };
+
+ return { attachments, composer, mentions, MEDIA_ORIGIN, NETWORK_ORIGIN };
+}
+
+module.exports = { createComposerClient, MEDIA_ORIGIN, NETWORK_ORIGIN, ATTACHMENT_MAX };
@@ -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.147.0';
+const SDK_VERSION = '0.148.0';
module.exports = {
module.exports = {
createClient,
createClient,
@@ -47,6 +47,7 @@ const crypto = require('node:crypto');
@@ -47,6 +47,7 @@ const crypto = require('node:crypto');
const EXCHANGE_BASE_URL = 'https://api.openvibe.exchange';
const EXCHANGE_BASE_URL = 'https://api.openvibe.exchange';
/** The Network's origin: the user-owned AI offers live there. */
/** The Network's origin: the user-owned AI offers live there. */
const NETWORK_BASE_URL = 'https://api.openvibe.network';
const NETWORK_BASE_URL = 'https://api.openvibe.network';
+const CATALOG_SEARCH_URL = 'https://openvibe.exchange';
/** How far a signed webhook's timestamp may be from this server's clock, in seconds. */
/** How far a signed webhook's timestamp may be from this server's clock, in seconds. */
const SIGNATURE_TOLERANCE_SEC = 300;
const SIGNATURE_TOLERANCE_SEC = 300;
@@ -664,6 +665,7 @@ function createExchangeClient({
@@ -664,6 +665,7 @@ function createExchangeClient({
}
}
module.exports = {
module.exports = {
+ createCatalogSearchClient,
createExchangeClient,
createExchangeClient,
ExchangeError,
ExchangeError,
verifyWebhook,
verifyWebhook,
@@ -673,3 +675,18 @@ module.exports = {
@@ -673,3 +675,18 @@ module.exports = {
DEFAULT_PATHS,
DEFAULT_PATHS,
RETRY_STATUS,
RETRY_STATUS,
};
};
+
+/** A public, keyless reader for Exchange's model and provider search. */
+function createCatalogSearchClient({ baseUrl = CATALOG_SEARCH_URL, fetchImpl = globalThis.fetch } = {}) {
+ return {
+ async search(q, filters = {}, { signal } = {}) {
+ if (typeof q !== 'string' || q.length > 200) throw new TypeError('q must be a string of at most 200 characters');
+ const url = new URL('/api/v1/search', baseUrl);
+ for (const [key, value] of Object.entries({ ...filters, q })) if (value != null && value !== '') url.searchParams.set(key, String(value));
+ const res = await fetchImpl(url, { headers: { Accept: 'application/json' }, signal });
+ const body = await res.json();
+ if (!res.ok) throw new ExchangeError(body?.detail || body?.error || `Search answered ${res.status}`, { status: res.status, body });
+ return body;
+ },
+ };
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,15 @@
@@ -0,0 +1,15 @@
+'use strict';
+
+/** Browser-safe reader for the public, cross-site Feed cards on OpenVibe.Community. */
+function createFeedClient(client, { communityBaseUrl } = {}) {
+ if (!client || typeof client.json !== 'function') throw new TypeError('createFeedClient: pass an openvibe-sdk/core client');
+ return {
+ /** Read one page of cards; pass next_cursor back as cursor to continue. */
+ list: ({ kind, cursor } = {}, { signal } = {}) => client.json({
+ service: 'community', baseUrl: communityBaseUrl, audience: 'openvibe.community',
+ path: '/api/v1/feed', query: { kind, cursor }, signal,
+ }),
+ };
+}
+
+module.exports = { createFeedClient };
@@ -1,10 +1,11 @@
@@ -1,10 +1,11 @@
'use strict';
'use strict';
-/** The menu's portable Home layout, network day counts and personal wall. Pass a user-token core client;
- * contribute() needs an app token with the person's community.pulse.contribute grant. */
+/** The menu's portable Home layout, network day counts, what people do now and personal wall. Pass a user-token core
+ * client; contribute() needs an app token with the person's community.pulse.contribute grant. */
function createHomeFeedClient(client, { networkBaseUrl, communityBaseUrl } = {}) {
function createHomeFeedClient(client, { networkBaseUrl, communityBaseUrl } = {}) {
if (!client || typeof client.json !== 'function') throw new TypeError('createHomeFeedClient: pass an openvibe-sdk/core client');
if (!client || typeof client.json !== 'function') throw new TypeError('createHomeFeedClient: pass an openvibe-sdk/core client');
const network = (opts) => client.json({ service: 'network', baseUrl: networkBaseUrl, audience: 'openvibe.network', ...opts });
const network = (opts) => client.json({ service: 'network', baseUrl: networkBaseUrl, audience: 'openvibe.network', ...opts });
const community = (opts) => client.json({ service: 'community', baseUrl: communityBaseUrl, audience: 'openvibe.community', ...opts });
const community = (opts) => client.json({ service: 'community', baseUrl: communityBaseUrl, audience: 'openvibe.community', ...opts });
+ const limitQuery = (limit) => (limit == null ? undefined : { limit });
return {
return {
home: {
home: {
get: ({ signal } = {}) => network({ path: '/api/v1/me/home-screen', signal }),
get: ({ signal } = {}) => network({ path: '/api/v1/me/home-screen', signal }),
@@ -12,6 +13,10 @@ function createHomeFeedClient(client, { networkBaseUrl, communityBaseUrl } = {})
@@ -12,6 +13,10 @@ function createHomeFeedClient(client, { networkBaseUrl, communityBaseUrl } = {})
},
},
network: {
network: {
today: ({ signal } = {}) => network({ path: '/api/v1/frame/today', auth: false, signal }),
today: ({ signal } = {}) => network({ path: '/api/v1/frame/today', auth: false, signal }),
+ // The signed-out front door's list: Report's public aggregate, no credentials sent.
+ popular: ({ limit, signal } = {}) => network({ path: '/api/v1/home/popular', query: limitQuery(limit), auth: false, signal }),
+ // The signed-in person's own suggestions; Network computes them in the caller's request.
+ tryNext: ({ limit, signal } = {}) => network({ path: '/api/v1/me/try-next', query: limitQuery(limit), signal }),
},
},
feed: {
feed: {
list: ({ for: audience = 'me', cursor, signal } = {}) => community({ path: '/api/v1/pulse', query: { for: audience, cursor }, signal }),
list: ({ for: audience = 'me', cursor, signal } = {}) => community({ path: '/api/v1/pulse', query: { for: audience, cursor }, signal }),
new file mode 100644
new file mode 100644
@@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
+'use strict';
+/** Browser facade for Shared's portable live player. */
+function host() { return typeof window === 'undefined' ? null : window.OVOverlay || null; }
+function current() { return host()?.current() || null; }
+function handoff(url) { return host()?.handoffUrl(url) || url; }
+function on(listener) {
+ if (typeof listener !== 'function') throw new TypeError('listener must be a function');
+ if (typeof window === 'undefined') return () => {};
+ const receive = (event) => listener(event.detail);
+ window.addEventListener('openvibe:player', receive);
+ return () => window.removeEventListener('openvibe:player', receive);
+}
+module.exports = { current, handoff, on };
@@ -11,7 +11,8 @@
@@ -11,7 +11,8 @@
* networkUrl: 'https://openvibe.network', networkInternalUrl: 'http://127.0.0.1:4000',
* networkUrl: 'https://openvibe.network', networkInternalUrl: 'http://127.0.0.1:4000',
* secureCookies: true,
* secureCookies: true,
* });
* });
- * app.use('/auth', sso.router(express)); // /login /callback /fedcm /logout /me /refresh
+ * app.use('/auth', sso.router(express)); // /login /callback /fedcm /logout /me /refresh /health /handoff
+ * app.use(sso.handoff()); // ?ov_handoff= arrival: redeem before the page renders
* app.use(sso.optionalAuth()); // req.user, req.token when a valid session is presented
* app.use(sso.optionalAuth()); // req.user, req.token when a valid session is presented
* app.get('/private', sso.requireAuth(), handler); // 401 JSON without a session
* app.get('/private', sso.requireAuth(), handler); // 401 JSON without a session
*
*
@@ -35,6 +36,10 @@
@@ -35,6 +36,10 @@
* GET /health → { ok, client: 'ok' | 'invalid' | 'unknown' }: does the Network accept this site's OAuth client?
* GET /health → { ok, client: 'ok' | 'invalid' | 'unknown' }: does the Network accept this site's OAuth client?
* (a token request with a code that cannot exist: invalid_grant means the client is fine,
* (a token request with a code that cannot exist: invalid_grant means the client is fine,
* invalid_client means its id or secret is wrong; cached 10 minutes; 503 only for 'invalid')
* invalid_client means its id or secret is wrong; cached 10 minutes; 503 only for 'invalid')
+ * POST /handoff → { ticket, expires_in }: a one-time 60 s ticket for a link to another OpenVibe site
+ * (the caller's session proves the person; body: the destination's client_id + redirect_uri).
+ * Mount sso.handoff() early: an arrival carrying ?ov_handoff= redeems for this site's session
+ * before the page renders — no extra round trip, never switches a live session, never wakes a guest.
*
*
* When a sign-in cannot finish (0.73.0): a lost or expired flow (the state cookie is gone or does not match) and a code
* When a sign-in cannot finish (0.73.0): a lost or expired flow (the state cookie is gone or does not match) and a code
* the Network no longer accepts (invalid_grant: used twice, or too old) start the sign-in once more by themselves, with
* the Network no longer accepts (invalid_grant: used twice, or too old) start the sign-in once more by themselves, with
@@ -107,6 +112,24 @@ function withParam(target, key, value) {
@@ -107,6 +112,24 @@ function withParam(target, key, value) {
return `${base}${base.includes('?') ? '&' : '?'}${encodeURIComponent(key)}=${encodeURIComponent(value)}${hash}`;
return `${base}${base.includes('?') ? '&' : '?'}${encodeURIComponent(key)}=${encodeURIComponent(value)}${hash}`;
}
}
+/**
+ * The cross-site handoff ticket arrives as ?ov_handoff=<ticket> on a plain link (Shared's handoffLinks
+ * carries it; the source minted it at its own /auth/handoff). It is single-use and short-lived, so the
+ * first thing a redeem does is scrub it from the address bar with a same-site redirect.
+ */
+const HANDOFF_PARAM = 'ov_handoff';
+const TICKET_RE = /^ovh_[A-Za-z0-9_-]{20,200}$/;
+
+/** Remove the handoff ticket from a path+query URL, keeping everything else. Never throws. */
+function stripHandoffParam(target) {
+ try {
+ const u = new URL(String(target || '/'), 'https://site.invalid');
+ u.searchParams.delete(HANDOFF_PARAM);
+ const q = u.searchParams.toString();
+ return `${u.pathname}${q ? `?${q}` : ''}${u.hash}`;
+ } catch { return '/'; }
+}
+
/** A JWT's payload WITHOUT checking its signature: only to pre-check a FedCM nonce before the Network verifies it. */
/** A JWT's payload WITHOUT checking its signature: only to pre-check a FedCM nonce before the Network verifies it. */
function decodeJwtPayload(token) {
function decodeJwtPayload(token) {
if (typeof token !== 'string') return null;
if (typeof token !== 'string') return null;
@@ -238,6 +261,32 @@ function createSsoClient(opts = {}) {
@@ -238,6 +261,32 @@ function createSsoClient(opts = {}) {
}
}
const SIGN_IN_AGAIN = { title: 'Sign-in didn\'t finish', text: 'It took too long, or it started in another tab or app. Start it again and it will go through; your account is fine.' };
const SIGN_IN_AGAIN = { title: 'Sign-in didn\'t finish', text: 'It took too long, or it started in another tab or app. Start it again and it will go through; your account is fine.' };
+ /** A JSON POST to the Network: the internal URL first, the public one if it cannot be reached. */
+ async function networkPost(path, body, bearer) {
+ let lastErr = null;
+ for (const base of [...new Set([networkInternalUrl, networkUrl])]) {
+ try {
+ const res = await fetchImpl(`${base}${path}`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json', ...(bearer ? { Authorization: `Bearer ${bearer}` } : {}) },
+ body: JSON.stringify(body),
+ signal: AbortSignal.timeout(10_000),
+ });
+ const data = await res.json().catch(() => ({}));
+ if (!res.ok) {
+ throw Object.assign(new Error(data.error_description || data.error || `request failed (${res.status})`), {
+ status: res.status, error: data.error || 'invalid_grant', error_description: data.error_description || null,
+ });
+ }
+ return data;
+ } catch (err) {
+ lastErr = err;
+ if (err.status && err.status < 500) throw err; // the request itself is bad: another base will not help
+ }
+ }
+ throw lastErr || new Error('Network unreachable');
+ }
+
/** A token grant at the Network: the internal URL first, the public one if it cannot be reached. */
/** A token grant at the Network: the internal URL first, the public one if it cannot be reached. */
async function tokenGrant(body) {
async function tokenGrant(body) {
let lastErr = null;
let lastErr = null;
@@ -403,6 +452,31 @@ function createSsoClient(opts = {}) {
@@ -403,6 +452,31 @@ function createSsoClient(opts = {}) {
return res.status(502).json({ error: 'OpenVibe.Network could not be reached' });
return res.status(502).json({ error: 'OpenVibe.Network could not be reached' });
}
}
},
},
+
+ /**
+ * Mint a one-time cross-site handoff ticket for a link to another OpenVibe site. The caller's
+ * session proves the person (Bearer or cookie); the ticket is bound to the destination the caller
+ * names and lives 60 seconds. Shared's handoffLinks calls this same-origin on intent.
+ */
+ async handoffMint(req, res) {
+ const token = extractToken(req);
+ if (!await verify(token)) return res.status(401).json({ error: 'Sign in required' });
+ const { client_id, redirect_uri } = (req.body || {});
+ if (typeof client_id !== 'string' || !client_id || typeof redirect_uri !== 'string' || !redirect_uri) {
+ return res.status(400).json({ error: 'invalid_request', error_description: 'client_id and redirect_uri are required' });
+ }
+ try {
+ const data = await networkPost('/api/v1/handoff/tickets', { client_id, redirect_uri }, token);
+ if (!data || typeof data.ticket !== 'string' || !TICKET_RE.test(data.ticket)) {
+ throw Object.assign(new Error('Network issued no ticket'), { status: 502 });
+ }
+ return res.json({ ticket: data.ticket, expires_in: data.expires_in || 60 });
+ } catch (err) {
+ if (err.status && err.status < 500) return res.status(err.status).json({ error: err.error || 'invalid_request', error_description: err.error_description || err.message });
+ log.error(`[${site} auth] handoff mint failed: ${err.message}`);
+ return res.status(502).json({ error: 'OpenVibe.Network could not be reached' });
+ }
+ },
};
};
let health = null; // { client, at }: the last check, or the last sign-in's verdict
let health = null; // { client, at }: the last check, or the last sign-in's verdict
@@ -442,6 +516,7 @@ function createSsoClient(opts = {}) {
@@ -442,6 +516,7 @@ function createSsoClient(opts = {}) {
r.get('/me', wrap(handlers.me));
r.get('/me', wrap(handlers.me));
r.post('/refresh', wrap(handlers.refresh));
r.post('/refresh', wrap(handlers.refresh));
r.get('/health', wrap(handlers.health));
r.get('/health', wrap(handlers.health));
+ r.post('/handoff', json, badJson, wrap(handlers.handoffMint));
return r;
return r;
}
}
@@ -460,10 +535,45 @@ function createSsoClient(opts = {}) {
@@ -460,10 +535,45 @@ function createSsoClient(opts = {}) {
return (req, res, nextFn) => opt(req, res, () => (req.user ? nextFn() : res.status(401).json({ error: 'Sign in required' })));
return (req, res, nextFn) => opt(req, res, () => (req.user ? nextFn() : res.status(401).json({ error: 'Sign in required' })));
}
}
- return { router, handlers, optionalAuth, requireAuth, verify, extractToken, authorizeUrl, tokenGrant, checkClient, redirectUri, audience };
+ /**
+ * Arrival with ?ov_handoff=<ticket>: redeem it for this site's session before the page renders, so a
+ * link from another OpenVibe site lands signed in with no extra round trip. Mount early (before your
+ * pages): `app.use(sso.handoff())`. Rules, all enforced here, never in the page:
+ * - GET navigations only, and only when this browser has NO session here (never switch an account);
+ * - never when the browser carries this site's guest hint (a guest stays a guest, no Network call);
+ * - any redeem or exchange failure renders the page as a guest; the ticket is always scrubbed from
+ * the address bar with a same-site redirect, so a bearer ticket never sits in history.
+ */
+ function handoff() {
+ return (req, res, nextFn) => {
+ (async () => {
+ const ticket = (req.query || {})[HANDOFF_PARAM];
+ if (req.method !== 'GET' || typeof ticket !== 'string' || !TICKET_RE.test(ticket)) return nextFn();
+ if (parseCookies(req)[HINT_COOKIE] === 'guest') return nextFn();
+ if (await verify(extractToken(req))) return nextFn();
+ const target = req.originalUrl || req.url || '/';
+ const clean = stripHandoffParam(target);
+ try {
+ const data = await networkPost('/api/v1/handoff/redeem', {
+ ticket, client_id: opts.clientId, client_secret: opts.clientSecret, redirect_uri: redirectUri,
+ });
+ if (!data || typeof data.code !== 'string' || !data.code) throw new Error('Network issued no code');
+ const tokens = await tokenGrant({ grant_type: 'authorization_code', redirect_uri: redirectUri, code: data.code });
+ if (!tokens || typeof tokens.access_token !== 'string' || !tokens.access_token) throw new Error('Network issued no session');
+ setSession(res, tokens.access_token, tokens.refresh_token);
+ } catch (err) {
+ log.warn(`[${site} auth] handoff arrival failed: ${err.message}`);
+ }
+ return res.redirect(clean);
+ })().catch(nextFn);
+ };
+ }
+
+ return { router, handlers, handoff, optionalAuth, requireAuth, verify, extractToken, authorizeUrl, tokenGrant, checkClient, redirectUri, audience };
}
}
module.exports = {
module.exports = {
- createSsoClient, sanitizeNext, withParam, decodeJwtPayload, fedcmNonceMatches, claimsToUser, parseCookies, failurePage,
+ createSsoClient, sanitizeNext, withParam, stripHandoffParam, decodeJwtPayload, fedcmNonceMatches, claimsToUser, parseCookies, failurePage,
+ HANDOFF_PARAM,
COOKIES: Object.freeze({ access: ACCESS_COOKIE, refresh: REFRESH_COOKIE, hint: HINT_COOKIE }),
COOKIES: Object.freeze({ access: ACCESS_COOKIE, refresh: REFRESH_COOKIE, hint: HINT_COOKIE }),
};
};
@@ -1,9 +1,10 @@
@@ -1,9 +1,10 @@
'use strict';
'use strict';
-/** Personal bearer wallet/store facade. Amounts and ownership are enforced by Billing and Inventory. */
-function createWalletClient(client, { billingUrl, inventoryUrl } = {}) {
+/** Personal bearer wallet/store facade. Amounts and ownership are enforced by their ledger services. */
+function createWalletClient(client, { billingUrl, inventoryUrl, networkUrl } = {}) {
if (!client || typeof client.json !== 'function') throw new TypeError('createWalletClient requires an SDK core client');
if (!client || typeof client.json !== 'function') throw new TypeError('createWalletClient requires an SDK core client');
- const call = (service, path, opts = {}) => client.json({ service, baseUrl: service === 'billing' ? billingUrl : inventoryUrl,
+ const urls = { billing: billingUrl, inventory: inventoryUrl, network: networkUrl };
+ const call = (service, path, opts = {}) => client.json({ service, baseUrl: urls[service],
audience: `openvibe.${service}`, path, credentials: 'omit', ...opts });
audience: `openvibe.${service}`, path, credentials: 'omit', ...opts });
const key = (v) => {
const key = (v) => {
if (typeof v !== 'string' || !/^[A-Za-z0-9._:-]{8,128}$/.test(v)) throw new TypeError('An idempotency_key of 8–128 letters, digits, dot, colon, underscore or dash is required');
if (typeof v !== 'string' || !/^[A-Za-z0-9._:-]{8,128}$/.test(v)) throw new TypeError('An idempotency_key of 8–128 letters, digits, dot, colon, underscore or dash is required');
@@ -11,12 +12,38 @@ function createWalletClient(client, { billingUrl, inventoryUrl } = {}) {
@@ -11,12 +12,38 @@ function createWalletClient(client, { billingUrl, inventoryUrl } = {}) {
};
};
const whole = (v) => { if (!Number.isSafeInteger(v) || v < 1) throw new TypeError('Choose a positive whole amount'); return v; };
const whole = (v) => { if (!Number.isSafeInteger(v) || v < 1) throw new TypeError('Choose a positive whole amount'); return v; };
const itemId = (v) => { if (!/^itd_[0-9A-HJKMNP-TV-Z]{26}$/.test(v || '')) throw new TypeError('An item id is an itd_ id'); return encodeURIComponent(v); };
const itemId = (v) => { if (!/^itd_[0-9A-HJKMNP-TV-Z]{26}$/.test(v || '')) throw new TypeError('An item id is an itd_ id'); return encodeURIComponent(v); };
+ const cursor = (v, currency) => {
+ if (v == null) return v;
+ if (currency === 'vibes') {
+ if (typeof v !== 'string' || !/^txn_[0-9A-HJKMNP-TV-Z]{26}$/.test(v)) throw new TypeError('vibes cursor is a txn_ id');
+ } else if (!Number.isSafeInteger(Number(v)) || Number(v) <= 0 || !/^[0-9]+$/.test(String(v))) {
+ throw new TypeError('cursor is a positive move id');
+ }
+ return v;
+ };
+ const streamer = (v) => { if (!Number.isSafeInteger(v) || v <= 0) throw new TypeError('streamer is a positive user id'); return v; };
// The BODY key is durable in both services. Retries keep it; the SDK must not add an unrelated header key.
// The BODY key is durable in both services. Retries keep it; the SDK must not add an unrelated header key.
const write = (service, path, json, { signal } = {}) => call(service, path, { method: 'POST', json,
const write = (service, path, json, { signal } = {}) => call(service, path, { method: 'POST', json,
idempotencyKey: false, idempotent: true, signal });
idempotencyKey: false, idempotent: true, signal });
return {
return {
wallet: {
wallet: {
get: ({ signal } = {}) => call('billing', '/api/v1/me/wallet', { signal }),
get: ({ signal } = {}) => call('billing', '/api/v1/me/wallet', { signal }),
+ changes: ({ since_vibes, since_opencoins, since_channel_points, streamer: channel, limit, signal } = {}) => {
+ if (limit != null && (!Number.isSafeInteger(limit) || limit < 1 || limit > 500)) throw new TypeError('limit is 1 to 500');
+ return call('network', '/api/wallet/changes', { query: {
+ since_vibes: cursor(since_vibes, 'vibes'), since_opencoins: cursor(since_opencoins, 'opencoins'),
+ since_channel_points: cursor(since_channel_points, 'channel-points'),
+ streamer: channel == null ? undefined : streamer(channel), limit,
+ }, signal });
+ },
+ markRead: ({ currency, cursor: at, streamer: channel }, { signal } = {}) => {
+ if (!['vibes', 'opencoins', 'channel-points'].includes(currency)) throw new TypeError('currency is vibes, opencoins or channel-points');
+ if (at == null) throw new TypeError('cursor is required');
+ if (currency === 'channel-points' && channel == null) throw new TypeError('streamer is required for channel points');
+ return call('network', '/api/wallet/changes/read', { method: 'PUT', json: {
+ currency, cursor: cursor(at, currency), ...(currency === 'channel-points' ? { streamer: streamer(channel) } : {}),
+ }, signal });
+ },
topUp: ({ provider, amount_cents, idempotency_key }, options) => write('billing', '/api/v1/me/wallet/topups',
topUp: ({ provider, amount_cents, idempotency_key }, options) => write('billing', '/api/v1/me/wallet/topups',
{ provider, amount_cents: whole(amount_cents), idempotency_key: key(idempotency_key) }, options),
{ provider, amount_cents: whole(amount_cents), idempotency_key: key(idempotency_key) }, options),
},
},
new file mode 100644
new file mode 100644
@@ -0,0 +1,36 @@
@@ -0,0 +1,36 @@
+'use strict';
+const assert = require('assert');
+const { createAgentSheetClient } = require('../src/agent-sheet');
+const { createActorClient } = require('../src/actor');
+const seen = [];
+const client = createAgentSheetClient({ json: async (request) => { seen.push(request); return { ok: true }; } });
+const agent = 'agt_00000000000000000000000000';
+const memory = 'mem_00000000000000000000000000';
+
+(async () => {
+ await client.sheet(agent);
+ await client.codesSheet('codex');
+ await client.pause(agent);
+ await client.resume(agent);
+ await client.memory.add(agent, 'A fact');
+ await client.memory.update(agent, memory, 'New fact');
+ await client.memory.remove(agent, memory);
+ await createActorClient({ json: async (request) => { seen.push(request); return { ok: true }; }, request: async () => ({}) }).me.agents.sheet(agent);
+ assert.deepStrictEqual(seen.map((r) => [r.method || 'GET', r.path]), [
+ ['GET', `/api/frame/agents/${agent}/sheet`],
+ ['GET', '/api/frame/codes-agents/codex/sheet'],
+ ['PATCH', `/api/frame/agents/${agent}`],
+ ['PATCH', `/api/frame/agents/${agent}`],
+ ['POST', `/api/frame/agents/${agent}/memory`],
+ ['PATCH', `/api/frame/agents/${agent}/memory/${memory}`],
+ ['DELETE', `/api/frame/agents/${agent}/memory/${memory}`],
+ ['GET', `/api/v1/me/agents/${agent}`],
+ ]);
+ assert.ok(seen.slice(0, -1).every((r) => r.service === 'network' && r.audience === 'openvibe.network'));
+ assert.deepStrictEqual(seen.at(-1).query, { view: 'sheet' });
+ assert.deepStrictEqual([seen[2].json, seen[3].json], [{ status: 'paused' }, { status: 'active' }]);
+ assert.throws(() => client.sheet('../other'), TypeError);
+ assert.throws(() => client.codesSheet('../other'), TypeError);
+ assert.throws(() => client.memory.remove(agent, '../other'), TypeError);
+ console.log('agent sheet reader: fixed Network paths and validated IDs');
+})().catch((error) => { console.error(error); process.exitCode = 1; });
new file mode 100644
new file mode 100644
@@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
+'use strict';
+const assert = require('node:assert/strict');
+const { run, stubServer, send } = require('./helpers');
+const { createClient } = require('../src/core');
+const { createAgentsClient } = require('../src/agents');
+run([
+ ['run snapshot, post approval, and resumable AG-UI SSE', async () => {
+ let calls = 0; const srv = await stubServer((req,res) => {
+ if (req.url.endsWith('/approvals')) return send(res,200,{accepted:true});
+ if (req.url.endsWith('/events')) { calls++; assert.equal(req.headers['last-event-id'], calls === 1 ? '4' : '5'); res.writeHead(200,{'Content-Type':'text/event-stream'}); res.write(`id: ${calls===1?'5':'6'}\nevent: ${calls===1?'STEP_STARTED':'RUN_FINISHED'}\ndata: ${JSON.stringify({type:calls===1?'STEP_STARTED':'RUN_FINISHED',timestamp:'2026-11-11T00:00:00Z'})}\n\n`); return res.end(); }
+ return send(res,200,{id:'run_a',status:'running'});
+ });
+ try { const agents=createAgentsClient(createClient({baseUrls:{network:srv.url},token:'t'}));
+ assert.equal((await agents.run('run_a')).id,'run_a'); assert.deepEqual(await agents.post('run_a',{id:'approval_1',action:'approve'}),{accepted:true});
+ const got=[]; for await (const e of agents.events('run_a',{lastEventId:4,reconnectDelayMs:1})) got.push([e.id,e.event]);
+ assert.deepEqual(got,[['5','STEP_STARTED'],['6','RUN_FINISHED']]); assert.equal(srv.requests[3].headers['last-event-id'],'5');
+ } finally { await srv.close(); }
+ }],
+ ['reconnect retries use exponential backoff', async () => {
+ let calls=0; const srv=await stubServer((req,res)=>{ calls++; if(calls<4) return send(res,503,{code:'temporarily.unavailable'}); res.writeHead(200,{'Content-Type':'text/event-stream'}); res.end(`id: 1\nevent: RUN_FINISHED\ndata: ${JSON.stringify({type:'RUN_FINISHED',timestamp:1794355200000})}\n\n`); });
+ try { const agents=createAgentsClient(createClient({baseUrls:{network:srv.url},token:'t'})); const start=Date.now(); const got=[]; for await(const e of agents.events('run_a',{reconnectDelayMs:10,maxReconnects:5})) got.push(e.id); assert.deepEqual(got,['1']); assert.ok(Date.now()-start>=65,`backoff elapsed ${Date.now()-start}ms`); assert.equal(calls,4); }
+ finally { await srv.close(); }
+ }],
+]);
@@ -117,8 +117,11 @@ run([
@@ -117,8 +117,11 @@ run([
assert.equal(me.body.user.subject, USER);
assert.equal(me.body.user.subject, USER);
assert.deepEqual(me.body.user.capabilities, ['media.object.read']);
assert.deepEqual(me.body.user.capabilities, ['media.object.read']);
assert.equal(me.body.user.app, `app:${APP}`);
assert.equal(me.body.user.app, `app:${APP}`);
- assert.ok(!JSON.stringify(me.body).includes('eyJ'), 'no token in the answer');
- assert.ok(!JSON.stringify(ok.cookies).includes('eyJ'), 'no token in the cookie');
+ // A token is a JWT's shape (three base64url parts, the first "eyJ…"), not the letters "eyJ": a random session id
+ // can hold those three letters, and the bare substring failed now and then on the slow macOS runners (2026-11-11).
+ const JWT = /eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/;
+ assert.ok(!JWT.test(JSON.stringify(me.body)), 'no token in the answer');
+ assert.ok(!JWT.test(JSON.stringify(ok.cookies)), 'no token in the cookie');
// Signed out: no user.
// Signed out: no user.
const out = fakeRes();
const out = fakeRes();
@@ -76,7 +76,7 @@ run([
@@ -76,7 +76,7 @@ run([
for (const [re, why] of FORBIDDEN) if (re.test(src)) problems.push(`${path.relative(ROOT, file)}: ${why}`);
for (const [re, why] of FORBIDDEN) if (re.test(src)) problems.push(`${path.relative(ROOT, file)}: ${why}`);
}
}
assert.deepEqual(problems, []);
assert.deepEqual(problems, []);
- assert.deepEqual(blocked.sort(), ['./account-data', './actor', './app-signin', './assertions', './blog', './cache', './chat-apps', './commerce', './compute', './consent', './cost', './coupons', './db', './deals', './delivery', './design', './download', './events', './exchange', './food', './git', './govern', './host', './identity', './inventory', './limits', './link', './live', './menu', './news', './notifications', './openre', './placement', './play', './pubsub', './quest', './queue', './reports', './resources', './reviews', './run', './runtime', './service', './share', './shoes', './space', './sso', './storage', './streams', './telemetry', './testing', './tips', './trade', './usage', './valkey', './vibes', './video', './vip', './wiki', './work', './zone']);
+ assert.deepEqual(blocked.sort(), ['./account-data', './actor', './app-signin', './assertions', './blog', './cache', './chat-apps', './codes', './commerce', './compute', './consent', './cost', './coupons', './db', './deals', './delivery', './design', './download', './events', './exchange', './food', './git', './govern', './host', './identity', './inventory', './limits', './link', './live', './menu', './news', './notifications', './openre', './placement', './play', './pubsub', './quest', './queue', './reports', './resources', './reviews', './run', './runtime', './service', './share', './shoes', './space', './sso', './storage', './streams', './telemetry', './testing', './tips', './trade', './usage', './valkey', './vibes', './video', './vip', './wiki', './work', './zone']);
const rel = [...scanned].map((f) => path.relative(ROOT, f).split(path.sep).join('/')); // the same names on Windows
const rel = [...scanned].map((f) => path.relative(ROOT, f).split(path.sep).join('/')); // the same names on Windows
for (const must of ['browser.js', 'src/core/client.js', 'src/auth/browser.js', 'src/cam/index.js', 'src/realtime.js', 'src/media.js', 'src/community.js', 'src/bot.js', 'src/modules.js', 'src/registry.js', 'src/jobs.js', 'src/tools.js', 'src/core/form.js', 'src/projects.js', 'browser/openvibe-sdk.mjs']) {
for (const must of ['browser.js', 'src/core/client.js', 'src/auth/browser.js', 'src/cam/index.js', 'src/realtime.js', 'src/media.js', 'src/community.js', 'src/bot.js', 'src/modules.js', 'src/registry.js', 'src/jobs.js', 'src/tools.js', 'src/core/form.js', 'src/projects.js', 'browser/openvibe-sdk.mjs']) {
assert.ok(rel.includes(must), `${must} is scanned`);
assert.ok(rel.includes(must), `${must} is scanned`);
@@ -20,6 +20,20 @@ async function chatStub() {
@@ -20,6 +20,20 @@ async function chatStub() {
}
}
run([
run([
+ ['person memory reader and privacy controls use the person API', async () => {
+ const srv = await chatStub();
+ const chat = createChatClient(createClient({ baseUrls: { chat: srv.url }, token: 'person-token' }));
+ await chat.people.memory('alice', { channel: 'streamer', level: 'week' });
+ await chat.people.forget('alice');
+ await chat.people.preferences('alice', false);
+ assert.deepEqual(srv.requests.map(r => [r.method, r.url]), [
+ ['GET', '/api/v1/people/alice/memory?channel=streamer&level=week'],
+ ['DELETE', '/api/v1/people/alice/memory'],
+ ['PUT', '/api/v1/people/alice/memory/preferences'],
+ ]);
+ assert.deepEqual(JSON.parse(String(srv.requests[2].body)), { opted_out: false });
+ await srv.close();
+ }],
['embeds reads chat.embed@1 and returns the typed shape, dropping what is not one', async () => {
['embeds reads chat.embed@1 and returns the typed shape, dropping what is not one', async () => {
const chat = createChatClient(createClient({ baseUrls: { chat: 'http://127.0.0.1:9' }, token: 'person-token' }));
const chat = createChatClient(createClient({ baseUrls: { chat: 'http://127.0.0.1:9' }, token: 'person-token' }));
const link = { kind: 'link', url: 'https://example.com/a', site_name: 'Example', title: 'A story', description: 'What happened', image: null, width: 640, height: 360 };
const link = { kind: 'link', url: 'https://example.com/a', site_name: 'Example', title: 'A story', description: 'What happened', image: null, width: 640, height: 360 };
new file mode 100644
new file mode 100644
@@ -0,0 +1,59 @@
@@ -0,0 +1,59 @@
+'use strict';
+// openvibe-sdk/codes: the Dock's Code tab calls (list, create, stop) through a real core client against a local stub.
+const assert = require('assert');
+const http = require('http');
+const { createClient } = require('../src/core');
+const { createCodesClient } = require('../src/codes');
+
+const TASK = 'tsk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3';
+const summary = (over = {}) => ({
+ id: TASK, title: 'Add a test', status: 'running', mode: 'repo', repo: 'https://github.com/octocat/Hello-World',
+ started_at: '2026-11-10T10:00:00.000Z', updated_at: '2026-11-10T10:04:00.000Z', steps: 2,
+ url: `https://openvibe.codes/tasks/${TASK}`, preview_url: null, pr_url: null, ...over,
+});
+
+(async () => {
+ const seen = [];
+ const server = http.createServer((req, res) => {
+ let body = '';
+ req.on('data', (c) => { body += c; });
+ req.on('end', () => {
+ seen.push({ method: req.method, url: req.url, auth: req.headers.authorization || null, body });
+ const json = (status, obj) => { res.writeHead(status, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(obj)); };
+ if (req.url === '/api/v1/me/tasks?limit=8' && req.method === 'GET') return json(200, { tasks: [summary()] });
+ if (req.url === '/api/v1/tasks' && req.method === 'POST') {
+ const b = JSON.parse(body);
+ if (b.mode === 'create') return json(200, { id: null, url: 'https://openvibe.space/auth/login?next=%2Fspaces%2Fnew' });
+ return json(201, { id: TASK, url: `https://openvibe.codes/tasks/${TASK}` });
+ }
+ if (req.url === `/api/v1/tasks/${TASK}/stop` && req.method === 'POST') return json(200, { ok: true });
+ return json(404, { code: 'codes.task_not_found' });
+ });
+ });
+ await new Promise((r) => server.listen(0, '127.0.0.1', r));
+ const base = `http://127.0.0.1:${server.address().port}`;
+ const codes = createCodesClient(createClient({ baseUrls: { codes: base }, token: 'person-token' }));
+ try {
+ const listed = await codes.tasks.list({ limit: 8 });
+ assert.deepStrictEqual(listed, { tasks: [summary()] });
+ const created = await codes.tasks.create({ text: 'Add a test.', mode: 'repo', repo: 'octocat/Hello-World' });
+ assert.deepStrictEqual(created, { id: TASK, url: `https://openvibe.codes/tasks/${TASK}` });
+ const space = await codes.tasks.create({ text: 'A bakery site.', mode: 'create' });
+ assert.strictEqual(space.id, null);
+ assert.ok(space.url.startsWith('https://openvibe.space'));
+ assert.deepStrictEqual(await codes.tasks.stop(TASK), { ok: true });
+
+ const posted = JSON.parse(seen.find((s) => s.url === '/api/v1/tasks' && JSON.parse(s.body).mode === 'repo').body);
+ assert.deepStrictEqual(posted, { text: 'Add a test.', mode: 'repo', repo: 'octocat/Hello-World' });
+ assert.ok(seen.every((s) => s.auth === 'Bearer ' + 'person-token'), 'the person token rides every call');
+ assert.throws(() => codes.tasks.list({ limit: 0 }), /limit is 1-50/);
+ assert.throws(() => codes.tasks.create({ text: '', mode: 'repo' }), /text is the instruction/);
+ assert.throws(() => codes.tasks.create({ text: 'Hi', mode: 'teleport' }), /mode is create/);
+ assert.throws(() => codes.tasks.create({ text: 'Hi', mode: 'repo', repo: ' ' }), /repo is a repository/);
+ assert.throws(() => codes.tasks.create({ text: 'Hi', mode: 'repo', attachments: [42] }), /attachment ids/);
+ assert.throws(() => codes.tasks.stop('nope'), /tsk_/);
+ console.log(' ok codes tasks: list, create (repo and create), stop, and every validation');
+ } finally {
+ await new Promise((r) => server.close(r));
+ }
+})().catch((err) => { console.error(err); process.exit(1); });
@@ -161,7 +161,7 @@ run([
@@ -161,7 +161,7 @@ run([
const sdk = require('..');
const sdk = require('..');
const community = sdk.community.createCommunityClient(sdk.createClient({ baseUrls: { community: srv.url } }));
const community = sdk.community.createCommunityClient(sdk.createClient({ baseUrls: { community: srv.url } }));
const query = { kind: 'stream', site: 'live', q: 'broadcast', following: 1, limit: 1 };
const query = { kind: 'stream', site: 'live', q: 'broadcast', following: 1, limit: 1 };
- const first = await community.feed(query);
+ const first = await community.feed.list(query);
assert.deepEqual(first, { items: [], next_cursor: 'opaque-cursor' });
assert.deepEqual(first, { items: [], next_cursor: 'opaque-cursor' });
const second = await community.feed({ ...query, cursor: first.next_cursor });
const second = await community.feed({ ...query, cursor: first.next_cursor });
assert.deepEqual(second, { items: [item], next_cursor: null });
assert.deepEqual(second, { items: [item], next_cursor: null });
new file mode 100644
new file mode 100644
@@ -0,0 +1,118 @@
@@ -0,0 +1,118 @@
+'use strict';
+/** The shared composer client: XHR upload with real progress, abort, remove, prefs with the revision, mentions. */
+const assert = require('node:assert/strict');
+const { stubServer, send, run } = require('./helpers');
+const { createClient } = require('../src/core');
+const { createComposerClient } = require('../src/composer');
+
+function fakeXHR({ never = false } = {}) {
+ return class FakeXHR {
+ constructor() { this.upload = {}; this.headers = {}; this.sent = false; }
+ open(method, url) { this.method = method; this.url = url; }
+ setRequestHeader(k, v) { this.headers[k.toLowerCase()] = v; }
+ send(body) {
+ this.sent = true;
+ this.body = body;
+ if (never) return;
+ fetch(this.url, { method: this.method, headers: this.headers, body }).then(async (r) => {
+ this.status = r.status;
+ this.responseText = await r.text();
+ this.upload.onprogress?.({ loaded: 1, total: 1, lengthComputable: true });
+ this.onload?.();
+ }).catch((err) => this.onerror?.(err));
+ }
+ abort() { this.aborted = true; }
+ };
+}
+
+run([
+ ['the ESM entry exports the same client', async () => {
+ const esm = await import('../esm/composer.mjs');
+ assert.equal(typeof esm.createComposerClient, 'function');
+ assert.equal(esm.MEDIA_ORIGIN, 'https://openvibe.media');
+ }],
+
+ ['upload posts one multipart file through XHR, with the token and progress', async () => {
+ const srv = await stubServer((req, res) => send(res, 201, {
+ id: 'att_01JAB2C3D4E5F6G7H8J9K0MNPQ', kind: 'image', name: 'shot.png', size: 3, mime: 'image/png',
+ url: 'https://openvibe.media/a/att_01JAB2C3D4E5F6G7H8J9K0MNPQ/shot.png', thumb: null, w: 1, h: 1,
+ expires_at: '2026-11-11T12:00:00.000Z',
+ }));
+ const composer = createComposerClient(createClient({ baseUrls: { media: srv.url }, token: 'person-token' }));
+ const progress = [];
+ const out = await composer.attachments.upload(new Blob([new Uint8Array([1, 2, 3])], { type: 'image/png' }), {
+ filename: 'shot.png', onProgress: (p) => progress.push(p), xhr: fakeXHR(),
+ });
+ assert.equal(out.id, 'att_01JAB2C3D4E5F6G7H8J9K0MNPQ');
+ assert.equal(out.kind, 'image');
+ const r = srv.requests[0];
+ assert.equal(r.method, 'POST');
+ assert.equal(r.url, '/api/v1/attachments');
+ assert.equal(r.headers.authorization, 'Bearer person-token');
+ assert.match(r.headers['content-type'], /^multipart\/form-data; boundary=/);
+ const text = r.body.toString('latin1');
+ assert.match(text, /name="file"; filename="shot.png"/);
+ assert.match(text, /content-type: image\/png/i);
+ assert.ok(progress.length >= 1 && progress.every((p) => p.loaded <= p.total && p.total === 1), 'real XHR progress');
+ await srv.close();
+ }],
+
+ ['a getToken client supplies the upload token; a signal aborts', async () => {
+ const composer = createComposerClient(createClient({ baseUrls: { media: 'http://127.0.0.1:9' }, getToken: async ({ service, audience }) => `${service}:${audience}` }));
+ assert.equal(composer.MEDIA_ORIGIN, 'https://openvibe.media');
+ const ctrl = new AbortController();
+ const p = composer.attachments.upload(new Blob(['x']), { signal: ctrl.signal, xhr: fakeXHR({ never: true }) });
+ ctrl.abort();
+ await assert.rejects(p, (err) => err.code === 'sdk.aborted');
+ }],
+
+ ['remove deletes and answers true, false when already gone', async () => {
+ const srv = await stubServer((req, res) => (req.url.endsWith('/missing') ? send(res, 404, { code: 'media.attachment.not_found' }) : res.writeHead(204).end()));
+ const composer = createComposerClient(createClient({ baseUrls: { media: srv.url } }), { mediaBaseUrl: srv.url });
+ assert.equal(await composer.attachments.remove('att_01JAB2C3D4E5F6G7H8J9K0MNPQ'), true);
+ assert.equal(await composer.attachments.remove('missing'), false);
+ assert.deepEqual(srv.requests.map((r) => `${r.method} ${r.url}`), ['DELETE /api/v1/attachments/att_01JAB2C3D4E5F6G7H8J9K0MNPQ', 'DELETE /api/v1/attachments/missing']);
+ await srv.close();
+ }],
+
+ ['prefs get/put name the revision; a stale rev carries the current copy', async () => {
+ const prefs = { rev: 2, send_key: 'enter', tools: [{ id: 'attach', hidden: false }], density: 'comfy', voice: true, snippets: [] };
+ const srv = await stubServer((req, res, raw) => {
+ if (req.method === 'GET') return send(res, 200, prefs);
+ const body = JSON.parse(raw.toString());
+ if (body.rev !== 2) return send(res, 409, { status: 409, code: 'composer.rev_conflict', error: 'moved', current: prefs });
+ return send(res, 200, { ...body, rev: 3 });
+ });
+ const composer = createComposerClient(createClient({ baseUrls: { network: srv.url } }), { networkBaseUrl: srv.url });
+ assert.deepEqual(await composer.composer.prefs.get(), prefs);
+ const saved = await composer.composer.prefs.put({ send_key: 'mod-enter', tools: [], density: 'compact', voice: false, snippets: [] }, 2);
+ assert.equal(saved.rev, 3);
+ assert.equal(saved.send_key, 'mod-enter');
+ const sent = JSON.parse(srv.requests[1].body.toString());
+ assert.equal(srv.requests[1].method, 'PUT');
+ assert.equal(sent.rev, 2);
+ await assert.rejects(composer.composer.prefs.put({ send_key: 'enter', tools: [], density: 'comfy', voice: true, snippets: [] }, 1), (err) => {
+ assert.equal(err.status, 409);
+ assert.equal(err.code, 'composer.rev_conflict');
+ assert.equal(err.problem.current.rev, 2);
+ return true;
+ });
+ assert.throws(() => composer.composer.prefs.put({}, '2'), TypeError);
+ await srv.close();
+ }],
+
+ ['mentions searches with q and kinds and returns the list', async () => {
+ const srv = await stubServer((req, res) => send(res, 200, { mentions: [
+ { kind: 'person', id: 'usr_01JAB2C3D4E5F6G7H8J9K0MNPQ', label: 'Ana', handle: 'ana', avatar: null, url: 'https://openvibe.network/@ana' },
+ ] }));
+ const composer = createComposerClient(createClient({ baseUrls: { network: srv.url } }));
+ const out = await composer.mentions.search('an a', ['person', 'repo']);
+ assert.equal(out.length, 1);
+ assert.equal(out[0].handle, 'ana');
+ const u = new URL(`http://x${srv.requests[0].url}`);
+ assert.equal(u.pathname, '/api/v1/mentions');
+ assert.equal(u.searchParams.get('q'), 'an a');
+ assert.equal(u.searchParams.get('kinds'), 'person,repo');
+ await srv.close();
+ }],
+]);
new file mode 100644
new file mode 100644
@@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
+'use strict';
+const assert = require('node:assert/strict');
+const { createCatalogSearchClient, ExchangeError } = require('../src/exchange');
+
+(async () => {
+ let url;
+ const client = createCatalogSearchClient({ fetchImpl: async (input, init) => {
+ url = new URL(input);
+ assert.equal(init.headers.Authorization, undefined);
+ return new Response(JSON.stringify({ q: 'fast coder', count: 0, results: [], quality_unavailable: false }), { status: 200 });
+ } });
+ const result = await client.search('fast coder', { max_price: 1 });
+ assert.equal(result.count, 0);
+ assert.equal(url.pathname, '/api/v1/search');
+ assert.equal(url.searchParams.get('max_price'), '1');
+ assert.equal(url.searchParams.get('q'), 'fast coder');
+ await assert.rejects(client.search(null), TypeError);
+ const failing = createCatalogSearchClient({ fetchImpl: async () => new Response(JSON.stringify({ error: 'catalog_unavailable' }), { status: 503 }) });
+ await assert.rejects(failing.search('vision'), (err) => err instanceof ExchangeError && err.status === 503);
+ process.stdout.write('Exchange catalog reader passed\n');
+})().catch((err) => { process.stderr.write(String(err.stack || err)); process.exitCode = 1; });
new file mode 100644
new file mode 100644
@@ -0,0 +1,12 @@
@@ -0,0 +1,12 @@
+'use strict';
+const assert = require('assert');
+const { createFeedClient } = require('../src/feed');
+
+(async () => {
+ const calls = [];
+ const client = createFeedClient({ json: async (input) => { calls.push(input); return { items: [], next_cursor: null }; } }, { communityBaseUrl: 'https://community.test' });
+ const result = await client.list({ kind: 'clip', cursor: 'opaque' });
+ assert.deepStrictEqual(result, { items: [], next_cursor: null });
+ assert.deepStrictEqual(calls[0], { service: 'community', baseUrl: 'https://community.test', audience: 'openvibe.community', path: '/api/v1/feed', query: { kind: 'clip', cursor: 'opaque' }, signal: undefined });
+ console.log('feed.list sends kind and cursor to Community');
+})().catch((error) => { console.error(error); process.exitCode = 1; });
@@ -10,19 +10,26 @@ const { createHomeFeedClient } = require('../src/home-feed');
@@ -10,19 +10,26 @@ const { createHomeFeedClient } = require('../src/home-feed');
await ov.home.get();
await ov.home.get();
await ov.home.set(layout);
await ov.home.set(layout);
await ov.network.today();
await ov.network.today();
+ await ov.network.popular({ limit: 5 });
+ await ov.network.tryNext({ limit: 3 });
await ov.feed.list({ for: 'me', cursor: 'next' });
await ov.feed.list({ for: 'me', cursor: 'next' });
await ov.feed.post('Hello', { link: 'https://openvibe.live/' });
await ov.feed.post('Hello', { link: 'https://openvibe.live/' });
await ov.feed.contribute({ id: 'update-1', text: 'New thing', url: 'https://example.test/' });
await ov.feed.contribute({ id: 'update-1', text: 'New thing', url: 'https://example.test/' });
assert.deepEqual(calls.map((c) => [c.service, c.method || 'GET', c.path]), [
assert.deepEqual(calls.map((c) => [c.service, c.method || 'GET', c.path]), [
['network', 'GET', '/api/v1/me/home-screen'], ['network', 'PUT', '/api/v1/me/home-screen'],
['network', 'GET', '/api/v1/me/home-screen'], ['network', 'PUT', '/api/v1/me/home-screen'],
- ['network', 'GET', '/api/v1/frame/today'], ['community', 'GET', '/api/v1/pulse'],
+ ['network', 'GET', '/api/v1/frame/today'], ['network', 'GET', '/api/v1/home/popular'],
+ ['network', 'GET', '/api/v1/me/try-next'], ['community', 'GET', '/api/v1/pulse'],
['community', 'POST', '/api/v1/me/status'], ['community', 'POST', '/api/v1/pulse/items'],
['community', 'POST', '/api/v1/me/status'], ['community', 'POST', '/api/v1/pulse/items'],
]);
]);
assert.deepEqual(calls[1].json, layout);
assert.deepEqual(calls[1].json, layout);
assert.equal(calls[2].auth, false);
assert.equal(calls[2].auth, false);
- assert.deepEqual(calls[3].query, { for: 'me', cursor: 'next' });
- assert.equal(calls[4].idempotencyKey, false);
- assert.equal(calls[5].idempotencyKey, false);
+ assert.equal(calls[3].auth, false);
+ assert.deepEqual(calls[3].query, { limit: 5 });
+ assert.equal(calls[4].auth, undefined);
+ assert.deepEqual(calls[4].query, { limit: 3 });
+ assert.deepEqual(calls[5].query, { for: 'me', cursor: 'next' });
+ assert.equal(calls[6].idempotencyKey, false);
+ assert.equal(calls[7].idempotencyKey, false);
const esm = await import('../esm/home-feed.mjs');
const esm = await import('../esm/home-feed.mjs');
assert.equal(typeof esm.createHomeFeedClient, 'function');
assert.equal(typeof esm.createHomeFeedClient, 'function');
const packageEntry = await import('openvibe-sdk/home-feed');
const packageEntry = await import('openvibe-sdk/home-feed');
@@ -72,7 +72,7 @@ run([
@@ -72,7 +72,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, 103); // user-card adds the 103rd subpath after history.
+ assert.equal(subpaths.length, 109); // composer, feed and agents add the 104th-106th after user-card; the 0.148.0 train's codes, player and the rest bring 109.
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,32 @@
@@ -0,0 +1,32 @@
+'use strict';
+const assert = require('node:assert/strict');
+const { run } = require('./helpers');
+const player = require('../src/player');
+
+run([
+ ['player delegates current and handoff to the shared host', async () => {
+ const old = global.window;
+ try {
+ global.window = { OVOverlay: { current: () => ({ id: 'live', href: 'https://openvibe.live/@test' }), handoffUrl: (url) => `${url}#ovo=card` } };
+ assert.equal(player.current().id, 'live');
+ assert.equal(player.handoff('https://openvibe.games/'), 'https://openvibe.games/#ovo=card');
+ } finally { global.window = old; }
+ }],
+ ['player events can be unsubscribed', async () => {
+ const old = global.window;
+ const listeners = new Map();
+ try {
+ global.window = { addEventListener: (name, fn) => listeners.set(name, fn), removeEventListener: (name) => listeners.delete(name) };
+ const values = [];
+ const off = player.on((event) => values.push(event.reason));
+ listeners.get('openvibe:player')({ detail: { reason: 'open' } });
+ off();
+ assert.deepEqual(values, ['open']);
+ assert.equal(listeners.size, 0);
+ } finally { global.window = old; }
+ }],
+ ['player ESM subpath exposes handoff', async () => {
+ const esm = await import('../esm/player.mjs');
+ assert.equal(typeof esm.handoff, 'function');
+ }],
+]);
@@ -42,7 +42,9 @@ function skipsIn(output) {
@@ -42,7 +42,9 @@ function skipsIn(output) {
function runOne(file) {
function runOne(file) {
return new Promise((resolve) => {
return new Promise((resolve) => {
const started = Date.now();
const started = Date.now();
- const child = spawn(process.execPath, [path.join(__dirname, file)], {
+ // PGlite's revocation schema can exhaust the default Windows V8 stack during WASM init.
+ const stack = process.platform === 'win32' && file === 'revocations.test.js' ? ['--stack_size=4096'] : [];
+ const child = spawn(process.execPath, [...stack, path.join(__dirname, file)], {
cwd: path.join(__dirname, '..'),
cwd: path.join(__dirname, '..'),
env: { ...process.env, NODE_ENV: 'test' },
env: { ...process.env, NODE_ENV: 'test' },
stdio: ['ignore', 'pipe', 'pipe'],
stdio: ['ignore', 'pipe', 'pipe'],
new file mode 100644
new file mode 100644
@@ -0,0 +1,159 @@
@@ -0,0 +1,159 @@
+'use strict';
+/** openvibe-sdk/sso handoff: mint a ticket for a link, redeem an arrival before first paint. */
+const assert = require('node:assert/strict');
+const crypto = require('node:crypto');
+const http = require('node:http');
+const { run, waitFor } = require('./helpers');
+const { createSsoClient, stripHandoffParam, HANDOFF_PARAM } = require('../src/sso');
+
+const keys = crypto.generateKeyPairSync('rsa', { modulusLength: 2048 });
+const ISS = 'https://openvibe.network';
+const now = () => Math.floor(Date.now() / 1000);
+const b64 = (o) => Buffer.from(JSON.stringify(o)).toString('base64url');
+const sign = (claims) => { const input = `${b64({ alg: 'RS256', typ: 'JWT', kid: 'k1' })}.${b64(claims)}`; return `${input}.${crypto.sign('RSA-SHA256', Buffer.from(input), keys.privateKey).toString('base64url')}`; };
+const session = (over = {}) => sign({ sub: 7, id: 7, username: 'ana', iss: ISS, aud: ['openvibe.network', 'openvibe.trade'], iat: now(), exp: now() + 600, ...over });
+
+const TICKET = `ovh_${'t'.repeat(40)}`;
+const USED_TICKET = `ovh_${'u'.repeat(40)}`;
+const BAD_CODE_TICKET = `ovh_${'b'.repeat(40)}`;
+
+// A stub Network: handoff tickets + redeem, the code exchange, the JWKS.
+const net = { tickets: [], redeems: [] };
+const server = http.createServer((req, res) => {
+ let raw = ''; req.on('data', (c) => { raw += c; });
+ req.on('end', () => {
+ const send = (status, body) => { res.statusCode = status; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(body)); };
+ if (req.url === '/api/.well-known/jwks') return send(200, { keys: [{ ...keys.publicKey.export({ format: 'jwk' }), kid: 'k1', use: 'sig', alg: 'RS256' }] });
+ const body = raw ? JSON.parse(raw) : {};
+ if (req.url === '/api/v1/handoff/tickets') {
+ net.tickets.push({ auth: req.headers.authorization, body });
+ if (req.headers.authorization !== `Bearer ${net.session}`) return send(401, { error: 'Authentication required' });
+ if (body.client_id === 'nope') return send(400, { error: 'invalid_request' });
+ return send(200, { ticket: TICKET, expires_at: new Date(Date.now() + 60000).toISOString(), expires_in: 60, client_id: body.client_id });
+ }
+ if (req.url === '/api/v1/handoff/redeem') {
+ net.redeems.push(body);
+ if (body.client_secret !== 'secret') return send(401, { error: 'invalid_client' });
+ if (body.ticket === TICKET && body.client_id === 'trade') return send(200, { code: 'handoff-code', expires_in: 300 });
+ if (body.ticket === BAD_CODE_TICKET) return send(200, { code: 'bad-code', expires_in: 300 });
+ return send(400, { error: 'invalid_grant', error_description: 'Unknown or used handoff ticket' });
+ }
+ if (req.url === '/oauth/token') {
+ if (body.client_secret !== 'secret') return send(401, { error: 'invalid_client' });
+ if (body.grant_type === 'authorization_code' && body.code === 'handoff-code') {
+ return send(200, { access_token: session(), refresh_token: 'rt_handoff', token_type: 'Bearer' });
+ }
+ return send(400, { error: 'invalid_grant' });
+ }
+ return send(404, {});
+ });
+});
+
+function fakeRes() {
+ const r = { cookies: {}, cleared: [], statusCode: 200 };
+ r.cookie = (k, v, o) => { r.cookies[k] = { v, o }; return r; };
+ r.clearCookie = (k) => { r.cleared.push(k); return r; };
+ r.redirect = (u) => { r.statusCode = 302; r.location = u; return r; };
+ r.status = (s) => { r.statusCode = s; return r; };
+ r.json = (b) => { r.body = b; return r; };
+ r.send = (b) => { r.body = b; return r; };
+ r.type = (t) => { r.contentType = t; return r; };
+ r.set = (k, v) => { (r.headers = r.headers || {})[k] = v; return r; };
+ return r;
+}
+const cookieHeader = (o) => Object.entries(o).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('; ');
+const silent = { warn() {}, error() {}, info() {} };
+
+let sso, base;
+run([
+ ['handoff param name and strip helper', async () => {
+ assert.equal(HANDOFF_PARAM, 'ov_handoff');
+ assert.equal(stripHandoffParam('/deals?a=1&ov_handoff=ovh_x#h'), '/deals?a=1#h');
+ assert.equal(stripHandoffParam('/deals?ov_handoff=ovh_x'), '/deals');
+ assert.equal(stripHandoffParam('/'), '/');
+ assert.equal(stripHandoffParam(null), '/');
+ }],
+
+ ['mint: 401 without a session, 400 without a destination', async () => {
+ await new Promise((r) => server.listen(0, '127.0.0.1', r));
+ base = `http://127.0.0.1:${server.address().port}`;
+ net.session = session();
+ sso = createSsoClient({ site: 'trade', baseUrl: 'https://openvibe.trade', clientId: 'trade', clientSecret: 'secret', networkUrl: ISS, networkInternalUrl: base, issuer: ISS, log: silent });
+ let res = fakeRes();
+ await sso.handlers.handoffMint({ headers: {}, body: { client_id: 'news', redirect_uri: 'https://openvibe.news/auth/callback' } }, res);
+ assert.equal(res.statusCode, 401, 'no session mints nothing');
+ res = fakeRes();
+ await sso.handlers.handoffMint({ headers: { cookie: cookieHeader({ ov_token: net.session }) }, body: {} }, res);
+ assert.equal(res.statusCode, 400, 'no destination is refused');
+ }],
+
+ ['mint: forwards the person and the destination, returns the ticket', async () => {
+ const res = fakeRes();
+ await sso.handlers.handoffMint({ headers: { cookie: cookieHeader({ ov_token: net.session }) }, body: { client_id: 'news', redirect_uri: 'https://openvibe.news/auth/callback' } }, res);
+ assert.equal(res.statusCode, 200);
+ assert.equal(res.body.ticket, TICKET);
+ assert.equal(res.body.expires_in, 60);
+ assert.equal(net.tickets.at(-1).auth, `Bearer ${net.session}`, 'the person’s own token proves the mint');
+ assert.equal(net.tickets.at(-1).body.client_id, 'news');
+ }],
+
+ ['mint: the Network refusing or silent is a 4xx or 502, never a ticket', async () => {
+ let res = fakeRes();
+ await sso.handlers.handoffMint({ headers: { cookie: cookieHeader({ ov_token: net.session }) }, body: { client_id: 'nope', redirect_uri: 'https://x.example/cb' } }, res);
+ assert.equal(res.statusCode, 400, 'the Network’s refusal is passed through');
+ const down = createSsoClient({ site: 'trade', baseUrl: 'https://openvibe.trade', clientId: 'trade', clientSecret: 'secret', networkUrl: 'http://127.0.0.1:1', networkInternalUrl: 'http://127.0.0.1:1', issuer: ISS, log: silent, publicKey: keys.publicKey.export({ format: 'pem', type: 'spki' }) });
+ res = fakeRes();
+ await down.handlers.handoffMint({ headers: { cookie: cookieHeader({ ov_token: net.session }) }, body: { client_id: 'news', redirect_uri: 'https://openvibe.news/auth/callback' } }, res);
+ assert.equal(res.statusCode, 502, 'an unreachable Network is a 502');
+ }],
+
+ ['arrival: no ticket, a bad ticket, a guest, or a live session all pass through untouched', async () => {
+ const before = net.redeems.length;
+ for (const req of [
+ { method: 'GET', query: {}, headers: {}, url: '/deals' },
+ { method: 'GET', query: { ov_handoff: 'not-a-ticket' }, headers: {}, url: '/deals?ov_handoff=not-a-ticket' },
+ { method: 'GET', query: { ov_handoff: TICKET }, headers: { cookie: cookieHeader({ ov_sso_hint: 'guest' }) }, url: '/' },
+ { method: 'GET', query: { ov_handoff: TICKET }, headers: { cookie: cookieHeader({ ov_token: net.session }) }, url: '/' },
+ { method: 'POST', query: { ov_handoff: TICKET }, headers: {}, url: '/' },
+ ]) {
+ let nexted = false;
+ const res = fakeRes();
+ sso.handoff()(req, res, () => { nexted = true; });
+ await waitFor(() => nexted || res.location !== undefined, { timeoutMs: 30000 });
+ assert.ok(nexted, `passes through: ${req.url} ${JSON.stringify(req.headers)}`);
+ assert.equal(res.location, undefined, 'no redirect when passing through');
+ }
+ assert.equal(net.redeems.length, before, 'no redeem call for any pass-through');
+ }],
+
+ ['arrival: a good ticket signs the browser in and scrubs the address bar', async () => {
+ const res = fakeRes();
+ let nexted = false;
+ sso.handoff()({ method: 'GET', query: { ov_handoff: TICKET }, headers: {}, url: '/deals?a=1&ov_handoff=xx' }, res, () => { nexted = true; });
+ await waitFor(() => nexted || res.location !== undefined, { timeoutMs: 30000 });
+ assert.equal(nexted, false);
+ assert.equal(res.statusCode, 302);
+ assert.equal(res.location, '/deals?a=1', 'the ticket leaves the address bar, the rest stays');
+ assert.ok(res.cookies.ov_token && res.cookies.ov_token.v.length > 100, 'a session is set');
+ assert.equal(res.cookies.ov_sso_hint.v, 'account');
+ assert.equal(net.redeems.at(-1).client_id, 'trade', 'redeemed as this site’s own client');
+ }],
+
+ ['arrival: a refused or broken handoff renders as a guest on a clean URL', async () => {
+ for (const ticket of [USED_TICKET, BAD_CODE_TICKET]) {
+ const res = fakeRes();
+ let nexted = false;
+ sso.handoff()({ method: 'GET', query: { ov_handoff: ticket }, headers: {}, url: `/?ov_handoff=${ticket}` }, res, () => { nexted = true; });
+ await waitFor(() => nexted || res.location !== undefined, { timeoutMs: 30000 });
+ assert.equal(nexted, false);
+ assert.equal(res.location, '/', 'the ticket is scrubbed even on failure');
+ assert.equal(res.cookies.ov_token, undefined, 'no session on failure');
+ }
+ server.close();
+ }],
+ ['the ESM subpath exports the public handoff helpers', async () => {
+ const esm = await import('../esm/sso.mjs');
+ assert.equal(esm.HANDOFF_PARAM, 'ov_handoff');
+ assert.equal(esm.stripHandoffParam('/deals?ov_handoff=ovh_x&sort=top#list'), '/deals?sort=top#list');
+ }],
+]);
@@ -5,14 +5,33 @@ const { createWalletClient } = require('../src/wallet');
@@ -5,14 +5,33 @@ const { createWalletClient } = require('../src/wallet');
(async () => {
(async () => {
const calls = []; let retry = true;
const calls = []; let retry = true;
- const client = createClient({ token: 'person-token', baseUrls: { billing: 'https://billing.test', inventory: 'https://inventory.test' },
+ const client = createClient({ token: 'person-token', baseUrls: { billing: 'https://billing.test', inventory: 'https://inventory.test', network: 'https://network.test' },
credentials: 'include', retries: 1, retryDelayMs: 1, fetch: async (url, init) => {
credentials: 'include', retries: 1, retryDelayMs: 1, fetch: async (url, init) => {
calls.push({ url, ...init, json: init.body ? JSON.parse(init.body) : null });
calls.push({ url, ...init, json: init.body ? JSON.parse(init.body) : null });
if (retry && url.endsWith('/buy')) { retry = false; return new Response(JSON.stringify({ detail: 'retry' }), { status: 503 }); }
if (retry && url.endsWith('/buy')) { retry = false; return new Response(JSON.stringify({ detail: 'retry' }), { status: 503 }); }
+ if (url.includes('/api/wallet/changes?')) return new Response(JSON.stringify({
+ vibes: { net: 12, parts: { earned: 12, spent: 0, received: 0, sent: 0 }, moves: [{ id: 3, delta: 12, part: 'earned' }], cursor: 3 },
+ opencoins: { net: -4, parts: { earned: 0, spent: -4, received: 0, sent: 0 }, moves: [{ id: 8, delta: -4, part: 'spent' }], cursor: 8 },
+ channel_points: null, state: { vibes: 'open', opencoins: 'open', channel_points: 'needs_streamer' },
+ }), { status: 200, headers: { 'Content-Type': 'application/json' } });
return new Response(JSON.stringify({ ok: true }), { status: 200, headers: { 'Content-Type': 'application/json' } });
return new Response(JSON.stringify({ ok: true }), { status: 200, headers: { 'Content-Type': 'application/json' } });
} });
} });
const ov = createWalletClient(client);
const ov = createWalletClient(client);
await ov.wallet.get();
await ov.wallet.get();
+ const vibesCursor = 'txn_' + '0'.repeat(25) + '1';
+ const changes = await ov.wallet.changes({ since_vibes: vibesCursor, since_opencoins: 7 });
+ assert.strictEqual(changes.vibes.net, 12);
+ assert.strictEqual(changes.opencoins.net, -4);
+ assert.strictEqual(changes.state.channel_points, 'needs_streamer');
+ assert(calls.at(-1).url.includes(`since_vibes=${vibesCursor}`));
+ assert(calls.at(-1).url.includes('since_opencoins=7'));
+ assert(!calls.at(-1).url.includes('subject='));
+ await ov.wallet.changes({ streamer: 9, since_channel_points: 21 });
+ assert(calls.at(-1).url.includes('since_channel_points=21'));
+ await ov.wallet.markRead({ currency: 'channel-points', streamer: 9, cursor: 22 });
+ assert.deepStrictEqual(calls.at(-1).json, { currency: 'channel-points', streamer: 9, cursor: 22 });
+ await ov.wallet.markRead({ currency: 'vibes', cursor: vibesCursor });
+ assert.deepStrictEqual(calls.at(-1).json, { currency: 'vibes', cursor: vibesCursor });
await ov.wallet.topUp({ provider: 'stripe', amount_cents: 1000, idempotency_key: 'topup-test-01', subject: 'ignored' });
await ov.wallet.topUp({ provider: 'stripe', amount_cents: 1000, idempotency_key: 'topup-test-01', subject: 'ignored' });
await ov.vibes.buy({ provider: 'stripe', vibes: 100, idempotency_key: 'vibes-test-01', amount_cents: 1 });
await ov.vibes.buy({ provider: 'stripe', vibes: 100, idempotency_key: 'vibes-test-01', amount_cents: 1 });
await ov.store.list();
await ov.store.list();
@@ -34,5 +53,8 @@ const { createWalletClient } = require('../src/wallet');
@@ -34,5 +53,8 @@ const { createWalletClient } = require('../src/wallet');
assert.throws(() => ov.store.buy(id, { currency: 'USD', idempotency_key: 'safe-key-01' }));
assert.throws(() => ov.store.buy(id, { currency: 'USD', idempotency_key: 'safe-key-01' }));
assert.throws(() => ov.vibes.buy({ provider: 'stripe', vibes: 1.5, idempotency_key: 'safe-key-01' }));
assert.throws(() => ov.vibes.buy({ provider: 'stripe', vibes: 1.5, idempotency_key: 'safe-key-01' }));
assert.throws(() => ov.wallet.topUp({ provider: 'stripe', amount_cents: 1000 }));
assert.throws(() => ov.wallet.topUp({ provider: 'stripe', amount_cents: 1000 }));
+ assert.throws(() => ov.wallet.changes({ streamer: -1 }));
+ assert.throws(() => ov.wallet.changes({ since_vibes: 'someone-else' }));
+ assert.throws(() => ov.wallet.markRead({ currency: 'channel-points', cursor: 22 }));
console.log('wallet facade: routes, personal bearer, no cookies, stable retry keys and no price/subject injection');
console.log('wallet facade: routes, personal bearer, no cookies, stable retry keys and no price/subject injection');
})().catch((e) => { console.error(e); process.exitCode = 1; });
})().catch((e) => { console.error(e); process.exitCode = 1; });
@@ -44,6 +44,7 @@ export interface ActorClient {
@@ -44,6 +44,7 @@ export interface ActorClient {
list(opts?: Opts): Promise<{ agents: ActorJson[] }>;
list(opts?: Opts): Promise<{ agents: ActorJson[] }>;
create(agent: Record<string, unknown>, opts?: Opts): Promise<ActorJson>;
create(agent: Record<string, unknown>, opts?: Opts): Promise<ActorJson>;
get(id: string, opts?: Opts): Promise<ActorJson>;
get(id: string, opts?: Opts): Promise<ActorJson>;
+ sheet(id: string, opts?: Opts): Promise<import('./agent-sheet').AgentSheet>;
update(id: string, patch: Record<string, unknown>, opts?: Opts): Promise<ActorJson>;
update(id: string, patch: Record<string, unknown>, opts?: Opts): Promise<ActorJson>;
remove(id: string, opts?: Opts): Promise<ActorJson>;
remove(id: string, opts?: Opts): Promise<ActorJson>;
run(id: string, req?: { input?: string }, opts?: Opts): Promise<ActorJson>;
run(id: string, req?: { input?: string }, opts?: Opts): Promise<ActorJson>;
new file mode 100644
new file mode 100644
@@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
+import type { OpenVibeClient } from './core';
+export interface AgentSheet {
+ agent: { id: string; name: string; face: string | null; state: string; [key: string]: unknown };
+ now: Record<string, unknown> | null;
+ work: Array<Record<string, unknown>>;
+ asks: Array<Record<string, unknown>>;
+ asks_empty: string | null;
+ computer: Record<string, unknown> | null;
+ schedules: Array<Record<string, unknown>>;
+ memory: Array<Record<string, unknown>>;
+ activity: Array<Record<string, unknown>>;
+}
+export interface AgentSheetClient {
+ sheet(agentId: string, options?: { signal?: AbortSignal }): Promise<AgentSheet>;
+ codesSheet(harnessId: string, options?: { signal?: AbortSignal }): Promise<AgentSheet>;
+ pause(agentId: string, options?: { signal?: AbortSignal }): Promise<Record<string, unknown>>;
+ resume(agentId: string, options?: { signal?: AbortSignal }): Promise<Record<string, unknown>>;
+ memory: {
+ add(agentId: string, text: string, options?: { signal?: AbortSignal }): Promise<Record<string, unknown>>;
+ update(agentId: string, memoryId: string, text: string, options?: { signal?: AbortSignal }): Promise<Record<string, unknown>>;
+ remove(agentId: string, memoryId: string, options?: { signal?: AbortSignal }): Promise<unknown>;
+ };
+}
+export function createAgentSheetClient(client: OpenVibeClient, options?: { baseUrl?: string }): AgentSheetClient;
new file mode 100644
new file mode 100644
@@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
+/** AG-UI 1.0 event stream and agent.run@1 snapshot. */
+export interface AgentSource { url: string; title: string; site: string }
+export interface AgentUiEvent { type: string; timestamp: string; threadId?: string; runId?: string; metadata?: { ov?: { title?: string; kind?: string; sources?: AgentSource[]; attachments?: unknown[]; cost_usd?: number | null; needs_you?: object | null; product?: string; ref?: string | null } }; [field: string]: unknown }
+export interface AgentRun { id: string; product: string; title: string; status: 'queued'|'running'|'waiting'|'needs_you'|'succeeded'|'failed'|'stopped'|'cancelled'; plan: Array<{title:string;status:string}>; steps: Array<Record<string, unknown>>; sources: AgentSource[]; result: Record<string, unknown>|null; started_at: string; ended_at: string|null; cost_usd: number|null }
+export interface AgentApproval { id?: string; action?: string; [field: string]: unknown }
+export interface AgentEvent { id: string|null; event: string|null; data: AgentUiEvent|null }
+export interface AgentsClient { run(id: string, options?: {signal?: AbortSignal}): Promise<AgentRun|null>; events(id: string, options?: {lastEventId?: string|number; signal?: AbortSignal; maxReconnects?: number; reconnectDelayMs?: number}): AsyncIterable<AgentEvent>; post(id: string, approval: AgentApproval, options?: {signal?: AbortSignal}): Promise<unknown> }
+export function createAgentsClient(client: unknown, options?: {baseUrl?: string; service?: string; audience?: string}): AgentsClient;
@@ -1,15 +1,19 @@
@@ -1,15 +1,19 @@
import * as userCard from './user-card';
import * as userCard from './user-card';
+import * as player from './player';
+import * as agentSheet from './agent-sheet';
/** browser/openvibe-sdk.mjs: one self-contained ES module with the browser-safe subpaths (same shape as the package's browser entry). */
/** browser/openvibe-sdk.mjs: one self-contained ES module with the browser-safe subpaths (same shape as the package's browser entry). */
import * as auth from './auth-browser';
import * as auth from './auth-browser';
import * as registry from './registry';
import * as registry from './registry';
import * as modules from './modules';
import * as modules from './modules';
import * as realtime from './realtime';
import * as realtime from './realtime';
import * as media from './media';
import * as media from './media';
+import * as composer from './composer';
import * as cam from './cam';
import * as cam from './cam';
import * as mediaUpload from './media-upload';
import * as mediaUpload from './media-upload';
import * as ui from './ui';
import * as ui from './ui';
import * as community from './community';
import * as community from './community';
import * as homeFeed from './home-feed';
import * as homeFeed from './home-feed';
+import * as feed from './feed';
import * as bot from './bot';
import * as bot from './bot';
import * as search from './search';
import * as search from './search';
import * as bench from './bench';
import * as bench from './bench';
@@ -22,6 +26,7 @@ import * as chat from './chat';
@@ -22,6 +26,7 @@ import * as chat from './chat';
import * as ai from './ai';
import * as ai from './ai';
import * as credentials from './credentials';
import * as credentials from './credentials';
import * as jobs from './jobs';
import * as jobs from './jobs';
+import * as agents from './agents';
import * as tools from './tools';
import * as tools from './tools';
import * as projects from './projects';
import * as projects from './projects';
import * as geo from './geo';
import * as geo from './geo';
@@ -33,16 +38,19 @@ import * as core from './core';
@@ -33,16 +38,19 @@ import * as core from './core';
export const createWalletClient: typeof import('./wallet').createWalletClient;
export const createWalletClient: typeof import('./wallet').createWalletClient;
export * from './core';
export * from './core';
+export * as agentSheet from './agent-sheet';
export * as auth from './auth-browser';
export * as auth from './auth-browser';
export * as registry from './registry';
export * as registry from './registry';
export * as modules from './modules';
export * as modules from './modules';
export * as realtime from './realtime';
export * as realtime from './realtime';
export * as media from './media';
export * as media from './media';
+export * as composer from './composer';
export * as cam from './cam';
export * as cam from './cam';
export * as mediaUpload from './media-upload';
export * as mediaUpload from './media-upload';
export * as ui from './ui';
export * as ui from './ui';
export * as community from './community';
export * as community from './community';
export * as homeFeed from './home-feed';
export * as homeFeed from './home-feed';
+export * as feed from './feed';
export * as bot from './bot';
export * as bot from './bot';
export * as search from './search';
export * as search from './search';
export * as bench from './bench';
export * as bench from './bench';
@@ -55,6 +63,7 @@ export * as chat from './chat';
@@ -55,6 +63,7 @@ export * as chat from './chat';
export * as ai from './ai';
export * as ai from './ai';
export * as credentials from './credentials';
export * as credentials from './credentials';
export * as jobs from './jobs';
export * as jobs from './jobs';
+export * as agents from './agents';
export * as tools from './tools';
export * as tools from './tools';
export * as projects from './projects';
export * as projects from './projects';
export * as geo from './geo';
export * as geo from './geo';
@@ -64,11 +73,13 @@ export * as trust from './trust';
@@ -64,11 +73,13 @@ export * as trust from './trust';
export * as levels from './levels';
export * as levels from './levels';
export * as userCard from './user-card';
export * as userCard from './user-card';
+export * as player from './player';
declare const sdk: typeof core & {
declare const sdk: typeof core & {
createWalletClient: typeof createWalletClient;
createWalletClient: typeof createWalletClient;
userCard: typeof userCard;
userCard: typeof userCard;
+ player: typeof player;
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; cam: typeof cam; mediaUpload: typeof mediaUpload; ui: typeof ui; community: typeof community; homeFeed: typeof homeFeed; bot: typeof bot; search: typeof search; bench: typeof bench; meme: typeof meme; vision: typeof vision; themes: typeof themes; design: typeof design; redact: typeof redact; chat: typeof chat; ai: typeof ai; credentials: typeof credentials; jobs: typeof jobs; tools: typeof tools; projects: typeof projects; geo: typeof geo; history: typeof history; locker: typeof locker; trust: typeof trust; levels: typeof levels;
+ media: typeof media; composer: typeof composer; cam: typeof cam; mediaUpload: typeof mediaUpload; ui: typeof ui; community: typeof community; homeFeed: typeof homeFeed; feed: typeof feed; bot: typeof bot; search: typeof search; bench: typeof bench; meme: typeof meme; vision: typeof vision; themes: typeof themes; design: typeof design; redact: typeof redact; chat: typeof chat; ai: typeof ai; credentials: typeof credentials; jobs: typeof jobs; agents: typeof agents; tools: typeof tools; projects: typeof projects; geo: typeof geo; history: typeof history; locker: typeof locker; trust: typeof trust; levels: typeof levels;
};
};
export default sdk;
export default sdk;
@@ -93,11 +93,30 @@ export interface RoomMemory {
@@ -93,11 +93,30 @@ export interface RoomMemory {
can_edit: boolean;
can_edit: boolean;
me: string | null;
me: string | null;
}
}
+/** chat.person-memory-result@1. Private fields appear only for the person and channel owner. */
+export interface PersonMemoryResult {
+ username: string;
+ level: 'day' | 'week' | 'month';
+ channel: string | null;
+ opted_out: boolean;
+ enabled: boolean;
+ entries: {
+ level: 'day' | 'week' | 'month'; period_start: string; channel_user_id: number;
+ summary: string; topics: string[];
+ highlights: { detail: string; stream_id: number | null; at: string | null }[];
+ message_count: number; tone?: string; catchphrases?: string[]; people?: string[];
+ }[];
+}
export interface ChatClient {
export interface ChatClient {
/** The typed address previews a message carries (chat.embed@1): [] when it has none. */
/** The typed address previews a message carries (chat.embed@1): [] when it has none. */
embeds(message?: { embeds?: unknown } | null): ChatEmbed[];
embeds(message?: { embeds?: unknown } | null): ChatEmbed[];
history: ChatHistoryHelper;
history: ChatHistoryHelper;
+ people: {
+ memory(username: string, query?: { channel?: string; level?: 'day' | 'week' | 'month' }, opts?: Opts): Promise<PersonMemoryResult>;
+ forget(username: string, opts?: Opts): Promise<{ deleted: boolean; opted_out: boolean }>;
+ preferences(username: string, optedOut: boolean, opts?: Opts): Promise<{ opted_out: boolean }>;
+ };
/** Ask Vibe using Auto, one catalogue model, or two to three models side by side. */
/** Ask Vibe using Auto, one catalogue model, or two to three models side by side. */
ask(input: { message: string; models?: string[]; slug?: string; fresh?: boolean; system_prompt?: string; temperature?: number }, opts?: Opts): Promise<{ chat: Json; message: Json }>;
ask(input: { message: string; models?: string[]; slug?: string; fresh?: boolean; system_prompt?: string; temperature?: number }, opts?: Opts): Promise<{ chat: Json; message: Json }>;
pick(messageId: number, model: string, opts?: Opts): Promise<{ chat: Json; model: string }>;
pick(messageId: number, model: string, opts?: Opts): Promise<{ chat: Json; model: string }>;
new file mode 100644
new file mode 100644
@@ -0,0 +1,20 @@
@@ -0,0 +1,20 @@
+/** openvibe-sdk/codes: OpenVibe.Codes — the person's coding tasks as the Dock's Code tab lists, starts and stops them. */
+type Opts = { signal?: AbortSignal };
+type CodesJson = Record<string, any>;
+export type CodesTaskMode = 'create' | 'openvibe' | 'repo';
+export type CodesTaskStatus = 'queued' | 'running' | 'needs-you' | 'done' | 'failed' | 'stopped';
+export interface CodesTaskSummary {
+ id: string; title: string; status: CodesTaskStatus; mode: CodesTaskMode; repo: string;
+ started_at: string; updated_at: string; steps: number; url: string; preview_url: string | null; pr_url: string | null;
+}
+export interface CodesTaskCreate {
+ text: string; mode: CodesTaskMode; repo?: string; attachments?: string[];
+}
+export interface CodesClient {
+ tasks: {
+ list(opts?: Opts & { limit?: number }): Promise<{ tasks: CodesTaskSummary[] }>;
+ create(req: CodesTaskCreate, opts?: Opts): Promise<{ id: string | null; url: string }>;
+ stop(id: string, opts?: Opts): Promise<{ ok: true }>;
+ };
+}
+export declare function createCodesClient(client: CodesJson, opts?: { baseUrl?: string }): { tasks: CodesClient['tasks'] };
@@ -12,7 +12,7 @@ export type CallOptions = ActingOptions & { signal?: AbortSignal };
@@ -12,7 +12,7 @@ export type CallOptions = ActingOptions & { signal?: AbortSignal };
export interface CommentQuery { after?: string | number; sort?: 'old' | 'new'; limit?: number; parent?: string | number; }
export interface CommentQuery { after?: string | number; sort?: 'old' | 'new'; limit?: number; parent?: string | number; }
export type CommentAttachment = AiFile & { url?: string };
export type CommentAttachment = AiFile & { url?: string };
-export interface Comment { id: number; thread_id: number | string; parent_id: number | null; message: string | null; reply_count?: number | null; replies?: Comment[]; attachments?: CommentAttachment[]; [field: string]: unknown; }
+export interface Comment { id: number; thread_id: number | string; parent_id: number | null; reply_to_id: number | null; reply_to: { id: number; display_name: string } | null; message: string | null; reply_count?: number | null; replies?: Comment[]; attachments?: CommentAttachment[]; [field: string]: unknown; }
export interface CommentPage { thread: Record<string, unknown>; comments: Comment[]; next_cursor: string | null; viewer: Record<string, boolean>; }
export interface CommentPage { thread: Record<string, unknown>; comments: Comment[]; next_cursor: string | null; viewer: Record<string, boolean>; }
export interface CommentsApi {
export interface CommentsApi {
resolve(ref: EntityRef, opts?: CallOptions): Promise<{ thread: Record<string, unknown>; created: boolean }>;
resolve(ref: EntityRef, opts?: CallOptions): Promise<{ thread: Record<string, unknown>; created: boolean }>;
@@ -60,7 +60,7 @@ export interface FeedItem {
@@ -60,7 +60,7 @@ export interface FeedItem {
site: string;
site: string;
site_name: string;
site_name: string;
actor: { name: string | null; avatar: string | null } | null;
actor: { name: string | null; avatar: string | null } | null;
- media: { thumb: string; ratio: number; length?: number } | null;
+ media: { thumb: string; preview?: string; ratio: number; length?: number } | null;
counts: { views?: number; likes?: number; comments?: number };
counts: { views?: number; likes?: number; comments?: number };
/** True while on air; false on an ended stream. Absent for other kinds. */
/** True while on air; false on an ended stream. Absent for other kinds. */
live_now?: boolean;
live_now?: boolean;
@@ -76,10 +76,14 @@ export interface PostsApi {
@@ -76,10 +76,14 @@ export interface PostsApi {
export interface CommunityClient {
export interface CommunityClient {
pastes: PastesApi;
pastes: PastesApi;
comments: CommentsApi;
comments: CommentsApi;
- feed(query?: FeedQuery, opts?: CallOptions): Promise<{ items: FeedItem[]; next_cursor: string | null }>;
+ feed: FeedReader;
posts: PostsApi;
posts: PostsApi;
as(who: string | ActingOptions): CommunityClient;
as(who: string | ActingOptions): CommunityClient;
headers(): Record<string, string>;
headers(): Record<string, string>;
}
}
+export interface FeedReader {
+ (query?: FeedQuery, opts?: CallOptions): Promise<{ items: FeedItem[]; next_cursor: string | null }>;
+ list(query?: Pick<FeedQuery, 'kind' | 'cursor'>, opts?: CallOptions): Promise<{ items: FeedItem[]; next_cursor: string | null }>;
+}
export declare function createCommunityClient(client: OpenVibeClient, defaults?: ActingOptions & { baseUrl?: string }): CommunityClient;
export declare function createCommunityClient(client: OpenVibeClient, defaults?: ActingOptions & { baseUrl?: string }): CommunityClient;
export declare function actingHeaders(opts?: ActingOptions): Record<string, string>;
export declare function actingHeaders(opts?: ActingOptions): Record<string, string>;
new file mode 100644
new file mode 100644
@@ -0,0 +1,75 @@
@@ -0,0 +1,75 @@
+import type { OpenVibeClient } from './core';
+
+/** composer.attachment@1: one file a person attached to a message. */
+export interface ComposerAttachment {
+ id: string;
+ kind: 'image' | 'video' | 'audio' | 'file' | 'code';
+ name: string;
+ size: number;
+ mime: string;
+ url: string;
+ thumb?: string | null;
+ w?: number | null;
+ h?: number | null;
+ expires_at: string;
+}
+
+/** composer.prefs@1: a person's composer settings on every site. */
+export interface ComposerPrefs {
+ /** The revision that was read; a PUT names it, and a moved one is 409 composer.rev_conflict. */
+ rev: number;
+ send_key: 'enter' | 'mod-enter';
+ tools: Array<{ id: string; hidden: boolean }>;
+ density: 'comfy' | 'compact';
+ voice: boolean;
+ snippets: Array<{ trigger: string; label: string; text: string }>;
+}
+
+/** The document a PUT sends: the fields, with rev = the revision it read (0 for a new person). */
+export type ComposerPrefsInput = Omit<ComposerPrefs, 'rev'>;
+
+/** composer.mention@1: one entry of the composer's @ menu. */
+export interface ComposerMention {
+ kind: 'person' | 'repo' | 'site' | 'file';
+ id: string;
+ label: string;
+ handle?: string;
+ avatar?: string | null;
+ url: string;
+}
+
+export type ComposerFile = Blob | ArrayBuffer | ArrayBufferView | string;
+
+export interface UploadOptions {
+ onProgress?: (p: { loaded: number; total: number; lengthComputable: boolean }) => void;
+ signal?: AbortSignal;
+ filename?: string;
+ contentType?: string;
+ /** An XMLHttpRequest implementation, for a non-browser shell or a test. */
+ xhr?: new () => XMLHttpRequest;
+}
+
+export interface ComposerClient {
+ attachments: {
+ upload(file: ComposerFile, opts?: UploadOptions): Promise<ComposerAttachment>;
+ /** true when it was deleted, false when it was already gone. */
+ remove(id: string): Promise<boolean>;
+ };
+ composer: {
+ prefs: {
+ get(): Promise<ComposerPrefs>;
+ /** A stale rev rejects with an OpenVibeError: status 409, code composer.rev_conflict, err.problem.current. */
+ put(prefs: ComposerPrefsInput, rev: number): Promise<ComposerPrefs>;
+ };
+ };
+ mentions: {
+ search(q?: string, kinds?: Array<'person' | 'repo' | 'site' | 'file'> | string): Promise<ComposerMention[]>;
+ };
+ MEDIA_ORIGIN: string;
+ NETWORK_ORIGIN: string;
+}
+
+export declare function createComposerClient(client: OpenVibeClient, opts?: { mediaBaseUrl?: string; networkBaseUrl?: string }): ComposerClient;
+export declare const MEDIA_ORIGIN: string;
+export declare const NETWORK_ORIGIN: string;
+export declare const ATTACHMENT_MAX: number;
@@ -1969,9 +1969,21 @@ export interface ServiceManifest {
@@ -1969,9 +1969,21 @@ export interface ServiceManifest {
statusApi?: string;
statusApi?: string;
};
};
/**
/**
- * The room's launcher group in OpenVibe.Shared rooms.js and OpenVibe.Network frame/sites.js; optional so older manifests retain their meaning.
+ * The site's launcher group: the eight current groups are watch, talk, play, read, shop, tools, make and build (OpenVibe.Network frame/sites.js GROUPS), whose order and labels the menu, the footer and the home pages show (OpenVibe.Shared rooms.js repeats the ids for its rooms). The pre-0.400.0 ids streams, games, answers and people stay accepted for older manifests.
*/
*/
- neighbourhood?: "streams" | "games" | "answers" | "make" | "shop" | "people" | "build";
+ neighbourhood?:
+ | "watch"
+ | "talk"
+ | "play"
+ | "read"
+ | "shop"
+ | "tools"
+ | "make"
+ | "build"
+ | "streams"
+ | "games"
+ | "answers"
+ | "people";
/**
/**
* This site's own items on the network's footer strip, read with the rest of it at openvibe.network GET /api/frame/online?site=<id>: a label, an icon from the shared glyph set, an https link on an OpenVibe domain (any openvibe.* host), and either a count the site reports or a fixed line. Network checks every field again before it reaches a page.
* This site's own items on the network's footer strip, read with the rest of it at openvibe.network GET /api/frame/online?site=<id>: a label, an icon from the shared glyph set, an https link on an OpenVibe domain (any openvibe.* host), and either a count the site reports or a fixed line. Network checks every field again before it reaches a page.
*
*
@@ -333,8 +333,12 @@ export type ExchangeEarnings = {
@@ -333,8 +333,12 @@ export type ExchangeEarnings = {
};
};
export type ExchangeKey = { id: string; name: string; prefix: string; monthly_cap_usd: number; allowed_models: string[]; created_at: string; revoked_at: string | null };
export type ExchangeKey = { id: string; name: string; prefix: string; monthly_cap_usd: number; allowed_models: string[]; created_at: string; revoked_at: string | null };
-export type ExchangeSearchOptions = { engine?: string; limit?: number; license?: string; commercial?: boolean; format?: string; orientation?: string; size?: string; style?: 'line' | 'solid' | 'any'; set?: string; signal?: AbortSignal };
-export type ExchangeSearchResult = { results: Array<{ title: string; url: string; snippet: string; engine: string; published: string | null; thumbnail?: string | null; license?: string | null; author?: string | null; source_page?: string | null; attribution?: string | null }>; receipt: { engine: string; cost_usd: number; time_ms: number } };
+export type ExchangeSearchOptions = { engine?: string; limit?: number; license?: string; commercial?: boolean; format?: string; orientation?: string; size?: 'small' | 'medium' | 'large'; color?: 'black_and_white' | 'black' | 'white' | 'yellow' | 'orange' | 'red' | 'purple' | 'magenta' | 'green' | 'teal' | 'blue'; type?: 'icon' | 'photo' | 'illustration' | 'vector'; after?: string; safe_search?: 'strict'; style?: 'line' | 'solid' | 'any'; set?: string; signal?: AbortSignal };
+export interface CatalogSearchFilters { provider?: string; capability?: string; min_price?: number; max_price?: number; max_speed_ms?: number; sort?: 'relevance' | 'price' | 'speed' | 'quality'; rating_kind?: string; }
+export interface CatalogModelResult { model: string; name: string; family: string | null; capabilities: Record<string, boolean>; provider: string; price: { in: number; out: number; cached?: number; per?: string; currency?: string } | null; speed: { p50_ms?: number; p95_ms?: number; tokens_per_second?: number } | null; quality: { elo: number; games: number; kind: string } | null; offers: number; task_match: string | null; score: number; }
+export interface CatalogSearchResponse { q: string; count: number; results: CatalogModelResult[]; quality_unavailable: boolean; }
+export declare function createCatalogSearchClient(options?: { baseUrl?: string; fetchImpl?: typeof fetch }): { search(q: string, filters?: CatalogSearchFilters, options?: { signal?: AbortSignal }): Promise<CatalogSearchResponse> };
+export type ExchangeSearchResult = { results: Array<{ title: string; url: string; snippet: string; engine: string; published: string | null; indexed_on?: string | null; type?: string | null; thumbnail?: string | null; license?: string | null; license_url?: string | null; author?: string | null; author_url?: string | null; source_page?: string | null; attribution?: string | null; width?: number | null; height?: number | null }>; receipt: { engine: string; cost_usd: number; time_ms: number } };
export type ExchangeClient = {
export type ExchangeClient = {
keys: {
keys: {
list(opts?: { signal?: AbortSignal }): Promise<{ keys: ExchangeKey[] }>;
list(opts?: { signal?: AbortSignal }): Promise<{ keys: ExchangeKey[] }>;
new file mode 100644
new file mode 100644
@@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
+import type { CallOptions } from './core';
+import type { FeedItem, FeedKind } from './community';
+
+export interface FeedClient {
+ list(query?: { kind?: FeedKind; cursor?: string }, opts?: CallOptions): Promise<{ items: FeedItem[]; next_cursor: string | null }>;
+}
+export declare function createFeedClient(client: import('./core').OpenVibeClient, options?: { communityBaseUrl?: string }): FeedClient;
@@ -4,9 +4,45 @@ export interface HomeLayout { home: string[]; folders: Array<{ id: string; name:
@@ -4,9 +4,45 @@ export interface HomeLayout { home: string[]; folders: Array<{ id: string; name:
export interface TodayPoint { day: string; people: number | null; streams: number | null; posts: number | null; new_videos: number | null; }
export interface TodayPoint { day: string; people: number | null; streams: number | null; posts: number | null; new_videos: number | null; }
export interface Today { day: string; people_today: number | null; streams_today: number | null; posts_today: number | null; new_videos_today: number | null; series: TodayPoint[]; updated_at: string; }
export interface Today { day: string; people_today: number | null; streams_today: number | null; posts_today: number | null; new_videos_today: number | null; series: TodayPoint[]; updated_at: string; }
export interface FeedItem { id: number; source: { service: string; type: string; id: string }; title: string; url: string; label: string | null; via?: string; picture_url?: string; actor: Record<string, unknown> | null; occurred_at: string | null; }
export interface FeedItem { id: number; source: { service: string; type: string; id: string }; title: string; url: string; label: string | null; via?: string; picture_url?: string; actor: Record<string, unknown> | null; occurred_at: string | null; }
+/** One destination in the signed-out "things people do now" list and in a person's Try next: a public label, an
+ * https address, an icon id and the one-line reason it is there, with the Report numbers behind them when known. */
+export interface PopularItem {
+ id: string;
+ kind: 'site' | 'tool';
+ name: string;
+ url: string;
+ icon: string;
+ reason: string;
+ score?: number;
+ opens_24h?: number | null;
+ opens_7d?: number | null;
+ opens_prev_7d?: number | null;
+ growth_7d_pct?: number | null;
+ uses_24h?: number | null;
+ uses_7d?: number | null;
+}
+export interface Popular {
+ source: 'report' | 'fallback';
+ reason: string;
+ generated_at: string | null;
+ fetched_at: string | null;
+ stale: boolean;
+ items: PopularItem[];
+ error?: string;
+}
+export interface TryNext {
+ source: 'report' | 'fallback';
+ basis: 'co-usage' | 'popular' | 'starter';
+ generated_at: string;
+ items: PopularItem[];
+}
export interface HomeFeedClient {
export interface HomeFeedClient {
home: { get(opts?: { signal?: AbortSignal }): Promise<HomeLayout>; set(layout: HomeLayout, opts?: { signal?: AbortSignal }): Promise<HomeLayout> };
home: { get(opts?: { signal?: AbortSignal }): Promise<HomeLayout>; set(layout: HomeLayout, opts?: { signal?: AbortSignal }): Promise<HomeLayout> };
- network: { today(opts?: { signal?: AbortSignal }): Promise<Today> };
+ network: {
+ today(opts?: { signal?: AbortSignal }): Promise<Today>;
+ popular(opts?: { limit?: number; signal?: AbortSignal }): Promise<Popular>;
+ tryNext(opts?: { limit?: number; signal?: AbortSignal }): Promise<TryNext>;
+ };
feed: {
feed: {
list(opts?: { for?: 'me'; cursor?: string; signal?: AbortSignal }): Promise<{ items: FeedItem[]; next_cursor: string | null }>;
list(opts?: { for?: 'me'; cursor?: string; signal?: AbortSignal }): Promise<{ items: FeedItem[]; next_cursor: string | null }>;
post(text: string, opts?: { link?: string; signal?: AbortSignal }): Promise<{ id: number; post_id: number; text: string; url: string; created_at: string }>;
post(text: string, opts?: { link?: string; signal?: AbortSignal }): Promise<{ id: number; post_id: number; text: string; url: string; created_at: string }>;
@@ -15,6 +15,7 @@ export * as mediaUpload from './media-upload';
@@ -15,6 +15,7 @@ export * as mediaUpload from './media-upload';
export * as ui from './ui';
export * as ui from './ui';
export * as community from './community';
export * as community from './community';
export * as homeFeed from './home-feed';
export * as homeFeed from './home-feed';
+export * as feed from './feed';
export * as bot from './bot';
export * as bot from './bot';
export * as search from './search';
export * as search from './search';
export * as bench from './bench';
export * as bench from './bench';
@@ -27,6 +28,7 @@ export * as chatApps from './chat-apps';
@@ -27,6 +28,7 @@ export * as chatApps from './chat-apps';
export * as ai from './ai';
export * as ai from './ai';
export * as credentials from './credentials';
export * as credentials from './credentials';
export * as jobs from './jobs';
export * as jobs from './jobs';
+export * as agents from './agents';
export * as tools from './tools';
export * as tools from './tools';
export * as projects from './projects';
export * as projects from './projects';
export * as resources from './resources';
export * as resources from './resources';
@@ -48,6 +50,7 @@ export * as host from './host';
@@ -48,6 +50,7 @@ export * as host from './host';
export * as news from './news';
export * as news from './news';
export * as food from './food';
export * as food from './food';
export * as actor from './actor';
export * as actor from './actor';
+export * as codes from './codes';
export * as run from './run';
export * as run from './run';
export * as video from './video';
export * as video from './video';
export * as pics from './pics';
export * as pics from './pics';
@@ -69,5 +72,7 @@ export * as locker from './locker';
@@ -69,5 +72,7 @@ export * as locker from './locker';
export const createWalletClient: typeof import('./wallet').createWalletClient;
export const createWalletClient: typeof import('./wallet').createWalletClient;
export * as levels from './levels';
export * as levels from './levels';
-
+export * as composer from './composer';
export * as userCard from './user-card';
export * as userCard from './user-card';
+export * as player from './player';
+export * as agentSheet from './agent-sheet';
new file mode 100644
new file mode 100644
@@ -0,0 +1,17 @@
@@ -0,0 +1,17 @@
+export interface CurrentPlayer {
+ id: string;
+ src: string;
+ title: string;
+ href?: string;
+ live: boolean;
+ sound: boolean;
+ playing?: boolean;
+ side?: { src: string; title: string };
+}
+export interface PlayerEvent { reason: 'open' | 'close' | 'declare' | 'handoff' | 'state'; card: CurrentPlayer | null }
+/** The active portable live stream, if one exists on this page. */
+export function current(): CurrentPlayer | null;
+/** Add the active player to an OpenVibe destination URL for same-tab navigation. */
+export function handoff(url: string): string;
+/** Subscribe to changes. Returns a function that removes the listener. */
+export function on(listener: (event: PlayerEvent) => void): () => void;
@@ -41,9 +41,11 @@ export interface SsoClientOptions {
@@ -41,9 +41,11 @@ export interface SsoClientOptions {
export type SsoUser = Omit<UserTokenClaims, 'iat' | 'exp' | 'aud' | 'iss' | 'nbf' | 'jti'> & Record<string, unknown>;
export type SsoUser = Omit<UserTokenClaims, 'iat' | 'exp' | 'aud' | 'iss' | 'nbf' | 'jti'> & Record<string, unknown>;
export interface SsoClient {
export interface SsoClient {
- /** GET /login /callback /logout /me /health, POST /fedcm /refresh; mount at /auth. */
+ /** GET /login /callback /logout /me /health, POST /fedcm /refresh /handoff; mount at /auth. */
router(express: any): any;
router(express: any): any;
- handlers: { login: Handler; callback: Handler; fedcm: Handler; logout: Handler; me: Handler; refresh: Handler; health: Handler };
+ handlers: { login: Handler; callback: Handler; fedcm: Handler; logout: Handler; me: Handler; refresh: Handler; health: Handler; handoffMint: Handler };
+ /** Redeems a single-use cross-site ticket before the page renders; never replaces an existing session. */
+ handoff(): (req: Req, res: Res, next: Next) => void;
/** Sets req.user and req.token when a valid session is presented. */
/** Sets req.user and req.token when a valid session is presented. */
optionalAuth(): (req: Req, res: Res, next: Next) => void;
optionalAuth(): (req: Req, res: Res, next: Next) => void;
/** 401 JSON without a valid session. */
/** 401 JSON without a valid session. */
@@ -64,6 +66,8 @@ export declare function createSsoClient(opts: SsoClientOptions): SsoClient;
@@ -64,6 +66,8 @@ export declare function createSsoClient(opts: SsoClientOptions): SsoClient;
/** A same-site path, this site's https origin or the Network's; anything else (control characters, backslashes) is '/'. */
/** A same-site path, this site's https origin or the Network's; anything else (control characters, backslashes) is '/'. */
export declare function sanitizeNext(next: unknown, opts?: { baseUrl?: string; networkUrl?: string }): string;
export declare function sanitizeNext(next: unknown, opts?: { baseUrl?: string; networkUrl?: string }): string;
export declare function withParam(target: string, key: string, value: string): string;
export declare function withParam(target: string, key: string, value: string): string;
+/** Remove the one-time handoff ticket from a path+query URL, preserving all other parameters and the fragment. */
+export declare function stripHandoffParam(target: string): string;
/** The self-contained page a sign-in that cannot finish shows (text is escaped). */
/** The self-contained page a sign-in that cannot finish shows (text is escaped). */
export declare function failurePage(o: { siteName: string; title: string; text: string; retryHref: string; backHref: string }): string;
export declare function failurePage(o: { siteName: string; title: string; text: string; retryHref: string; backHref: string }): string;
/** The payload of a JWT, unverified; null when it does not parse. */
/** The payload of a JWT, unverified; null when it does not parse. */
@@ -72,3 +76,4 @@ export declare function fedcmNonceMatches(token: string, nonce: string): boolean
@@ -72,3 +76,4 @@ export declare function fedcmNonceMatches(token: string, nonce: string): boolean
export declare function claimsToUser(claims: Record<string, any> | null): SsoUser | null;
export declare function claimsToUser(claims: Record<string, any> | null): SsoUser | null;
export declare function parseCookies(req: { headers?: Record<string, any> }): Record<string, string>;
export declare function parseCookies(req: { headers?: Record<string, any> }): Record<string, string>;
export declare const COOKIES: Readonly<{ access: string; refresh: string; hint: string }>;
export declare const COOKIES: Readonly<{ access: string; refresh: string; hint: string }>;
+export declare const HANDOFF_PARAM: 'ov_handoff';
@@ -10,9 +10,14 @@ export interface Wallet {
@@ -10,9 +10,14 @@ export interface Wallet {
export interface StoreItem { id: string; name: string; description: string; issuer: string; prices: { coins?: number; vibes?: number }; preview: { emoji?: string; token?: string; media_id?: string }; kind: string; slots: string[]; surfaces: Array<{ surface: string; renderer?: string }>; owned: boolean; available: boolean; }
export interface StoreItem { id: string; name: string; description: string; issuer: string; prices: { coins?: number; vibes?: number }; preview: { emoji?: string; token?: string; media_id?: string }; kind: string; slots: string[]; surfaces: Array<{ surface: string; renderer?: string }>; owned: boolean; available: boolean; }
export interface Purchase { id: string; subject: string; item_id: string; name: string; issuer: string; currency: 'coins' | 'vibes'; amount: number; status: 'pending' | 'fulfilled'; instance_id: string | null; payment_id: string | null; }
export interface Purchase { id: string; subject: string; item_id: string; name: string; issuer: string; currency: 'coins' | 'vibes'; amount: number; status: 'pending' | 'fulfilled'; instance_id: string | null; payment_id: string | null; }
export interface Checkout { intent: Record<string, unknown>; checkout_url: string | null; }
export interface Checkout { intent: Record<string, unknown>; checkout_url: string | null; }
+export type BalanceCurrency = 'vibes' | 'opencoins' | 'channel-points';
+export type BalancePart = 'earned' | 'spent' | 'received' | 'sent';
+export interface BalanceMove { id: string | number; at: string; delta: number; part: BalancePart; label: string; detail?: string; reason?: string | null; reason_key?: string; subject?: { name: string; url: string } | null; streamer_id?: number; }
+export interface BalanceChanges { currency: BalanceCurrency; balance: number | Record<string, number> | null; since: string | number | null; moves: BalanceMove[]; parts: Record<BalancePart, number>; net: number; cursor: string | number | null; next_since: string | number | null; read_cursor: string | number | null; streamer?: number; truncated?: boolean; }
+export interface WalletChanges { vibes: BalanceChanges | null; opencoins: BalanceChanges | null; channel_points: BalanceChanges | null; state: { vibes: 'open' | 'unavailable' | 'not_authoritative'; opencoins: 'open' | 'unavailable'; channel_points: 'open' | 'unavailable' | 'unlinked' | 'needs_streamer' }; }
type Options = { signal?: AbortSignal };
type Options = { signal?: AbortSignal };
-export function createWalletClient(client: OpenVibeClient, options?: { billingUrl?: string; inventoryUrl?: string }): {
- wallet: { get(options?: Options): Promise<Wallet>; topUp(input: { provider: string; amount_cents: number; idempotency_key: string }, options?: Options): Promise<Checkout> };
+export function createWalletClient(client: OpenVibeClient, options?: { billingUrl?: string; inventoryUrl?: string; networkUrl?: string }): {
+ wallet: { get(options?: Options): Promise<Wallet>; changes(options?: Options & { since_vibes?: string; since_opencoins?: number; since_channel_points?: number; streamer?: number; limit?: number }): Promise<WalletChanges>; markRead(input: { currency: BalanceCurrency; cursor: string | number; streamer?: number }, options?: Options): Promise<{ currency: BalanceCurrency; read_cursor: string | number; streamer?: number }>; topUp(input: { provider: string; amount_cents: number; idempotency_key: string }, options?: Options): Promise<Checkout> };
vibes: { buy(input: { provider: string; vibes: number; idempotency_key: string }, options?: Options): Promise<Checkout> };
vibes: { buy(input: { provider: string; vibes: number; idempotency_key: string }, options?: Options): Promise<Checkout> };
store: { list(options?: Options): Promise<{ items: StoreItem[] }>; buy(id: string, input: { currency: 'coins' | 'vibes'; idempotency_key: string }, options?: Options): Promise<Purchase>; submit(id: string, prices: { coins?: number; vibes?: number }, options?: Options): Promise<{ definition_id: string; issuer: string; prices: { coins?: number; vibes?: number }; status: 'in_review' }> };
store: { list(options?: Options): Promise<{ items: StoreItem[] }>; buy(id: string, input: { currency: 'coins' | 'vibes'; idempotency_key: string }, options?: Options): Promise<Purchase>; submit(id: string, prices: { coins?: number; vibes?: number }, options?: Options): Promise<{ definition_id: string; issuer: string; prices: { coins?: number; vibes?: number }; status: 'in_review' }> };
items: { list(options?: Options & { cursor?: string; limit?: number; kind?: string }): Promise<{ subject: string; instances: import('./inventory').Instance[]; definitions: Record<string, import('./inventory').Definition>; next_cursor: string | null }>; equip(input: { kind: string; slot: string; instance_id: string | null }, options?: Options): Promise<import('./inventory').EquippedSet> };
items: { list(options?: Options & { cursor?: string; limit?: number; kind?: string }): Promise<{ subject: string; instances: import('./inventory').Instance[]; definitions: Record<string, import('./inventory').Definition>; next_cursor: string | null }>; equip(input: { kind: string; slot: string; instance_id: string | null }, options?: Options): Promise<import('./inventory').EquippedSet> };