openvibers/openvibe.sdk Commits
SDK 0.146.0: consolidate six feature releases (#199)
Changes
@@@ -1,5 +1,14 @@# Changelog++## 0.146.0 — 2026-11-10++++- #183: add the private history reader for cursor reads, search, summaries, agent recent reads, settings and owner-scoped deletes.++- #184: add `frame.online()` with member presence, anonymous guest counts and live channel/site items.++- #186: add `quest.today()` for the authenticated daily Quest response.++- #195: add `bot.embedUrl(robotId, mode)` for panel, overlay and compact embeds.++- #196: add merged code search, cited deep answers and cursor iteration to Search.++- #197: add filtered, paged Inventory holdings reads and counts, plus async holding pages.++## 0.145.0 — 2026-11-10- `bench.ratings.get`: shared fitted ratings and confidence intervals with a twenty-game floor. Browser-safe, anonymous and typed; pairs with `ai.metrics` across sites.@@@ -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.108.0++npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.146.0```> Supported browser and server clients for the OpenVibe platform.--**Status:** alpha, v0.145.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.146.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.145.0++npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.146.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,13 +203,14 @@ Until OpenVibe.Tools serves the gateway facade (Tools S6), point the jobs client| `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/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/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`), `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/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/vision` | both | `createVisionClient(client)`: image questions, OCR, private search, network tools, UI checks, consent and face privacy, and `/mcp`. Audience `openvibe.vision`; service id `vision`. || `openvibe-sdk/consent` | server | `createConsent({networkUrl, token, ttlMs?})`: the L24 check before any use of a person's content or likeness for AI or data. `allowed(subject, purpose, {kind, client, feature})` → boolean, `filter(subjects, purpose, opts)` → the allowed subset (batches of 500, input order), `onChange(event)` drops that subject's answers for the purpose on `network.consent.changed`, `mock(map)` for tests. **Fails closed** (network error, 2 s timeout, non-200 or malformed answer → false); caches 60 s; logs at most once a minute. [Example](docs/consent.md) |--| `openvibe-sdk/search` | both | `createSearchClient(client, {actingSubject?})`: `query(text, {owner, type, lang, filter, facets, limit, cursor})`, `iterate(text, {…, max})`, `suggest(text, {owner, type, limit})`, `document(owner, type, id)` (null when missing or not visible) |++| `openvibe-sdk/search` | both | `createSearchClient(client, {actingSubject?})`: `query(text, {owner, type, lang, filter, facets, limit, cursor})`, `iterate(text, {…, max})`, `suggest(text, {owner, type, limit})`, `document(owner, type, id)` (null when missing or not visible), `code(text, opts)` for merged code results or `{deep: true}` for a cited answer, `codeIterate(text, opts)` || `openvibe-sdk/limits` | node | `createActorLimiter({limits: {minute, hour, day}, actor?, maxActors?, onLimited?})` → `limits(name, {minute?, hour?, day?})` Express middleware: per-actor fixed windows at a capability boundary (service principal, else the signed-in person, else `ip:`), 429 problem+json `rate_limited` with Retry-After before the handler; `defaultActor(req)`, `.stats()` || `openvibe-sdk/account-data` | node | `createAccountData({db, service, tables: [{table, subject, value?, file?, columns?, erase?}], extraExport?, extraErase?})` → `.apply(event, {send})`, `.consumer({secrets, send, onEvent?})` (a POST /internal/events handler), `.exportPart(subject)`, `.erase(subjects)`, `.ensureSchema()`; `createNetworkSender({networkInternalUrl, clientId, clientSecret})`; `startSubscriptions({eventsUrl, endpoint, secret, networkInternalUrl, clientId, clientSecret})` (the two Events subscriptions, created at boot when missing): a service's share of account export and deletion (ADR-033), idempotent per export/deletion id; telemetry is covered once here (the person's `telemetry_samples` exported as `telemetry.json`, its `subject` set to NULL and the sample kept) unless the table is listed; `uncovered(db, {tables, taskKeyed?, exempt?})` lists the tables with a person column (`subject`, `owner`, `created_by`, `user_id`, `person`) that nothing covers, for a service test to assert is empty || `openvibe-sdk/chat` | both | `createChatClient(client)`: `global.send/history/search`, `rooms.*` (list, create, get, update, messages, send, deleteMessage, join, leave, read, members, setMember), `dms.*` (list, create, get, messages, send, deleteMessage, read, unread, blocks, block, unblock), `exportMine()`, `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) |@@@ -223,10 +224,10 @@ Until OpenVibe.Tools serves the gateway facade (Tools S6), point the jobs client| `openvibe-sdk/placement` | server | `plan(requirements, offers, {rateCards, states, now, current, minGain, weights, success})` → `platform.placement-result@1`; `place(requirements, offers, {…plan, person, step, floor, priors, outcomes, rules, names})` → `{result, line, ranked, rejected, fallback}`: the person's rules and ownership, the quality bar, cost per success and one sentence saying why; `optionKey`, `learn`, `successOf`, `escalate`, `lineFor`; `TRUST_CLASSES`, `TRUST_ORDER`, `DEFAULT_TRUST`; `planAi(requirements, deployments, {enabled, usage, evidence, rates, current})` → `{chosen, rejected, objective, estimate, explain}`: the AI Exchange face of the same planner (hard gates, the cost per success in nano-USD, sparse evidence marked), off by default ([details](docs/placement.md)) || `openvibe-sdk/play` | server | `createPlayClient(client)`: OpenVibe.Play's achievements and leaderboards for any game — `titles.define(title, {name, url?, summary?, achievements, boards})`, `scores.post(title, {board, ref, entries})`, `achievements.unlock(title, {player, achievements})`, `players.adopt(title, guest, person)`, `players.achievements(title, player)`; public `titles.list/get` and `boards.get(title, board, {period, page})` need no token || `openvibe-sdk/download` | server | `createDownloadClient({ key })`: OpenVibe.Download, a person's drive — `files.list/all/get/remove`, `folders.list/create/ensurePath(path)`, `upload({ path \| data, name, folderId })` in parts, `usage()`, and `sync(dir, { to, delete, dryRun, exclude })` backups. The `openvibe-download` command: `sync <folder> --to <drive/path>`, `ls`, `usage` (`OPENVIBE_DOWNLOAD_KEY`, a sync key from openvibe.download/sync) |--| `openvibe-sdk/quest` | server | `createQuestClient(client)`: `daily()`, `myDaily()`, `stepTypes()`, `contributeStepType(step)` for the shared daily quest and reviewable event-backed steps (`quest.steps.contribute`); `appQuests.define/list/retire` for an app's own badge quests (`quest.app_quest.manage`); `eventType(projectId, name)` builds an app event topic. App quests pay badges only; production events count |++| `openvibe-sdk/quest` | server | `createQuestClient(client)`: `daily()`, `myDaily()`, `today()` (authenticated current-day response), `stepTypes()`, `contributeStepType(step)` for the shared daily quest and reviewable event-backed steps (`quest.steps.contribute`); `appQuests.define/list/retire` for an app's own badge quests (`quest.app_quest.manage`); `eventType(projectId, name)` builds an app event topic. App quests pay badges only; production events count || `openvibe-sdk/live` | server | `createLiveClient(client)`: OpenVibe.Live's documented routes. `streams.list/get/recentlyOnline/mine/update/end/heartbeat/endpoint`, `slots.list/create/update/remove/regenerateKey` (a slot is a saved setup with its own stream key), `goLive(slotId, {title, category, tags})` → `{ stream, endpoint }`, `channels.get/mine/update/toggleFollow/url`, `content.feed/moments({type, sort, window, cursor, limit})`; WHIP: `whip.url(streamKey)`, `whip.publish(url, offerSdp, {key?})` → `{ answer, location }` (the client's own token never goes to `/whip`), `whip.end(location)`. Writes are never retried || `openvibe-sdk/streams` | server | `createStreamsClient(client, {baseUrl?})`: an agent's own stream on OpenVibe.Live (the Agent Streams plan, S1; `live.agent-stream@1`, an `ags_…` id with no stream key and no slot). `startAgentStream({title, task, visibility, allowSites})` → `{stream, publish}` (the WHIP url and the one-time `wpt_…` ticket with its expiry), `renewTicket(id)` and `revokeTicket(id)` (the kill switch: Live ends the WHIP session at once), `state(id, {ticket})` (what the desk polls: viewers, `encode`, the curtain, `ends_at`), `postFrame(id, bytes, {ticket, contentType})` (the last frame while Live wants no video, JPEG or WebP at most 200 KB), `setCurtain(id, up, reason)`, `setVisibility(id, v)`, `end(id)` and `viewTicket(id)`. The publish ticket is a bearer of its own on the desk routes; a bad id, visibility, curtain reason or an oversized frame is refused before anything leaves. [Docs](docs/streams.md) |--| `openvibe-sdk/inventory` | server | `createInventoryClient(client)`: OpenVibe.Inventory, the network-wide inventory (ADR-054). Public reads: `kinds.list/get`, `definitions.list({kind, issuer})/get`, `people.items(subject)`, `people.equipped(subject)`, `equipped([subjects])` (≤ 100, for chat renders). The person's own: `me.items()`, `me.equip({kind, slot, instance_id | null})`. An issuer's: `definitions.create/update`, `grants.create({definition_id, subject, idempotency_key})` → `{ instance, created }` (retry-safe on the key), `instances.consume/revoke`. `forPerson(usr_…)` sends `X-OV-Subject` for a service acting for a person. Audience `openvibe.inventory` |++| `openvibe-sdk/inventory` | server | `createInventoryClient(client)`: OpenVibe.Inventory, the network-wide inventory (ADR-054). Public reads: `kinds.list/get`, `definitions.list({kind, issuer})/get`, `people.items(subject)`, `people.holdings(subject, opts)`, `people.holdingsCount(subject, opts)`, `people.holdingPages(subject, opts)`, `people.equipped(subject)`, `equipped([subjects])` (≤ 100, for chat renders). The person's own: `me.items()`, `me.equip({kind, slot, instance_id | null})`. An issuer's: `definitions.create/update`, `grants.create({definition_id, subject, idempotency_key})` → `{ instance, created }` (retry-safe on the key), `instances.consume/revoke`. `forPerson(usr_…)` sends `X-OV-Subject` for a service acting for a person. Audience `openvibe.inventory` || `openvibe-sdk/tips` | server | `createTipsClient(client)`: OpenVibe.Tips' `/api/v1`. A creator's page: `profiles.get/update/totals/sounds/supporters`. Tipping: `tip({creator, amount, message?, goal_id?, pay_with})` → `{ interaction, checkout_url, checkout_ref }`, `send.message/tts/media/sound`. Receipts: `interactions.get/list/recordExternal/hide/restore`. `goals.list/get/create/update/close`, `moderation.queue/log/moderators.*`, `overlays.tokens.*/configs.*/simulate`, `me.export/erase`. Every write carries an Idempotency-Key, which Tips replays once, so retries are safe. Audience `openvibe.tips` || `openvibe-sdk/blog` | server | `createBlogClient(client)`: OpenVibe.Blog for the person who authorized the app. `blogs.get/posts(handle, {limit, offset, all})/feed` (JSON Feed), `changelog({service, kind, area, q, limit, before})` (the network changelog's `blog.changelog-entry@2` rows), `posts.create(handle, {title, body, summary, visibility, …})` → `{ post, revision }`, `posts.get/update(id, {…, expected_revision})/publish(id, {revision})/unpublish/schedule(id, {at, revision, action})/cancelSchedule/revert/remove/revisions/revision(id, n)`. Public capabilities since openvibe-contracts 0.206.0; publish, unpublish, schedule and delete are sensitive. Creating and reverting are never retried on their own. Audience `openvibe.blog` || `openvibe-sdk/reviews` | server | `createReviewsClient(client)`: OpenVibe.Reviews. `resolve({name, url, gtin, sku, mpn, source, external})` (a match or candidates; a name alone never matches), `entities.list({q, limit, offset})/get(ref)/history/aggregate/summaryRevision(ref, n)`, `corrections.submit(ref, {body, target_type, target_id, evidence_url})` (to the editors' queue for the person, never retried on its own). Public capabilities since openvibe-contracts 0.207.0. Audience `openvibe.reviews` |@@@ -3,7 +3,7 @@"stage": "alpha","code": true,"wave": "W2",-- "version": "0.145.0",++ "version": "0.146.0","plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), \u00a73.2; roadmap \u00a730","deployed": false,"deployment": "none: a package released as git tags; services and apps pin a tag",@@@ -36,4 +36,5 @@ module.exports = {tools: require('./src/tools'),projects: require('./src/projects'),geo: require('./src/geo'),++ history: require('./src/history'),};@@@ -1,6 +1,6 @@--// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.145.0, MIT). Do not edit.++// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.146.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). No server-only code: no service tokens, JWT verification, events, identity.++// (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). No server-only code: no service tokens, JWT verification, events, identity./* eslint-disable */const __defs = [// ── 0: browser.js ──────────────────────────────────────────────────@@@ -43,6 +43,7 @@ module.exports = {tools: __load(48),projects: __load(49),geo: __load(50),++ history: __load(51),};},// ── 1: src/core/index.js ───────────────────────────────────────────@@@ -57,7 +58,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.145.0';++const SDK_VERSION = '0.146.0';module.exports = {createClient,@@@ -3676,6 +3677,10 @@ const { isActingSubjectId } = __load(5);const { paginate } = __load(7);const enc = encodeURIComponent;++const DEFAULT_BOT_URL = 'https://openvibe.bot';++// The three views OpenVibe.Bot's embeddable panel renders (docs/controls-embed.md there).++const EMBED_MODES = ['panel', 'overlay', 'compact'];++const ROBOT_ID = /^rob_[A-Za-z0-9_-]{4,60}$/;const DESK_NAME = /^desk-[a-z0-9][a-z0-9-]{0,23}$/;const randomDesk = () => `desk-${Math.random().toString(36).slice(2, 12).padEnd(10, '0')}`;@@@ -3705,6 +3710,19 @@ function createBotClient(client, defaults = {}) {const orNull = (p) => p.catch((err) => { if (isOpenVibeError(err) && err.status === 404) return null; throw err; });const once = { idempotencyKey: false }; // a one-time code is its own idempotency: never repeat the call++ /**++ * The address of a robot's embeddable panel, for a page that frames or links it:++ * `https://openvibe.bot/panel/<rob_…>/embed?mode=panel|overlay|compact`. `panel` is the full panel,++ * `overlay` only the live controls (transparent; over a video) and `compact` one row for a narrow card.++ * No request is made; the host page's origin must be in Bot's BOT_EMBED_ORIGINS to frame it.++ */++ function embedUrl(robotId, mode = 'panel') {++ if (typeof robotId !== 'string' || !ROBOT_ID.test(robotId)) throw new TypeError('robotId must be a Bot robot id (rob_…)');++ if (!EMBED_MODES.includes(mode)) throw new TypeError(`mode must be one of ${EMBED_MODES.join(', ')}`);++ const base = String(baseUrl || DEFAULT_BOT_URL).replace(/\/+$/, '');++ return `${base}/panel/${enc(robotId)}/embed?mode=${mode}`;++ }++const robots = {/** { robots } — a person's own robots; a service must name owner (or act as them). */list: (query = {}, o = {}) => call({ path: '/api/v1/robots', query }, o),@@@ -3846,6 +3864,8 @@ function createBotClient(client, defaults = {}) {},/** The agent redeems a one-time pairing code (the code is the credential; never retried) -> the pair result. */pair: (input = {}, o = {}) => call({ method: 'POST', path: '/api/v1/pair', json: input, ...once }, o),++ /** The embeddable panel's address for a robot and view (no request; see the doc comment above). */++ embedUrl,/** The same client acting for someone else: as('usr_…') or as({ actingSubject }). */as(who) {const next = typeof who === 'string' ? { actingSubject: who } : { ...who };@@@ -3901,6 +3921,23 @@ function fabricOf(text, o = {}) {if (o.answer) q.answer = 'true';return q;}++const CODE_KEYS = ['lang', 'repo', 'licence', 'limit', 'cursor'];++const CODE_BODY_KEYS = ['lang', 'repo', 'licence'];++++/** The query string of GET /api/v1/search/code. */++function codeOf(text, o = {}) {++ const q = { q: String(text == null ? '' : text) };++ for (const k of CODE_KEYS) if (o[k] !== undefined && o[k] !== null && o[k] !== '') q[k] = o[k];++ return q;++}++++/** The body of POST /api/v1/search/code/deep; q is required, the filters are optional. */++function codeBody(text, o = {}) {++ const body = { q: String(text == null ? '' : text) };++ for (const k of CODE_BODY_KEYS) if (o[k] !== undefined && o[k] !== null && o[k] !== '') body[k] = o[k];++ return body;++}++const FACET_KEY = /^[a-z][a-z0-9_]{0,39}$/;function queryOf(text, o = {}) {@@@ -3965,10 +4002,26 @@ function createSearchClient(client, defaults = {}) {suggest: (text, o = {}) => call({ path: pathWith('/api/v1/suggest', queryOf(text, { owner: o.owner, type: o.type, limit: o.limit })) }, o),/** One document by exact id, or null (a document you may not see answers the same as a missing one). */document: (owner, type, id, o = {}) => orNull(call({ path: `/api/v1/documents/${enc(owner)}/${enc(type)}/${enc(id)}` }, o)),++ /** { results, sources, next_cursor } — merged public code; `deep: true` asks the cited answer (signed in). */++ code: (text, o = {}) => (o.deep++ ? call({ method: 'POST', path: '/api/v1/search/code/deep', json: codeBody(text, o), idempotencyKey: false }, o)++ : call({ path: pathWith('/api/v1/search/code', codeOf(text, o)) }, o)),++ /** Every merged code result, page after page (cursor). Deep mode is one answer, not a page stream. */++ async *codeIterate(text, o = {}) {++ if (o.deep) throw new TypeError('codeIterate is for pages; use code(text, { deep: true }) for an answer');++ let cursor = o.cursor || null;++ let seen = 0;++ const max = Number.isFinite(o.max) ? o.max : Infinity;++ do {++ const page = await call({ path: pathWith('/api/v1/search/code', codeOf(text, { ...o, cursor })) }, o);++ for (const r of page.results || []) { if (seen++ >= max) return; yield r; }++ cursor = page.next_cursor || null;++ } while (cursor);++ },};}--module.exports = { createSearchClient, queryOf, fabricOf };++module.exports = { createSearchClient, queryOf, fabricOf, codeOf };},// ── 35: src/bench.js ────────────────────────────────────────────────function (module, exports) {@@@ -6078,6 +6131,34 @@ function createGeoClient(client, { baseUrl, fetch: fetchImpl = null, samples = 3module.exports = { createGeoClient };},++// ── 51: src/history.js ──────────────────────────────────────────────++function (module, exports) {++'use strict';++++/** Reader and owner controls for the signed-in person's private cross-site history. */++function createHistoryClient(client, { baseUrl } = {}) {++ if (!client || typeof client.json !== 'function') throw new TypeError('createHistoryClient: pass an openvibe-sdk/core client');++ const call = (opts, o = {}) => client.json({ service: 'network', baseUrl, ...opts, signal: o.signal });++ const enc = (v) => encodeURIComponent(String(v));++ return {++ /** Newest first. query accepts cursor, limit, day, site, kind and q. */++ list: (query = {}, o = {}) => call({ path: '/api/v1/history', query }, o),++ /** Search titles with PostgreSQL full text; query is natural language. */++ find: (q, { limit, ...o } = {}) => call({ path: '/api/v1/history/find', query: { q, limit } }, o),++ /** Last day or week, using titles and kinds only. */++ summary: (period = 'day', o = {}) => call({ path: '/api/v1/history/summary', query: { period } }, o),++ /** Recent rows for the signed-in person's agent; the server enforces the opt-in. */++ agentRecent: (query = {}, o = {}) => call({ path: '/api/v1/history/agents/recent', query }, o),++ settings: (patch, o = {}) => call({ method: 'PUT', path: '/api/v1/history/settings', json: patch }, o),++ remove: (id, o = {}) => call({ method: 'DELETE', path: `/api/v1/history/${enc(id)}` }, o),++ removeDay: (day, o = {}) => call({ method: 'DELETE', path: `/api/v1/history/day/${enc(day)}` }, o),++ removeSite: (site, o = {}) => call({ method: 'DELETE', path: `/api/v1/history/site/${enc(site)}` }, o),++ clear: (o = {}) => call({ method: 'DELETE', path: '/api/v1/history' }, o),++ };++}++++module.exports = { createHistoryClient };++},];const __cache = new Array(__defs.length);function __load(i) {@@@ -6097,6 +6178,6 @@ export const {realtime, media, cam, mediaUpload, ui, community,homeFeed, bot, search, bench, meme, vision,themes, design, redact, chat, ai, credentials,-- jobs, tools, projects, geo,++ jobs, tools, projects, geo, history,} = sdk;export default sdk;@@@ -182,6 +182,11 @@"createGitClient"]},++ "history": {++ "object": [++ "createHistoryClient"++ ]++ },"homeFeed": {"object": ["createHomeFeedClient"@@@ -380,6 +385,7 @@"satisfiesRange": "function","search": {"object": [++ "codeOf","createSearchClient","fabricOf","queryOf"@@@ -792,6 +798,7 @@"frameConfig": "function","frameTags": "function","mountFrame": "function",++ "online": "function","scriptUrls": "function"},"openvibe-sdk/front-page": {@@@ -819,6 +826,9 @@},"createGovernor": "function"},++ "openvibe-sdk/history": {++ "createHistoryClient": "function"++ },"openvibe-sdk/home-feed": {"createHomeFeedClient": "function"},@@@ -1075,6 +1085,7 @@"validateRuntimeOffer": "function"},"openvibe-sdk/search": {++ "codeOf": "function","createSearchClient": "function","fabricOf": "function","queryOf": "function"@@@ -2,6 +2,6 @@import cjs from '../src/frame.js';export const {-- mountFrame, frameTags, frameConfig, scriptUrls, DEFAULT_BASE,++ mountFrame, frameTags, frameConfig, scriptUrls, online, DEFAULT_BASE,} = cjs;export default cjs;nnew file mode 100644@@@ -0,0 +1,7 @@++// Generated by scripts/esm.js from src/history.js. Do not edit.++import cjs from '../src/history.js';++++export const {++ createHistoryClient,++} = cjs;++export default cjs;@@@ -15,6 +15,6 @@ export const {deals, shoes, trade, space, host, news,food, actor, run, video, pics, work,zone, git, play, download, openre, geo,-- usage, commerce, moneyReasons, telemetry,++ history, usage, commerce, moneyReasons, telemetry,} = cjs;export default cjs;@@@ -2,6 +2,6 @@import cjs from '../src/search.js';export const {-- createSearchClient, queryOf, fabricOf,++ createSearchClient, queryOf, fabricOf, codeOf,} = cjs;export default cjs;@@@ -69,6 +69,7 @@ module.exports = {download: require('./src/download'),openre: require('./src/openre'),geo: require('./src/geo'),++ history: require('./src/history'),usage: require('./src/usage'),commerce: require('./src/commerce'),moneyReasons: require('./src/money-reasons'),@@@ -1,12 +1,12 @@{"name": "openvibe-sdk",-- "version": "0.145.0",++ "version": "0.146.0","lockfileVersion": 3,"requires": true,"packages": {"": {"name": "openvibe-sdk",-- "version": "0.145.0",++ "version": "0.146.0","license": "MIT","bin": {"openvibe-download": "bin/openvibe-download.js"@@@ -1,6 +1,6 @@{"name": "openvibe-sdk",-- "version": "0.145.0",++ "version": "0.146.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",@@@ -320,6 +320,11 @@"import": "./esm/geo.mjs","default": "./src/geo.js"},++ "./history": {++ "types": "./types/history.d.ts",++ "import": "./esm/history.mjs",++ "default": "./src/history.js"++ },"./openre": {"types": "./types/openre.d.ts","browser": null,@@@ -73,6 +73,7 @@ const ENTRIES = {'tools.mjs': '../src/tools.js','projects.mjs': '../src/projects.js','geo.mjs': '../src/geo.js',++ 'history.mjs': '../src/history.js','openre.mjs': '../src/openre.js','play.mjs': '../src/play.js','download.mjs': '../src/download.js',@@@ -37,6 +37,10 @@ const { isActingSubjectId } = require('./core/ids');const { paginate } = require('./core/paginate');const enc = encodeURIComponent;++const DEFAULT_BOT_URL = 'https://openvibe.bot';++// The three views OpenVibe.Bot's embeddable panel renders (docs/controls-embed.md there).++const EMBED_MODES = ['panel', 'overlay', 'compact'];++const ROBOT_ID = /^rob_[A-Za-z0-9_-]{4,60}$/;const DESK_NAME = /^desk-[a-z0-9][a-z0-9-]{0,23}$/;const randomDesk = () => `desk-${Math.random().toString(36).slice(2, 12).padEnd(10, '0')}`;@@@ -66,6 +70,19 @@ function createBotClient(client, defaults = {}) {const orNull = (p) => p.catch((err) => { if (isOpenVibeError(err) && err.status === 404) return null; throw err; });const once = { idempotencyKey: false }; // a one-time code is its own idempotency: never repeat the call++ /**++ * The address of a robot's embeddable panel, for a page that frames or links it:++ * `https://openvibe.bot/panel/<rob_…>/embed?mode=panel|overlay|compact`. `panel` is the full panel,++ * `overlay` only the live controls (transparent; over a video) and `compact` one row for a narrow card.++ * No request is made; the host page's origin must be in Bot's BOT_EMBED_ORIGINS to frame it.++ */++ function embedUrl(robotId, mode = 'panel') {++ if (typeof robotId !== 'string' || !ROBOT_ID.test(robotId)) throw new TypeError('robotId must be a Bot robot id (rob_…)');++ if (!EMBED_MODES.includes(mode)) throw new TypeError(`mode must be one of ${EMBED_MODES.join(', ')}`);++ const base = String(baseUrl || DEFAULT_BOT_URL).replace(/\/+$/, '');++ return `${base}/panel/${enc(robotId)}/embed?mode=${mode}`;++ }++const robots = {/** { robots } — a person's own robots; a service must name owner (or act as them). */list: (query = {}, o = {}) => call({ path: '/api/v1/robots', query }, o),@@@ -207,6 +224,8 @@ function createBotClient(client, defaults = {}) {},/** The agent redeems a one-time pairing code (the code is the credential; never retried) -> the pair result. */pair: (input = {}, o = {}) => call({ method: 'POST', path: '/api/v1/pair', json: input, ...once }, o),++ /** The embeddable panel's address for a robot and view (no request; see the doc comment above). */++ embedUrl,/** The same client acting for someone else: as('usr_…') or as({ actingSubject }). */as(who) {const next = typeof who === 'string' ? { actingSubject: who } : { ...who };@@@ -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.145.0';++const SDK_VERSION = '0.146.0';module.exports = {createClient,@@@ -147,4 +147,25 @@ async function mountFrame(opts = {}) {return { navbar, footer };}--module.exports = { mountFrame, frameTags, frameConfig, scriptUrls, DEFAULT_BASE };++/**++ * The network's "who is online" read, the data the shared footer strip draws: GET <base>/api/frame/online++ * (openvibe.network). `site` is the calling site's id (its Frame service id), so the answer carries that++ * site's own strip items; omit it and `items` is empty. The answer is returned as it is:++ *++ * { members, guests, total, people: [{ name, username, avatar, site }], more,++ * live: { channels, watching, first: [{ name, url }] }, counts: [{ label, value, icon, url }],++ * items: [{ label, icon, link, value? , line? }], sites, views_7d }++ *++ * A guest is a number only: the read never says who they are, where they are or what made them distinct.++ * Public and CORS-open, cached 30 seconds at the edge. Throws on a non-2xx answer or when fetch is missing.++ */++async function online({ base = DEFAULT_BASE, site = '', signal, fetch: fetchImpl = (typeof fetch === 'function' ? fetch : null) } = {}) {++ const b = baseOf(base);++ if (typeof fetchImpl !== 'function') throw new TypeError('openvibe-sdk/frame: online needs a fetch implementation');++ const q = /^[a-z0-9][a-z0-9-]{0,39}$/.test(String(site || '')) ? `?site=${encodeURIComponent(String(site))}` : '';++ const res = await fetchImpl(`${b}/api/frame/online${q}`, { headers: { accept: 'application/json' }, credentials: 'omit', signal });++ if (!res || !res.ok) throw new Error(`openvibe-sdk/frame: online answered ${res ? res.status : 'nothing'}`);++ return await res.json();++}++++module.exports = { mountFrame, frameTags, frameConfig, scriptUrls, online, DEFAULT_BASE };nnew file mode 100644@@@ -0,0 +1,25 @@++'use strict';++++/** Reader and owner controls for the signed-in person's private cross-site history. */++function createHistoryClient(client, { baseUrl } = {}) {++ if (!client || typeof client.json !== 'function') throw new TypeError('createHistoryClient: pass an openvibe-sdk/core client');++ const call = (opts, o = {}) => client.json({ service: 'network', baseUrl, ...opts, signal: o.signal });++ const enc = (v) => encodeURIComponent(String(v));++ return {++ /** Newest first. query accepts cursor, limit, day, site, kind and q. */++ list: (query = {}, o = {}) => call({ path: '/api/v1/history', query }, o),++ /** Search titles with PostgreSQL full text; query is natural language. */++ find: (q, { limit, ...o } = {}) => call({ path: '/api/v1/history/find', query: { q, limit } }, o),++ /** Last day or week, using titles and kinds only. */++ summary: (period = 'day', o = {}) => call({ path: '/api/v1/history/summary', query: { period } }, o),++ /** Recent rows for the signed-in person's agent; the server enforces the opt-in. */++ agentRecent: (query = {}, o = {}) => call({ path: '/api/v1/history/agents/recent', query }, o),++ settings: (patch, o = {}) => call({ method: 'PUT', path: '/api/v1/history/settings', json: patch }, o),++ remove: (id, o = {}) => call({ method: 'DELETE', path: `/api/v1/history/${enc(id)}` }, o),++ removeDay: (day, o = {}) => call({ method: 'DELETE', path: `/api/v1/history/day/${enc(day)}` }, o),++ removeSite: (site, o = {}) => call({ method: 'DELETE', path: `/api/v1/history/site/${enc(site)}` }, o),++ clear: (o = {}) => call({ method: 'DELETE', path: '/api/v1/history' }, o),++ };++}++++module.exports = { createHistoryClient };@@@ -33,6 +33,17 @@ function createInventoryClient(client, { baseUrl, subject } = {}) {return enc(String(v));};const person = (s) => check(SUBJECT_RE, s, 'a person is a usr_ subject id');++ const bool = (value) => typeof value === 'boolean' ? String(value) : value;++ const holdingsQuery = ({ issuer, site, kind, rarity, equipped, tradable, text, sort, cursor, limit } = {}) =>++ ({ issuer, site, kind, rarity, equipped: bool(equipped), tradable: bool(tradable), text, sort, cursor, limit });++ const holdingPages = async function* (path, options = {}) {++ let cursor = options.cursor;++ do {++ const page = await call({ path, query: holdingsQuery({ ...options, cursor }), signal: options.signal });++ yield page;++ cursor = page.next_cursor;++ } while (cursor);++ };const kinds = {/** Every kind: its schema, slots and the surfaces that render it. */@@@ -50,6 +61,10 @@ function createInventoryClient(client, { baseUrl, subject } = {}) {const people = {/** A person's owned items (public), newest first: { items, next }. */items: (s, { kind, cursor, limit, signal } = {}) => call({ path: `/api/v1/people/${person(s)}/items`, query: { kind, cursor, limit }, signal }),++ /** Stacked holdings; one entry per definition, with count and representative instance. */++ holdings: (s, options = {}) => call({ path: `/api/v1/people/${person(s)}/holdings`, query: holdingsQuery(options), signal: options.signal }),++ holdingsCount: (s, options = {}) => call({ path: `/api/v1/people/${person(s)}/holdings/count`, query: holdingsQuery(options), signal: options.signal }),++ holdingPages: (s, options = {}) => holdingPages(`/api/v1/people/${person(s)}/holdings`, options),/** What a person wears: { subject, slots: { '<kind>:<slot>': { instance_id, definition_id, token?, media_id? } } }. */equipped: (s, { signal } = {}) => call({ path: `/api/v1/people/${person(s)}/equipped`, signal }),};@@@ -63,6 +78,9 @@ function createInventoryClient(client, { baseUrl, subject } = {}) {const me = {/** The caller's own items, every state (owned, consumed, revoked). */items: ({ kind, cursor, limit, signal } = {}) => call({ path: '/api/v1/me/items', query: { kind, cursor, limit }, signal }),++ holdings: (options = {}) => call({ path: '/api/v1/me/holdings', query: holdingsQuery(options), signal: options.signal }),++ holdingsCount: (options = {}) => call({ path: '/api/v1/me/holdings/count', query: holdingsQuery(options), signal: options.signal }),++ holdingPages: (options = {}) => holdingPages('/api/v1/me/holdings', options),/** Wear an item in a slot, or clear the slot with instance_id: null (inventory.equip-request@1). */equip: ({ kind, slot, instance_id: instanceId }, { signal } = {}) => {if (instanceId !== null) check(ID_RE.instance, instanceId, 'instance_id is an inv_ id, or null to clear the slot');@@@ -50,6 +50,8 @@ function createQuestClient(client, { baseUrl } = {}) {appQuests, eventType, projectKey,/** The network-wide quest for today, or a past YYYY-MM-DD date (quest.daily@1). */daily: ({ date, signal } = {}) => call({ path: date ? `/api/v1/daily/${enc(date)}` : '/api/v1/daily', signal }),++ /** This signed-in person's guided quest for today (requires a person token). */++ today: ({ signal } = {}) => call({ path: '/api/v1/quests/today', signal }),/** The person token's own checklist, completion and streak (quest.daily-progress@1). */myDaily: ({ signal } = {}) => call({ path: '/api/v1/me/daily', signal }),/** Approved event-backed step types (quest.step-type@1). */@@@ -40,6 +40,23 @@ function fabricOf(text, o = {}) {if (o.answer) q.answer = 'true';return q;}++const CODE_KEYS = ['lang', 'repo', 'licence', 'limit', 'cursor'];++const CODE_BODY_KEYS = ['lang', 'repo', 'licence'];++++/** The query string of GET /api/v1/search/code. */++function codeOf(text, o = {}) {++ const q = { q: String(text == null ? '' : text) };++ for (const k of CODE_KEYS) if (o[k] !== undefined && o[k] !== null && o[k] !== '') q[k] = o[k];++ return q;++}++++/** The body of POST /api/v1/search/code/deep; q is required, the filters are optional. */++function codeBody(text, o = {}) {++ const body = { q: String(text == null ? '' : text) };++ for (const k of CODE_BODY_KEYS) if (o[k] !== undefined && o[k] !== null && o[k] !== '') body[k] = o[k];++ return body;++}++const FACET_KEY = /^[a-z][a-z0-9_]{0,39}$/;function queryOf(text, o = {}) {@@@ -104,7 +121,23 @@ function createSearchClient(client, defaults = {}) {suggest: (text, o = {}) => call({ path: pathWith('/api/v1/suggest', queryOf(text, { owner: o.owner, type: o.type, limit: o.limit })) }, o),/** One document by exact id, or null (a document you may not see answers the same as a missing one). */document: (owner, type, id, o = {}) => orNull(call({ path: `/api/v1/documents/${enc(owner)}/${enc(type)}/${enc(id)}` }, o)),++ /** { results, sources, next_cursor } — merged public code; `deep: true` asks the cited answer (signed in). */++ code: (text, o = {}) => (o.deep++ ? call({ method: 'POST', path: '/api/v1/search/code/deep', json: codeBody(text, o), idempotencyKey: false }, o)++ : call({ path: pathWith('/api/v1/search/code', codeOf(text, o)) }, o)),++ /** Every merged code result, page after page (cursor). Deep mode is one answer, not a page stream. */++ async *codeIterate(text, o = {}) {++ if (o.deep) throw new TypeError('codeIterate is for pages; use code(text, { deep: true }) for an answer');++ let cursor = o.cursor || null;++ let seen = 0;++ const max = Number.isFinite(o.max) ? o.max : Infinity;++ do {++ const page = await call({ path: pathWith('/api/v1/search/code', codeOf(text, { ...o, cursor })) }, o);++ for (const r of page.results || []) { if (seen++ >= max) return; yield r; }++ cursor = page.next_cursor || null;++ } while (cursor);++ },};}--module.exports = { createSearchClient, queryOf, fabricOf };++module.exports = { createSearchClient, queryOf, fabricOf, codeOf };@@@ -242,6 +242,18 @@ run([await srv.close();}],++ ['embedUrl: the panel address for a robot and view, from the client\'s Bot origin', async () => {++ const bot = createBotClient(createClient({ baseUrls: { bot: 'https://openvibe.bot' } }));++ assert.equal(bot.embedUrl(ROB), `https://openvibe.bot/panel/${ROB}/embed?mode=panel`);++ assert.equal(bot.embedUrl(ROB, 'overlay'), `https://openvibe.bot/panel/${ROB}/embed?mode=overlay`);++ assert.equal(bot.embedUrl(ROB, 'compact'), `https://openvibe.bot/panel/${ROB}/embed?mode=compact`);++ const custom = createBotClient(createClient({ baseUrls: { bot: 'https://bot.example' } }), { baseUrl: 'https://bot.example/' });++ assert.equal(custom.embedUrl(ROB, 'overlay'), `https://bot.example/panel/${ROB}/embed?mode=overlay`, 'a trailing slash is trimmed');++ assert.equal(custom.as(USR).embedUrl(ROB), `https://bot.example/panel/${ROB}/embed?mode=panel`, 'as() keeps the origin');++ assert.throws(() => bot.embedUrl('not-a-robot'), TypeError);++ assert.throws(() => bot.embedUrl(ROB, 'huge'), TypeError);++ }],++['browser mode: cookie credentials, no Authorization and no X-OV headers', async () => {const srv = await server();const seen = [];@@@ -2,7 +2,7 @@// openvibe-sdk/frame: any app mounts the network's shared navbar, footer, shipped line and themes,// with its own session endpoints; server-rendered apps get the same markup as strings.const assert = require('node:assert/strict');--const { mountFrame, frameTags, frameConfig, scriptUrls } = require('../src/frame');++const { mountFrame, frameTags, frameConfig, scriptUrls, online } = require('../src/frame');const opts = {service: 'myapp',@@@ -66,6 +66,9 @@ function fakeDom() {}(async () => {++ const frameEsm = await import('../esm/frame.mjs');++ assert.equal(typeof frameEsm.online, 'function', 'the frame ESM subpath exports the online reader');++const d = fakeDom();const r = await mountFrame({ ...opts, document: d.doc, window: d.win });assert.ok(r.navbar && r.footer);@@@ -80,5 +83,24 @@ function fakeDom() {const r2 = await mountFrame({ document: d2.doc, window: d2.win });assert.deepEqual(r2, { navbar: null, footer: null });await assert.rejects(() => mountFrame({ document: null, window: null }), /browser/);++++ // The online read: the footer strip's data, asked for this site so its own items come back.++ {++ const calls = [];++ const answer = { members: 3, guests: 4, total: 7, people: [], more: 0, live: null, counts: [], items: [], sites: 56, views_7d: 1200 };++ const fakeFetch = async (url, init) => { calls.push({ url, init }); return { ok: true, status: 200, json: async () => answer }; };++ assert.deepEqual(await online({ site: 'live', fetch: fakeFetch }), answer);++ assert.equal(calls[0].url, 'https://openvibe.network/api/frame/online?site=live');++ assert.equal(calls[0].init.credentials, 'omit');++ assert.equal(calls[0].init.headers.accept, 'application/json');++ await online({ site: 'not a site!', fetch: fakeFetch });++ assert.equal(calls[1].url, 'https://openvibe.network/api/frame/online', 'an invalid id asks as no site, never in the query');++ await online({ base: 'https://openvibe.network/', site: 'games', fetch: fakeFetch });++ assert.equal(calls[2].url, 'https://openvibe.network/api/frame/online?site=games');++ await assert.rejects(() => online({ fetch: async () => ({ ok: false, status: 503 }) }), /answered 503/);++ await assert.rejects(() => online({ fetch: null }), /needs a fetch/, 'no fetch: a clear error');++ await assert.rejects(() => online({ base: 'http://evil.example', fetch: fakeFetch }), /https origin/);++ }++console.log('frame: all checks passed');})().catch((e) => { console.error(e); process.exit(1); });nnew file mode 100644@@@ -0,0 +1,21 @@++'use strict';++const assert = require('assert');++const { createHistoryClient } = require('../src/history');++++(async () => {++ const esm = await import('../esm/history.mjs');++ assert.strictEqual(esm.createHistoryClient, createHistoryClient, 'the ESM subpath matches CommonJS');++ const calls = [];++ const history = createHistoryClient({ json: async (args) => { calls.push(args); return args; } }, { baseUrl: 'https://network.test' });++ await history.list({ limit: 12, kind: 'video', cursor: 'next' });++ assert.deepStrictEqual(calls.pop(), { service: 'network', baseUrl: 'https://network.test', path: '/api/v1/history', query: { limit: 12, kind: 'video', cursor: 'next' }, signal: undefined });++ await history.find('a fox video');++ assert.deepStrictEqual(calls.pop().query, { q: 'a fox video', limit: undefined });++ await history.agentRecent({ limit: 7 });++ assert.strictEqual(calls.pop().path, '/api/v1/history/agents/recent');++ await history.removeDay('2026-11-10');++ assert.strictEqual(calls.pop().path, '/api/v1/history/day/2026-11-10');++ await history.settings({ agents_allowed: true });++ assert.deepStrictEqual(calls.pop().json, { agents_allowed: true });++ console.log('history SDK client: all checks passed');++})().catch((err) => { console.error(err); process.exit(1); });@@@ -22,6 +22,8 @@ const { createInventoryClient } = require('../src/inventory');return json(201, { instance: { id: 'inv_01JAB2C3D4E5F6G7H8J9K0MNPQ' }, created: true });}if (req.url === '/api/v1/definitions' && req.method === 'POST') return json(409, { type: 'about:blank', title: 'exhausted', status: 409, code: 'inventory.supply_exhausted' });++ if (req.url.includes('/holdings/count')) return json(200, { subject: 'test', count: 2 });++ if (req.url.includes('/holdings')) return json(200, { subject: 'test', holdings: [{ count: 1 }], next_cursor: req.url.includes('cursor=next') ? null : 'next' });return json(200, { ok: true });});});@@@ -57,6 +59,15 @@ const { createInventoryClient } = require('../src/inventory');assert.deepStrictEqual(seen[12].body, { reason: 'refund' });assert.ok(seen.every((s) => s.subject === null), 'a person\'s own client names nobody');++ seen.length = 0;++ assert.strictEqual((await inv.me.holdingsCount({ rarity: 'rare' })).count, 2);++ assert.strictEqual((await inv.people.holdings(U1, { sort: 'name', limit: 60, equipped: false, tradable: true })).holdings.length, 1);++ const pages = [];++ for await (const page of inv.me.holdingPages({ issuer: 'service:live', limit: 1 })) pages.push(page);++ assert.strictEqual(pages.length, 2);++ assert(seen.some((s) => s.url.includes('/me/holdings?') && s.url.includes('cursor=next')));++ assert(seen.some((s) => s.url.includes(`/people/${U1}/holdings?equipped=false&tradable=true&sort=name&limit=60`)));++// Grants: the body's idempotency_key makes a retry safe, so a 503 is retried and the item is granted once.seen.length = 0;const out = await inv.grants.create({ definition_id: DEF, subject: U1, idempotency_key: 'win-match-42' });@@@ -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, 101); // levels adds the 101st subpath.++ assert.equal(subpaths.length, 102); // history adds the 102nd subpath after levels.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 });@@@ -89,7 +89,9 @@ function cases(label, open) {outbox.kick();await new Promise((r) => setTimeout(r, 80));});-- await new Promise((r) => setTimeout(r, 120));++ // PGlite's claim can take longer than a fixed sleep on loaded CI hosts.++ const deadline = Date.now() + 2000;++ while ((inTx.length < 3 || platform.state.events.length < 1) && Date.now() < deadline) await new Promise((r) => setTimeout(r, 20));await outbox.stop();assert.equal(platform.state.events.length, 1, 'the event went out');assert.ok(inTx.length >= 3, `the relay kept ticking (${inTx.length} claims)`);@@@ -8,14 +8,15 @@ const client = createQuestClient({ json: async (opts) => { calls.push(opts); retawait client.daily();await client.daily({ date: '2026-11-08' });await client.myDaily();++ await client.today();await client.stepTypes();await client.contributeStepType({ id: 'chat', event_type: 'chat.room.message.created', site: 'openvibe.chat', sentence: 'Chat', href: 'https://openvibe.chat/' });assert.deepEqual(calls.map((x) => [x.method || 'GET', x.path]), [-- ['GET', '/api/v1/daily'], ['GET', '/api/v1/daily/2026-11-08'], ['GET', '/api/v1/me/daily'],++ ['GET', '/api/v1/daily'], ['GET', '/api/v1/daily/2026-11-08'], ['GET', '/api/v1/me/daily'], ['GET', '/api/v1/quests/today'],['GET', '/api/v1/step-types'], ['POST', '/api/v1/step-types'],]);-- assert.equal(calls[4].audience, 'openvibe.quest');-- assert.equal(calls[4].json.site, 'openvibe.chat');-- assert.equal(calls[4].idempotencyKey, false);++ assert.equal(calls[5].audience, 'openvibe.quest');++ assert.equal(calls[5].json.site, 'openvibe.chat');++ assert.equal(calls[5].idempotencyKey, false);process.stdout.write('quest daily SDK methods passed\n');})().catch((err) => { process.stderr.write(String(err.stack || err) + '\n'); process.exit(1); });@@@ -3,7 +3,7 @@const assert = require('node:assert/strict');const { stubServer, send, run } = require('./helpers');const { createClient } = require('../src/core');--const { createSearchClient, queryOf, fabricOf } = require('../src/search');++const { createSearchClient, queryOf, fabricOf, codeOf } = require('../src/search');const USR = 'usr_01JAB2C3D4E5F6G7H8J9K0MNPQ';@@@ -15,7 +15,14 @@ async function search() {if (!cursor) return send(res, 200, { results: [{ owner: 'live', type: 'vod', id: '1' }, { owner: 'live', type: 'vod', id: '2' }], next_cursor: 'c2' });return send(res, 200, { results: [{ owner: 'live', type: 'vod', id: '3' }], next_cursor: null });}-- if (u.pathname === '/api/v1/query') return send(res, 200, { results: [{ scope: 'web', kind: 'page', title: 'A', url: 'https://example.org/a', provider: 'wikipedia' }], groups: { web: 1 }, answer: null, citations: [], route: { explain: [{ scope: 'web', step: 'wikipedia', ok: true, count: 1 }], cost_usd: 0 }, next_cursor: null });++ if (u.pathname === '/api/v1/search/code') {++ const cursor = u.searchParams.get('cursor');++ const hit = { source: 'github', repo: 'OpenVibers/OpenVibe.Search', path: 'server/api/query.js', ref: 'abc123', line_start: 5, line_end: 5, lines: [{ number: 5, text: 'function createSearcher() {', match: true }], language: 'JavaScript', stars: 12, licence: 'AGPL-3.0', last_commit_at: '2026-11-09T10:00:00Z', permalink: 'https://github.com/OpenVibers/OpenVibe.Search/blob/abc123/server/api/query.js#L5' };++ if (!cursor) return send(res, 200, { query: { q: u.searchParams.get('q') }, results: [hit], sources: [{ id: 'github', kind: 'code', label: 'GitHub code search', enabled: true, health: 'ok', order: 10, calls: 1, failures: 0 }], next_cursor: 'c2' });++ return send(res, 200, { query: { q: u.searchParams.get('q') }, results: [{ ...hit, path: 'second.js' }], sources: [], next_cursor: null });++ }++ if (u.pathname === '/api/v1/search/code/deep') return send(res, 200, { query: { q: 'x' }, queries: ['createSearcher'], answer: 'It is here [1].', citations: [{ n: 1, repo: 'OpenVibers/OpenVibe.Search', path: 'server/api/query.js', line_start: 5, line_end: 5, permalink: 'https://github.com/OpenVibers/OpenVibe.Search/blob/abc123/server/api/query.js#L5' }], results: [], route: { budget: { max_model_calls: 2 } } });++ if (u.pathname === '/api/v1/query') return send(res, 200, { results: [{ scope: 'web', kind: 'page', title: 'A', url: 'https://example.org/a', provider: 'wikipedia' }], groups: { web: 1 }, answer: null, citations: [], route: { explain: [{ scope: 'web', step: 'wikipedia', ok: true, count: 1 }], cost_usd: 0 }, next_cursor: null });if (u.pathname === '/api/v1/query/providers') return send(res, 200, { providers: [{ provider: 'deepseek', scopes: ['web'], price_usd_per_call: 0.00075, available: true, checked_at: '2026-10-28T12:00:00Z' }] });if (u.pathname === '/api/v1/suggest') return send(res, 200, { suggestions: [{ owner: 'live', type: 'channel', id: '327', title: 'JapaneseOldGuy' }] });if (u.pathname.startsWith('/api/v1/documents/live/channel/404')) return send(res, 404, { type: 'about:blank', code: 'search.not_found', status: 404 });@@@ -25,6 +32,10 @@ async function search() {}run([++ ['ESM subpath exports code search and code iteration', async () => {++ const esm = await import('../esm/search.mjs');++ assert.equal(typeof esm.createSearchClient, 'function');++ }],['fabric: scopes go to GET /api/v1/query; query() with scopes is the same call; providers', async () => {const srv = await search();const s = createSearchClient(createClient({ baseUrls: { search: srv.url } }));@@@ -85,4 +96,29 @@ run([assert.throws(() => createSearchClient(createClient({ baseUrls: { search: srv.url } })).query('x', { actingSubject: 'nope' }), /actingSubject/);await srv.close();}],++ ['code: a filter query, pages via codeIterate, and the cited deep answer', async () => {++ const srv = await search();++ const s = createSearchClient(createClient({ baseUrls: { search: srv.url } }));++ const page = await s.code('createSearcher', { repo: 'OpenVibers/OpenVibe.Search', licence: 'AGPL-3.0', limit: 1 });++ const u = new URL(srv.requests[0].url, 'http://x');++ assert.equal(u.pathname, '/api/v1/search/code');++ assert.equal(u.searchParams.get('q'), 'createSearcher');++ assert.equal(u.searchParams.get('repo'), 'OpenVibers/OpenVibe.Search');++ assert.equal(u.searchParams.get('licence'), 'AGPL-3.0');++ assert.equal(u.searchParams.get('limit'), '1');++ assert.equal(page.results[0].permalink, 'https://github.com/OpenVibers/OpenVibe.Search/blob/abc123/server/api/query.js#L5');++ const paths = [];++ for await (const r of s.codeIterate('createSearcher')) paths.push(r.path);++ assert.deepEqual(paths, ['server/api/query.js', 'second.js']);++ assert.equal(new URL(srv.requests[2].url, 'http://x').searchParams.get('cursor'), 'c2');++ const deep = await s.code('Where?', { deep: true, repo: 'OpenVibers/OpenVibe.Search' });++ assert.equal(deep.answer, 'It is here [1].');++ const post = srv.requests[3];++ assert.equal(post.method, 'POST');++ assert.equal(JSON.parse(post.body.toString()).q, 'Where?');++ assert.equal(JSON.parse(post.body.toString()).repo, 'OpenVibers/OpenVibe.Search');++ assert.equal(post.headers['idempotency-key'], undefined, 'a paid run is not retried under an idempotency key');++ assert.deepEqual(codeOf('x', { lang: 'js', limit: 2 }), { q: 'x', lang: 'js', limit: 2 });++ await srv.close();++ }],]);@@@ -116,12 +116,15 @@ export interface MachinesApi {chat(id: string, input: MachineChatInput, opts?: CallOptions & { ms?: number }): Promise<MachineChatResult>;aiModels(id: string, on: boolean, opts?: CallOptions): Promise<{ machine: Machine }>;}++export type BotEmbedMode = 'panel' | 'overlay' | 'compact';export interface BotClient {robots: RobotsApi;machines: MachinesApi;devices: DevicesApi;profiles: ProfilesApi;kits: KitsApi;++ /** The embeddable panel's address for a robot and view (no request): https://openvibe.bot/panel/<rob_…>/embed?mode=… */++ embedUrl(robotId: string, mode?: BotEmbedMode): string;pair(input: PairInput, opts?: CallOptions): Promise<{ device_id: string; credential?: string; robot_id?: string; profile?: Record<string, unknown> | null; [field: string]: unknown }>;as(who: string | ActingOptions): BotClient;headers(): Record<string, string>;@@@ -24,6 +24,7 @@ import * as jobs from './jobs';import * as tools from './tools';import * as projects from './projects';import * as geo from './geo';++import * as history from './history';import * as locker from './locker';import * as trust from './trust';import * as levels from './levels';@@@ -56,6 +57,7 @@ export * as jobs from './jobs';export * as tools from './tools';export * as projects from './projects';export * as geo from './geo';++export * as history from './history';export * as locker from './locker';export * as trust from './trust';export * as levels from './levels';@@@ -63,6 +65,6 @@ export * as levels from './levels';declare const sdk: typeof core & {createWalletClient: typeof createWalletClient;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; locker: typeof locker; trust: typeof trust; levels: typeof levels;++ 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;};export default sdk;@@@ -39,8 +39,29 @@ export interface FrameOptions {export interface FrameConfig { navbar: Record<string, unknown>; footer: Record<string, unknown> | null }export interface FrameUrls { theme: string; navbar: string; footer: string; shipped: string }++/** One person the network may name: only when their presence setting lets others see them online. */++export interface FrameOnlinePerson { name: string; username: string; avatar: string | null; site: string | null }++/** A count that opens what it counts, or a site's own strip item (a number or a fixed line). */++export interface FrameOnlineItem { label: string; icon: string; link: string; value?: number; line?: string }++/** GET <base>/api/frame/online — who was on the network in the last thirty minutes. A guest is a number only. */++export interface FrameOnline {++ updated: string | null;++ members: number | null;++ guests: number | null;++ total: number | null;++ people: FrameOnlinePerson[];++ more: number;++ live: { channels: number; watching: number; first: Array<{ name: string; url: string }> } | null;++ counts: Array<{ id?: string; label: string; value: number; icon: string; url: string }>;++ items: FrameOnlineItem[];++ sites: number | null;++ views_7d: number | null;++}++export const DEFAULT_BASE: string;export function scriptUrls(opts?: { base?: string }): FrameUrls;export function frameConfig(opts?: FrameOptions): FrameConfig;export function frameTags(opts?: FrameOptions): { head: string; bodyStart: string; bodyEnd: string; config: FrameConfig; urls: FrameUrls };export function mountFrame(opts?: FrameOptions): Promise<{ navbar: unknown | null; footer: unknown | null }>;++/** The footer strip's read; `site` (the calling site's Frame id) brings that site's own items. */++export function online(opts?: { base?: string; site?: string; signal?: AbortSignal; fetch?: typeof fetch }): Promise<FrameOnline>;nnew file mode 100644@@@ -0,0 +1,16 @@++export interface HistoryRow {++ id: number; type: string; title: string; url: string; image_url?: string | null;++ service: string | null; service_label: string | null; created_at: string; updated_at: string;++}++export interface HistoryList { items: HistoryRow[]; total: number; next_cursor: string | null; keep_history: boolean; agents_allowed: boolean; }++export declare function createHistoryClient(client: unknown, options?: { baseUrl?: string }): {++ list(query?: { cursor?: string; limit?: number; day?: string; site?: string; kind?: string; q?: string }, options?: { signal?: AbortSignal }): Promise<HistoryList>;++ find(q: string, options?: { limit?: number; signal?: AbortSignal }): Promise<{ query: string; items: HistoryRow[] }>;++ summary(period?: 'day' | 'week', options?: { signal?: AbortSignal }): Promise<{ period: 'day' | 'week'; items: Array<{ kind: string; title: string }>; text: string }>;++ agentRecent(query?: { limit?: number }, options?: { signal?: AbortSignal }): Promise<{ items: HistoryRow[] }>;++ settings(patch: { keep_history?: boolean; agents_allowed?: boolean }, options?: { signal?: AbortSignal }): Promise<{ keep_history: boolean; agents_allowed: boolean }>;++ remove(id: number | string, options?: { signal?: AbortSignal }): Promise<{ ok: boolean }>;++ removeDay(day: string, options?: { signal?: AbortSignal }): Promise<{ deleted: number }>;++ removeSite(site: string, options?: { signal?: AbortSignal }): Promise<{ deleted: number }>;++ clear(options?: { signal?: AbortSignal }): Promise<{ deleted: number }>;++};@@@ -58,6 +58,7 @@ export * as play from './play';export * as download from './download';export * as openre from './openre';export * as geo from './geo';++export * as history from './history';export * as usage from './usage';export * as commerce from './commerce';export * as moneyReasons from './money-reasons';@@@ -9,6 +9,9 @@ export interface Definition {}export interface DefinitionRequest { kind: string; name: string; description?: string; rarity?: Rarity; art?: Definition['art']; attributes?: Record<string, unknown>; supply_cap?: number; [k: string]: unknown }export interface Instance { id: string; definition_id: string; owner_subject?: string; state?: 'owned' | 'consumed' | 'revoked'; serial?: number | null; [k: string]: unknown }++export interface Holding { definition: Definition; instance: Instance; count: number; equipped: boolean; tradable: boolean; kind_slots: string[] }++export interface HoldingsPage { subject: string; holdings: Holding[]; next_cursor: string | null }++export type HoldingsOptions = Opts & { issuer?: string; site?: string; kind?: string; rarity?: Rarity; equipped?: boolean; tradable?: boolean; text?: string; sort?: 'newest' | 'name' | 'rarity' | 'count'; cursor?: string; limit?: number };export interface EquippedSet { subject: string; slots: Record<string, { instance_id: string; definition_id: string; token?: string; media_id?: string }>; updated_at: string | null }export interface InventoryClient {kinds: { list(opts?: Opts): Promise<{ kinds: Kind[] }>; get(kindId: string, opts?: Opts): Promise<{ kind: Kind }> };@@@ -19,12 +22,18 @@ export interface InventoryClient {update(id: string, patch: Partial<DefinitionRequest>, opts?: Opts): Promise<{ definition: Definition }>;};people: {-- items(subject: string, opts?: Opts & { kind?: string; cursor?: string; limit?: number }): Promise<{ items: Instance[]; next: string | null; [k: string]: unknown }>;++ items(subject: string, opts?: Opts & { kind?: string; cursor?: string; limit?: number }): Promise<{ subject: string; instances: Instance[]; definitions: Record<string, Definition>; next_cursor: string | null }>;++ holdings(subject: string, opts?: HoldingsOptions): Promise<HoldingsPage>;++ holdingsCount(subject: string, opts?: HoldingsOptions): Promise<{ subject: string; count: number }>;++ holdingPages(subject: string, opts?: HoldingsOptions): AsyncIterable<HoldingsPage>;equipped(subject: string, opts?: Opts): Promise<EquippedSet>;};equipped(subjects: string[], opts?: Opts): Promise<{ equipped: EquippedSet[] }>;me: {-- items(opts?: Opts & { kind?: string; cursor?: string; limit?: number }): Promise<{ items: Instance[]; next: string | null; [k: string]: unknown }>;++ items(opts?: Opts & { kind?: string; cursor?: string; limit?: number }): Promise<{ subject: string; instances: Instance[]; definitions: Record<string, Definition>; next_cursor: string | null }>;++ holdings(opts?: HoldingsOptions): Promise<HoldingsPage>;++ holdingsCount(opts?: HoldingsOptions): Promise<{ subject: string; count: number }>;++ holdingPages(opts?: HoldingsOptions): AsyncIterable<HoldingsPage>;equip(req: { kind: string; slot: string; instance_id: string | null }, opts?: Opts): Promise<EquippedSet>;};grants: { create(req: { definition_id: string; subject: string; idempotency_key: string; [k: string]: unknown }, opts?: Opts): Promise<{ instance: Instance; created: boolean }> };@@@ -16,6 +16,7 @@ export interface AppQuest {}export interface QuestClient {daily(opts?: { date?: string; signal?: AbortSignal }): Promise<DailyQuest>;++ today(opts?: { signal?: AbortSignal }): Promise<DailyProgress>;myDaily(opts?: { signal?: AbortSignal }): Promise<DailyProgress>;stepTypes(opts?: { signal?: AbortSignal }): Promise<{ step_types: QuestStepType[] }>;contributeStepType(stepType: DailyStepTypeRequest, opts?: { signal?: AbortSignal }): Promise<QuestStepType>;@@@ -58,6 +58,34 @@ export interface ProviderOffer {available: boolean; reason?: string | null; daily_cap?: number | null; used_today?: number | null;latency_ms_p50?: number | null; answered_rate?: number | null; terms_url?: string | null; checked_at: string;}++export interface CodeLine { number: number | null; text: string; match: boolean; }++export interface CodeResult {++ source: string; sources?: string[]; repo: string; repo_url?: string | null; path: string; ref?: string | null;++ line_start: number | null; line_end: number | null; lines: CodeLine[];++ language?: string | null; stars: number; licence?: string | null;++ last_commit_at?: string | null; last_commit_source?: string | null; fetched_at?: string | null;++ permalink: string; exact_identifier?: boolean;++}++export interface CodeSource {++ id: string; kind: 'code'; label: string; cost: string;++ key?: { name: string; required: boolean; configured: boolean };++ quota?: { per_minute: number | null; window: string };++ enabled: boolean; health: string; order: number; calls: number; failures: number;++ last_ok_at?: string | null; last_error_at?: string | null; last_error?: string | null; last_latency_ms?: number | null;++}++export interface CodePage { query: Record<string, unknown>; results: CodeResult[]; sources: CodeSource[]; next_cursor: string | null; took_ms?: number; }++export interface CodeOptions {++ lang?: string; repo?: string; licence?: string; limit?: number; cursor?: string | null;++ actingSubject?: string; signal?: AbortSignal;++}++export interface CodeCitation {++ n: number; repo: string; path: string; line_start: number | null; line_end: number | null;++ permalink: string; language?: string | null; stars?: number; licence?: string | null;++}++export interface CodeDeepAnswer {++ query: Record<string, unknown>; queries: string[]; answer: string;++ citations: CodeCitation[]; results: CodeResult[]; route: Record<string, unknown>;++}export interface SearchClient {query(text: string, opts?: SearchOptions): Promise<SearchPage>;/** With scopes: the Search Fabric. */@@@ -67,7 +95,13 @@ export interface SearchClient {iterate(text: string, opts?: SearchOptions & { max?: number }): AsyncGenerator<SearchDocument, void, unknown>;suggest(text: string, opts?: Pick<SearchOptions, 'owner' | 'type' | 'limit' | 'actingSubject' | 'signal'>): Promise<{ suggestions: Array<{ owner: string; type: string; id: string; title: string; canonical_url?: string }> }>;document(owner: string, type: string, id: string, opts?: Pick<SearchOptions, 'actingSubject' | 'signal'>): Promise<SearchDocument | null>;++ /** Merged, de-duplicated public code from the hosted sources; one page per call. */++ code(text: string, opts: CodeOptions & { deep: true }): Promise<CodeDeepAnswer>;++ code(text: string, opts?: CodeOptions): Promise<CodePage>;++ /** Every code result, page after page. Deep mode is not a page stream. */++ codeIterate(text: string, opts?: CodeOptions & { max?: number }): AsyncGenerator<CodeResult, void, unknown>;}export function createSearchClient(client: OpenVibeClient, defaults?: { baseUrl?: string; actingSubject?: string }): SearchClient;export function queryOf(text: string, opts?: SearchOptions): Record<string, string | string[] | number>;export function fabricOf(text: string, opts?: FabricOptions): Record<string, string | number>;++export function codeOf(text: string, opts?: CodeOptions): Record<string, string | number>;
@@ -1,5 +1,14 @@
@@ -1,5 +1,14 @@
# Changelog
# Changelog
+## 0.146.0 — 2026-11-10
+
+- #183: add the private history reader for cursor reads, search, summaries, agent recent reads, settings and owner-scoped deletes.
+- #184: add `frame.online()` with member presence, anonymous guest counts and live channel/site items.
+- #186: add `quest.today()` for the authenticated daily Quest response.
+- #195: add `bot.embedUrl(robotId, mode)` for panel, overlay and compact embeds.
+- #196: add merged code search, cited deep answers and cursor iteration to Search.
+- #197: add filtered, paged Inventory holdings reads and counts, plus async holding pages.
+
## 0.145.0 — 2026-11-10
## 0.145.0 — 2026-11-10
- `bench.ratings.get`: shared fitted ratings and confidence intervals with a twenty-game floor. Browser-safe, anonymous and typed; pairs with `ai.metrics` across sites.
- `bench.ratings.get`: shared fitted ratings and confidence intervals with a twenty-game floor. Browser-safe, anonymous and typed; pairs with `ai.metrics` across sites.
@@ -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.108.0
+npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.146.0
```
```
> Supported browser and server clients for the OpenVibe platform.
> Supported browser and server clients for the OpenVibe platform.
-**Status:** alpha, v0.145.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.146.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.145.0
+npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.146.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,13 +203,14 @@ Until OpenVibe.Tools serves the gateway facade (Tools S6), point the jobs client
@@ -203,13 +203,14 @@ Until OpenVibe.Tools serves the gateway facade (Tools S6), point the jobs client
| `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/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/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/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`), `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`. |
| `openvibe-sdk/vision` | both | `createVisionClient(client)`: image questions, OCR, private search, network tools, UI checks, consent and face privacy, and `/mcp`. Audience `openvibe.vision`; service id `vision`. |
| `openvibe-sdk/vision` | both | `createVisionClient(client)`: image questions, OCR, private search, network tools, UI checks, consent and face privacy, and `/mcp`. Audience `openvibe.vision`; service id `vision`. |
| `openvibe-sdk/consent` | server | `createConsent({networkUrl, token, ttlMs?})`: the L24 check before any use of a person's content or likeness for AI or data. `allowed(subject, purpose, {kind, client, feature})` → boolean, `filter(subjects, purpose, opts)` → the allowed subset (batches of 500, input order), `onChange(event)` drops that subject's answers for the purpose on `network.consent.changed`, `mock(map)` for tests. **Fails closed** (network error, 2 s timeout, non-200 or malformed answer → false); caches 60 s; logs at most once a minute. [Example](docs/consent.md) |
| `openvibe-sdk/consent` | server | `createConsent({networkUrl, token, ttlMs?})`: the L24 check before any use of a person's content or likeness for AI or data. `allowed(subject, purpose, {kind, client, feature})` → boolean, `filter(subjects, purpose, opts)` → the allowed subset (batches of 500, input order), `onChange(event)` drops that subject's answers for the purpose on `network.consent.changed`, `mock(map)` for tests. **Fails closed** (network error, 2 s timeout, non-200 or malformed answer → false); caches 60 s; logs at most once a minute. [Example](docs/consent.md) |
-| `openvibe-sdk/search` | both | `createSearchClient(client, {actingSubject?})`: `query(text, {owner, type, lang, filter, facets, limit, cursor})`, `iterate(text, {…, max})`, `suggest(text, {owner, type, limit})`, `document(owner, type, id)` (null when missing or not visible) |
+| `openvibe-sdk/search` | both | `createSearchClient(client, {actingSubject?})`: `query(text, {owner, type, lang, filter, facets, limit, cursor})`, `iterate(text, {…, max})`, `suggest(text, {owner, type, limit})`, `document(owner, type, id)` (null when missing or not visible), `code(text, opts)` for merged code results or `{deep: true}` for a cited answer, `codeIterate(text, opts)` |
| `openvibe-sdk/limits` | node | `createActorLimiter({limits: {minute, hour, day}, actor?, maxActors?, onLimited?})` → `limits(name, {minute?, hour?, day?})` Express middleware: per-actor fixed windows at a capability boundary (service principal, else the signed-in person, else `ip:`), 429 problem+json `rate_limited` with Retry-After before the handler; `defaultActor(req)`, `.stats()` |
| `openvibe-sdk/limits` | node | `createActorLimiter({limits: {minute, hour, day}, actor?, maxActors?, onLimited?})` → `limits(name, {minute?, hour?, day?})` Express middleware: per-actor fixed windows at a capability boundary (service principal, else the signed-in person, else `ip:`), 429 problem+json `rate_limited` with Retry-After before the handler; `defaultActor(req)`, `.stats()` |
| `openvibe-sdk/account-data` | node | `createAccountData({db, service, tables: [{table, subject, value?, file?, columns?, erase?}], extraExport?, extraErase?})` → `.apply(event, {send})`, `.consumer({secrets, send, onEvent?})` (a POST /internal/events handler), `.exportPart(subject)`, `.erase(subjects)`, `.ensureSchema()`; `createNetworkSender({networkInternalUrl, clientId, clientSecret})`; `startSubscriptions({eventsUrl, endpoint, secret, networkInternalUrl, clientId, clientSecret})` (the two Events subscriptions, created at boot when missing): a service's share of account export and deletion (ADR-033), idempotent per export/deletion id; telemetry is covered once here (the person's `telemetry_samples` exported as `telemetry.json`, its `subject` set to NULL and the sample kept) unless the table is listed; `uncovered(db, {tables, taskKeyed?, exempt?})` lists the tables with a person column (`subject`, `owner`, `created_by`, `user_id`, `person`) that nothing covers, for a service test to assert is empty |
| `openvibe-sdk/account-data` | node | `createAccountData({db, service, tables: [{table, subject, value?, file?, columns?, erase?}], extraExport?, extraErase?})` → `.apply(event, {send})`, `.consumer({secrets, send, onEvent?})` (a POST /internal/events handler), `.exportPart(subject)`, `.erase(subjects)`, `.ensureSchema()`; `createNetworkSender({networkInternalUrl, clientId, clientSecret})`; `startSubscriptions({eventsUrl, endpoint, secret, networkInternalUrl, clientId, clientSecret})` (the two Events subscriptions, created at boot when missing): a service's share of account export and deletion (ADR-033), idempotent per export/deletion id; telemetry is covered once here (the person's `telemetry_samples` exported as `telemetry.json`, its `subject` set to NULL and the sample kept) unless the table is listed; `uncovered(db, {tables, taskKeyed?, exempt?})` lists the tables with a person column (`subject`, `owner`, `created_by`, `user_id`, `person`) that nothing covers, for a service test to assert is empty |
| `openvibe-sdk/chat` | both | `createChatClient(client)`: `global.send/history/search`, `rooms.*` (list, create, get, update, messages, send, deleteMessage, join, leave, read, members, setMember), `dms.*` (list, create, get, messages, send, deleteMessage, read, unread, blocks, block, unblock), `exportMine()`, `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), `dms.*` (list, create, get, messages, send, deleteMessage, read, unread, blocks, block, unblock), `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) |
@@ -223,10 +224,10 @@ Until OpenVibe.Tools serves the gateway facade (Tools S6), point the jobs client
@@ -223,10 +224,10 @@ Until OpenVibe.Tools serves the gateway facade (Tools S6), point the jobs client
| `openvibe-sdk/placement` | server | `plan(requirements, offers, {rateCards, states, now, current, minGain, weights, success})` → `platform.placement-result@1`; `place(requirements, offers, {…plan, person, step, floor, priors, outcomes, rules, names})` → `{result, line, ranked, rejected, fallback}`: the person's rules and ownership, the quality bar, cost per success and one sentence saying why; `optionKey`, `learn`, `successOf`, `escalate`, `lineFor`; `TRUST_CLASSES`, `TRUST_ORDER`, `DEFAULT_TRUST`; `planAi(requirements, deployments, {enabled, usage, evidence, rates, current})` → `{chosen, rejected, objective, estimate, explain}`: the AI Exchange face of the same planner (hard gates, the cost per success in nano-USD, sparse evidence marked), off by default ([details](docs/placement.md)) |
| `openvibe-sdk/placement` | server | `plan(requirements, offers, {rateCards, states, now, current, minGain, weights, success})` → `platform.placement-result@1`; `place(requirements, offers, {…plan, person, step, floor, priors, outcomes, rules, names})` → `{result, line, ranked, rejected, fallback}`: the person's rules and ownership, the quality bar, cost per success and one sentence saying why; `optionKey`, `learn`, `successOf`, `escalate`, `lineFor`; `TRUST_CLASSES`, `TRUST_ORDER`, `DEFAULT_TRUST`; `planAi(requirements, deployments, {enabled, usage, evidence, rates, current})` → `{chosen, rejected, objective, estimate, explain}`: the AI Exchange face of the same planner (hard gates, the cost per success in nano-USD, sparse evidence marked), off by default ([details](docs/placement.md)) |
| `openvibe-sdk/play` | server | `createPlayClient(client)`: OpenVibe.Play's achievements and leaderboards for any game — `titles.define(title, {name, url?, summary?, achievements, boards})`, `scores.post(title, {board, ref, entries})`, `achievements.unlock(title, {player, achievements})`, `players.adopt(title, guest, person)`, `players.achievements(title, player)`; public `titles.list/get` and `boards.get(title, board, {period, page})` need no token |
| `openvibe-sdk/play` | server | `createPlayClient(client)`: OpenVibe.Play's achievements and leaderboards for any game — `titles.define(title, {name, url?, summary?, achievements, boards})`, `scores.post(title, {board, ref, entries})`, `achievements.unlock(title, {player, achievements})`, `players.adopt(title, guest, person)`, `players.achievements(title, player)`; public `titles.list/get` and `boards.get(title, board, {period, page})` need no token |
| `openvibe-sdk/download` | server | `createDownloadClient({ key })`: OpenVibe.Download, a person's drive — `files.list/all/get/remove`, `folders.list/create/ensurePath(path)`, `upload({ path \| data, name, folderId })` in parts, `usage()`, and `sync(dir, { to, delete, dryRun, exclude })` backups. The `openvibe-download` command: `sync <folder> --to <drive/path>`, `ls`, `usage` (`OPENVIBE_DOWNLOAD_KEY`, a sync key from openvibe.download/sync) |
| `openvibe-sdk/download` | server | `createDownloadClient({ key })`: OpenVibe.Download, a person's drive — `files.list/all/get/remove`, `folders.list/create/ensurePath(path)`, `upload({ path \| data, name, folderId })` in parts, `usage()`, and `sync(dir, { to, delete, dryRun, exclude })` backups. The `openvibe-download` command: `sync <folder> --to <drive/path>`, `ls`, `usage` (`OPENVIBE_DOWNLOAD_KEY`, a sync key from openvibe.download/sync) |
-| `openvibe-sdk/quest` | server | `createQuestClient(client)`: `daily()`, `myDaily()`, `stepTypes()`, `contributeStepType(step)` for the shared daily quest and reviewable event-backed steps (`quest.steps.contribute`); `appQuests.define/list/retire` for an app's own badge quests (`quest.app_quest.manage`); `eventType(projectId, name)` builds an app event topic. App quests pay badges only; production events count |
+| `openvibe-sdk/quest` | server | `createQuestClient(client)`: `daily()`, `myDaily()`, `today()` (authenticated current-day response), `stepTypes()`, `contributeStepType(step)` for the shared daily quest and reviewable event-backed steps (`quest.steps.contribute`); `appQuests.define/list/retire` for an app's own badge quests (`quest.app_quest.manage`); `eventType(projectId, name)` builds an app event topic. App quests pay badges only; production events count |
| `openvibe-sdk/live` | server | `createLiveClient(client)`: OpenVibe.Live's documented routes. `streams.list/get/recentlyOnline/mine/update/end/heartbeat/endpoint`, `slots.list/create/update/remove/regenerateKey` (a slot is a saved setup with its own stream key), `goLive(slotId, {title, category, tags})` → `{ stream, endpoint }`, `channels.get/mine/update/toggleFollow/url`, `content.feed/moments({type, sort, window, cursor, limit})`; WHIP: `whip.url(streamKey)`, `whip.publish(url, offerSdp, {key?})` → `{ answer, location }` (the client's own token never goes to `/whip`), `whip.end(location)`. Writes are never retried |
| `openvibe-sdk/live` | server | `createLiveClient(client)`: OpenVibe.Live's documented routes. `streams.list/get/recentlyOnline/mine/update/end/heartbeat/endpoint`, `slots.list/create/update/remove/regenerateKey` (a slot is a saved setup with its own stream key), `goLive(slotId, {title, category, tags})` → `{ stream, endpoint }`, `channels.get/mine/update/toggleFollow/url`, `content.feed/moments({type, sort, window, cursor, limit})`; WHIP: `whip.url(streamKey)`, `whip.publish(url, offerSdp, {key?})` → `{ answer, location }` (the client's own token never goes to `/whip`), `whip.end(location)`. Writes are never retried |
| `openvibe-sdk/streams` | server | `createStreamsClient(client, {baseUrl?})`: an agent's own stream on OpenVibe.Live (the Agent Streams plan, S1; `live.agent-stream@1`, an `ags_…` id with no stream key and no slot). `startAgentStream({title, task, visibility, allowSites})` → `{stream, publish}` (the WHIP url and the one-time `wpt_…` ticket with its expiry), `renewTicket(id)` and `revokeTicket(id)` (the kill switch: Live ends the WHIP session at once), `state(id, {ticket})` (what the desk polls: viewers, `encode`, the curtain, `ends_at`), `postFrame(id, bytes, {ticket, contentType})` (the last frame while Live wants no video, JPEG or WebP at most 200 KB), `setCurtain(id, up, reason)`, `setVisibility(id, v)`, `end(id)` and `viewTicket(id)`. The publish ticket is a bearer of its own on the desk routes; a bad id, visibility, curtain reason or an oversized frame is refused before anything leaves. [Docs](docs/streams.md) |
| `openvibe-sdk/streams` | server | `createStreamsClient(client, {baseUrl?})`: an agent's own stream on OpenVibe.Live (the Agent Streams plan, S1; `live.agent-stream@1`, an `ags_…` id with no stream key and no slot). `startAgentStream({title, task, visibility, allowSites})` → `{stream, publish}` (the WHIP url and the one-time `wpt_…` ticket with its expiry), `renewTicket(id)` and `revokeTicket(id)` (the kill switch: Live ends the WHIP session at once), `state(id, {ticket})` (what the desk polls: viewers, `encode`, the curtain, `ends_at`), `postFrame(id, bytes, {ticket, contentType})` (the last frame while Live wants no video, JPEG or WebP at most 200 KB), `setCurtain(id, up, reason)`, `setVisibility(id, v)`, `end(id)` and `viewTicket(id)`. The publish ticket is a bearer of its own on the desk routes; a bad id, visibility, curtain reason or an oversized frame is refused before anything leaves. [Docs](docs/streams.md) |
-| `openvibe-sdk/inventory` | server | `createInventoryClient(client)`: OpenVibe.Inventory, the network-wide inventory (ADR-054). Public reads: `kinds.list/get`, `definitions.list({kind, issuer})/get`, `people.items(subject)`, `people.equipped(subject)`, `equipped([subjects])` (≤ 100, for chat renders). The person's own: `me.items()`, `me.equip({kind, slot, instance_id | null})`. An issuer's: `definitions.create/update`, `grants.create({definition_id, subject, idempotency_key})` → `{ instance, created }` (retry-safe on the key), `instances.consume/revoke`. `forPerson(usr_…)` sends `X-OV-Subject` for a service acting for a person. Audience `openvibe.inventory` |
+| `openvibe-sdk/inventory` | server | `createInventoryClient(client)`: OpenVibe.Inventory, the network-wide inventory (ADR-054). Public reads: `kinds.list/get`, `definitions.list({kind, issuer})/get`, `people.items(subject)`, `people.holdings(subject, opts)`, `people.holdingsCount(subject, opts)`, `people.holdingPages(subject, opts)`, `people.equipped(subject)`, `equipped([subjects])` (≤ 100, for chat renders). The person's own: `me.items()`, `me.equip({kind, slot, instance_id | null})`. An issuer's: `definitions.create/update`, `grants.create({definition_id, subject, idempotency_key})` → `{ instance, created }` (retry-safe on the key), `instances.consume/revoke`. `forPerson(usr_…)` sends `X-OV-Subject` for a service acting for a person. Audience `openvibe.inventory` |
| `openvibe-sdk/tips` | server | `createTipsClient(client)`: OpenVibe.Tips' `/api/v1`. A creator's page: `profiles.get/update/totals/sounds/supporters`. Tipping: `tip({creator, amount, message?, goal_id?, pay_with})` → `{ interaction, checkout_url, checkout_ref }`, `send.message/tts/media/sound`. Receipts: `interactions.get/list/recordExternal/hide/restore`. `goals.list/get/create/update/close`, `moderation.queue/log/moderators.*`, `overlays.tokens.*/configs.*/simulate`, `me.export/erase`. Every write carries an Idempotency-Key, which Tips replays once, so retries are safe. Audience `openvibe.tips` |
| `openvibe-sdk/tips` | server | `createTipsClient(client)`: OpenVibe.Tips' `/api/v1`. A creator's page: `profiles.get/update/totals/sounds/supporters`. Tipping: `tip({creator, amount, message?, goal_id?, pay_with})` → `{ interaction, checkout_url, checkout_ref }`, `send.message/tts/media/sound`. Receipts: `interactions.get/list/recordExternal/hide/restore`. `goals.list/get/create/update/close`, `moderation.queue/log/moderators.*`, `overlays.tokens.*/configs.*/simulate`, `me.export/erase`. Every write carries an Idempotency-Key, which Tips replays once, so retries are safe. Audience `openvibe.tips` |
| `openvibe-sdk/blog` | server | `createBlogClient(client)`: OpenVibe.Blog for the person who authorized the app. `blogs.get/posts(handle, {limit, offset, all})/feed` (JSON Feed), `changelog({service, kind, area, q, limit, before})` (the network changelog's `blog.changelog-entry@2` rows), `posts.create(handle, {title, body, summary, visibility, …})` → `{ post, revision }`, `posts.get/update(id, {…, expected_revision})/publish(id, {revision})/unpublish/schedule(id, {at, revision, action})/cancelSchedule/revert/remove/revisions/revision(id, n)`. Public capabilities since openvibe-contracts 0.206.0; publish, unpublish, schedule and delete are sensitive. Creating and reverting are never retried on their own. Audience `openvibe.blog` |
| `openvibe-sdk/blog` | server | `createBlogClient(client)`: OpenVibe.Blog for the person who authorized the app. `blogs.get/posts(handle, {limit, offset, all})/feed` (JSON Feed), `changelog({service, kind, area, q, limit, before})` (the network changelog's `blog.changelog-entry@2` rows), `posts.create(handle, {title, body, summary, visibility, …})` → `{ post, revision }`, `posts.get/update(id, {…, expected_revision})/publish(id, {revision})/unpublish/schedule(id, {at, revision, action})/cancelSchedule/revert/remove/revisions/revision(id, n)`. Public capabilities since openvibe-contracts 0.206.0; publish, unpublish, schedule and delete are sensitive. Creating and reverting are never retried on their own. Audience `openvibe.blog` |
| `openvibe-sdk/reviews` | server | `createReviewsClient(client)`: OpenVibe.Reviews. `resolve({name, url, gtin, sku, mpn, source, external})` (a match or candidates; a name alone never matches), `entities.list({q, limit, offset})/get(ref)/history/aggregate/summaryRevision(ref, n)`, `corrections.submit(ref, {body, target_type, target_id, evidence_url})` (to the editors' queue for the person, never retried on its own). Public capabilities since openvibe-contracts 0.207.0. Audience `openvibe.reviews` |
| `openvibe-sdk/reviews` | server | `createReviewsClient(client)`: OpenVibe.Reviews. `resolve({name, url, gtin, sku, mpn, source, external})` (a match or candidates; a name alone never matches), `entities.list({q, limit, offset})/get(ref)/history/aggregate/summaryRevision(ref, n)`, `corrections.submit(ref, {body, target_type, target_id, evidence_url})` (to the editors' queue for the person, never retried on its own). Public capabilities since openvibe-contracts 0.207.0. Audience `openvibe.reviews` |
@@ -3,7 +3,7 @@
@@ -3,7 +3,7 @@
"stage": "alpha",
"stage": "alpha",
"code": true,
"code": true,
"wave": "W2",
"wave": "W2",
- "version": "0.145.0",
+ "version": "0.146.0",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), \u00a73.2; roadmap \u00a730",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), \u00a73.2; roadmap \u00a730",
"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",
@@ -36,4 +36,5 @@ module.exports = {
@@ -36,4 +36,5 @@ module.exports = {
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'),
};
};
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
-// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.145.0, MIT). Do not edit.
+// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.146.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). No server-only code: no service tokens, JWT verification, events, identity.
+// (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). 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 ──────────────────────────────────────────────────
@@ -43,6 +43,7 @@ module.exports = {
@@ -43,6 +43,7 @@ module.exports = {
tools: __load(48),
tools: __load(48),
projects: __load(49),
projects: __load(49),
geo: __load(50),
geo: __load(50),
+ history: __load(51),
};
};
},
},
// ── 1: src/core/index.js ───────────────────────────────────────────
// ── 1: src/core/index.js ───────────────────────────────────────────
@@ -57,7 +58,7 @@ const { paginate, offsetPager } = __load(7);
@@ -57,7 +58,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.145.0';
+const SDK_VERSION = '0.146.0';
module.exports = {
module.exports = {
createClient,
createClient,
@@ -3676,6 +3677,10 @@ const { isActingSubjectId } = __load(5);
@@ -3676,6 +3677,10 @@ const { isActingSubjectId } = __load(5);
const { paginate } = __load(7);
const { paginate } = __load(7);
const enc = encodeURIComponent;
const enc = encodeURIComponent;
+const DEFAULT_BOT_URL = 'https://openvibe.bot';
+// The three views OpenVibe.Bot's embeddable panel renders (docs/controls-embed.md there).
+const EMBED_MODES = ['panel', 'overlay', 'compact'];
+const ROBOT_ID = /^rob_[A-Za-z0-9_-]{4,60}$/;
const DESK_NAME = /^desk-[a-z0-9][a-z0-9-]{0,23}$/;
const DESK_NAME = /^desk-[a-z0-9][a-z0-9-]{0,23}$/;
const randomDesk = () => `desk-${Math.random().toString(36).slice(2, 12).padEnd(10, '0')}`;
const randomDesk = () => `desk-${Math.random().toString(36).slice(2, 12).padEnd(10, '0')}`;
@@ -3705,6 +3710,19 @@ function createBotClient(client, defaults = {}) {
@@ -3705,6 +3710,19 @@ function createBotClient(client, defaults = {}) {
const orNull = (p) => p.catch((err) => { if (isOpenVibeError(err) && err.status === 404) return null; throw err; });
const orNull = (p) => p.catch((err) => { if (isOpenVibeError(err) && err.status === 404) return null; throw err; });
const once = { idempotencyKey: false }; // a one-time code is its own idempotency: never repeat the call
const once = { idempotencyKey: false }; // a one-time code is its own idempotency: never repeat the call
+ /**
+ * The address of a robot's embeddable panel, for a page that frames or links it:
+ * `https://openvibe.bot/panel/<rob_…>/embed?mode=panel|overlay|compact`. `panel` is the full panel,
+ * `overlay` only the live controls (transparent; over a video) and `compact` one row for a narrow card.
+ * No request is made; the host page's origin must be in Bot's BOT_EMBED_ORIGINS to frame it.
+ */
+ function embedUrl(robotId, mode = 'panel') {
+ if (typeof robotId !== 'string' || !ROBOT_ID.test(robotId)) throw new TypeError('robotId must be a Bot robot id (rob_…)');
+ if (!EMBED_MODES.includes(mode)) throw new TypeError(`mode must be one of ${EMBED_MODES.join(', ')}`);
+ const base = String(baseUrl || DEFAULT_BOT_URL).replace(/\/+$/, '');
+ return `${base}/panel/${enc(robotId)}/embed?mode=${mode}`;
+ }
+
const robots = {
const robots = {
/** { robots } — a person's own robots; a service must name owner (or act as them). */
/** { robots } — a person's own robots; a service must name owner (or act as them). */
list: (query = {}, o = {}) => call({ path: '/api/v1/robots', query }, o),
list: (query = {}, o = {}) => call({ path: '/api/v1/robots', query }, o),
@@ -3846,6 +3864,8 @@ function createBotClient(client, defaults = {}) {
@@ -3846,6 +3864,8 @@ function createBotClient(client, defaults = {}) {
},
},
/** The agent redeems a one-time pairing code (the code is the credential; never retried) -> the pair result. */
/** The agent redeems a one-time pairing code (the code is the credential; never retried) -> the pair result. */
pair: (input = {}, o = {}) => call({ method: 'POST', path: '/api/v1/pair', json: input, ...once }, o),
pair: (input = {}, o = {}) => call({ method: 'POST', path: '/api/v1/pair', json: input, ...once }, o),
+ /** The embeddable panel's address for a robot and view (no request; see the doc comment above). */
+ embedUrl,
/** The same client acting for someone else: as('usr_…') or as({ actingSubject }). */
/** The same client acting for someone else: as('usr_…') or as({ actingSubject }). */
as(who) {
as(who) {
const next = typeof who === 'string' ? { actingSubject: who } : { ...who };
const next = typeof who === 'string' ? { actingSubject: who } : { ...who };
@@ -3901,6 +3921,23 @@ function fabricOf(text, o = {}) {
@@ -3901,6 +3921,23 @@ function fabricOf(text, o = {}) {
if (o.answer) q.answer = 'true';
if (o.answer) q.answer = 'true';
return q;
return q;
}
}
+const CODE_KEYS = ['lang', 'repo', 'licence', 'limit', 'cursor'];
+const CODE_BODY_KEYS = ['lang', 'repo', 'licence'];
+
+/** The query string of GET /api/v1/search/code. */
+function codeOf(text, o = {}) {
+ const q = { q: String(text == null ? '' : text) };
+ for (const k of CODE_KEYS) if (o[k] !== undefined && o[k] !== null && o[k] !== '') q[k] = o[k];
+ return q;
+}
+
+/** The body of POST /api/v1/search/code/deep; q is required, the filters are optional. */
+function codeBody(text, o = {}) {
+ const body = { q: String(text == null ? '' : text) };
+ for (const k of CODE_BODY_KEYS) if (o[k] !== undefined && o[k] !== null && o[k] !== '') body[k] = o[k];
+ return body;
+}
+
const FACET_KEY = /^[a-z][a-z0-9_]{0,39}$/;
const FACET_KEY = /^[a-z][a-z0-9_]{0,39}$/;
function queryOf(text, o = {}) {
function queryOf(text, o = {}) {
@@ -3965,10 +4002,26 @@ function createSearchClient(client, defaults = {}) {
@@ -3965,10 +4002,26 @@ function createSearchClient(client, defaults = {}) {
suggest: (text, o = {}) => call({ path: pathWith('/api/v1/suggest', queryOf(text, { owner: o.owner, type: o.type, limit: o.limit })) }, o),
suggest: (text, o = {}) => call({ path: pathWith('/api/v1/suggest', queryOf(text, { owner: o.owner, type: o.type, limit: o.limit })) }, o),
/** One document by exact id, or null (a document you may not see answers the same as a missing one). */
/** One document by exact id, or null (a document you may not see answers the same as a missing one). */
document: (owner, type, id, o = {}) => orNull(call({ path: `/api/v1/documents/${enc(owner)}/${enc(type)}/${enc(id)}` }, o)),
document: (owner, type, id, o = {}) => orNull(call({ path: `/api/v1/documents/${enc(owner)}/${enc(type)}/${enc(id)}` }, o)),
+ /** { results, sources, next_cursor } — merged public code; `deep: true` asks the cited answer (signed in). */
+ code: (text, o = {}) => (o.deep
+ ? call({ method: 'POST', path: '/api/v1/search/code/deep', json: codeBody(text, o), idempotencyKey: false }, o)
+ : call({ path: pathWith('/api/v1/search/code', codeOf(text, o)) }, o)),
+ /** Every merged code result, page after page (cursor). Deep mode is one answer, not a page stream. */
+ async *codeIterate(text, o = {}) {
+ if (o.deep) throw new TypeError('codeIterate is for pages; use code(text, { deep: true }) for an answer');
+ let cursor = o.cursor || null;
+ let seen = 0;
+ const max = Number.isFinite(o.max) ? o.max : Infinity;
+ do {
+ const page = await call({ path: pathWith('/api/v1/search/code', codeOf(text, { ...o, cursor })) }, o);
+ for (const r of page.results || []) { if (seen++ >= max) return; yield r; }
+ cursor = page.next_cursor || null;
+ } while (cursor);
+ },
};
};
}
}
-module.exports = { createSearchClient, queryOf, fabricOf };
+module.exports = { createSearchClient, queryOf, fabricOf, codeOf };
},
},
// ── 35: src/bench.js ────────────────────────────────────────────────
// ── 35: src/bench.js ────────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
@@ -6078,6 +6131,34 @@ function createGeoClient(client, { baseUrl, fetch: fetchImpl = null, samples = 3
@@ -6078,6 +6131,34 @@ function createGeoClient(client, { baseUrl, fetch: fetchImpl = null, samples = 3
module.exports = { createGeoClient };
module.exports = { createGeoClient };
},
},
+// ── 51: src/history.js ──────────────────────────────────────────────
+function (module, exports) {
+'use strict';
+
+/** Reader and owner controls for the signed-in person's private cross-site history. */
+function createHistoryClient(client, { baseUrl } = {}) {
+ if (!client || typeof client.json !== 'function') throw new TypeError('createHistoryClient: pass an openvibe-sdk/core client');
+ const call = (opts, o = {}) => client.json({ service: 'network', baseUrl, ...opts, signal: o.signal });
+ const enc = (v) => encodeURIComponent(String(v));
+ return {
+ /** Newest first. query accepts cursor, limit, day, site, kind and q. */
+ list: (query = {}, o = {}) => call({ path: '/api/v1/history', query }, o),
+ /** Search titles with PostgreSQL full text; query is natural language. */
+ find: (q, { limit, ...o } = {}) => call({ path: '/api/v1/history/find', query: { q, limit } }, o),
+ /** Last day or week, using titles and kinds only. */
+ summary: (period = 'day', o = {}) => call({ path: '/api/v1/history/summary', query: { period } }, o),
+ /** Recent rows for the signed-in person's agent; the server enforces the opt-in. */
+ agentRecent: (query = {}, o = {}) => call({ path: '/api/v1/history/agents/recent', query }, o),
+ settings: (patch, o = {}) => call({ method: 'PUT', path: '/api/v1/history/settings', json: patch }, o),
+ remove: (id, o = {}) => call({ method: 'DELETE', path: `/api/v1/history/${enc(id)}` }, o),
+ removeDay: (day, o = {}) => call({ method: 'DELETE', path: `/api/v1/history/day/${enc(day)}` }, o),
+ removeSite: (site, o = {}) => call({ method: 'DELETE', path: `/api/v1/history/site/${enc(site)}` }, o),
+ clear: (o = {}) => call({ method: 'DELETE', path: '/api/v1/history' }, o),
+ };
+}
+
+module.exports = { createHistoryClient };
+},
];
];
const __cache = new Array(__defs.length);
const __cache = new Array(__defs.length);
function __load(i) {
function __load(i) {
@@ -6097,6 +6178,6 @@ export const {
@@ -6097,6 +6178,6 @@ export const {
realtime, media, cam, mediaUpload, ui, community,
realtime, media, cam, mediaUpload, ui, community,
homeFeed, bot, search, bench, meme, vision,
homeFeed, bot, search, bench, meme, vision,
themes, design, redact, chat, ai, credentials,
themes, design, redact, chat, ai, credentials,
- jobs, tools, projects, geo,
+ jobs, tools, projects, geo, history,
} = sdk;
} = sdk;
export default sdk;
export default sdk;
@@ -182,6 +182,11 @@
@@ -182,6 +182,11 @@
"createGitClient"
"createGitClient"
]
]
},
},
+ "history": {
+ "object": [
+ "createHistoryClient"
+ ]
+ },
"homeFeed": {
"homeFeed": {
"object": [
"object": [
"createHomeFeedClient"
"createHomeFeedClient"
@@ -380,6 +385,7 @@
@@ -380,6 +385,7 @@
"satisfiesRange": "function",
"satisfiesRange": "function",
"search": {
"search": {
"object": [
"object": [
+ "codeOf",
"createSearchClient",
"createSearchClient",
"fabricOf",
"fabricOf",
"queryOf"
"queryOf"
@@ -792,6 +798,7 @@
@@ -792,6 +798,7 @@
"frameConfig": "function",
"frameConfig": "function",
"frameTags": "function",
"frameTags": "function",
"mountFrame": "function",
"mountFrame": "function",
+ "online": "function",
"scriptUrls": "function"
"scriptUrls": "function"
},
},
"openvibe-sdk/front-page": {
"openvibe-sdk/front-page": {
@@ -819,6 +826,9 @@
@@ -819,6 +826,9 @@
},
},
"createGovernor": "function"
"createGovernor": "function"
},
},
+ "openvibe-sdk/history": {
+ "createHistoryClient": "function"
+ },
"openvibe-sdk/home-feed": {
"openvibe-sdk/home-feed": {
"createHomeFeedClient": "function"
"createHomeFeedClient": "function"
},
},
@@ -1075,6 +1085,7 @@
@@ -1075,6 +1085,7 @@
"validateRuntimeOffer": "function"
"validateRuntimeOffer": "function"
},
},
"openvibe-sdk/search": {
"openvibe-sdk/search": {
+ "codeOf": "function",
"createSearchClient": "function",
"createSearchClient": "function",
"fabricOf": "function",
"fabricOf": "function",
"queryOf": "function"
"queryOf": "function"
@@ -2,6 +2,6 @@
@@ -2,6 +2,6 @@
import cjs from '../src/frame.js';
import cjs from '../src/frame.js';
export const {
export const {
- mountFrame, frameTags, frameConfig, scriptUrls, DEFAULT_BASE,
+ mountFrame, frameTags, frameConfig, scriptUrls, online, DEFAULT_BASE,
} = 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/history.js. Do not edit.
+import cjs from '../src/history.js';
+
+export const {
+ createHistoryClient,
+} = cjs;
+export default cjs;
@@ -15,6 +15,6 @@ export const {
@@ -15,6 +15,6 @@ export const {
deals, shoes, trade, space, host, news,
deals, shoes, trade, space, host, news,
food, actor, run, video, pics, work,
food, actor, run, video, pics, work,
zone, git, play, download, openre, geo,
zone, git, play, download, openre, geo,
- usage, commerce, moneyReasons, telemetry,
+ history, usage, commerce, moneyReasons, telemetry,
} = cjs;
} = cjs;
export default cjs;
export default cjs;
@@ -2,6 +2,6 @@
@@ -2,6 +2,6 @@
import cjs from '../src/search.js';
import cjs from '../src/search.js';
export const {
export const {
- createSearchClient, queryOf, fabricOf,
+ createSearchClient, queryOf, fabricOf, codeOf,
} = cjs;
} = cjs;
export default cjs;
export default cjs;
@@ -69,6 +69,7 @@ module.exports = {
@@ -69,6 +69,7 @@ module.exports = {
download: require('./src/download'),
download: require('./src/download'),
openre: require('./src/openre'),
openre: require('./src/openre'),
geo: require('./src/geo'),
geo: require('./src/geo'),
+ history: require('./src/history'),
usage: require('./src/usage'),
usage: require('./src/usage'),
commerce: require('./src/commerce'),
commerce: require('./src/commerce'),
moneyReasons: require('./src/money-reasons'),
moneyReasons: require('./src/money-reasons'),
@@ -1,12 +1,12 @@
@@ -1,12 +1,12 @@
{
{
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.145.0",
+ "version": "0.146.0",
"lockfileVersion": 3,
"lockfileVersion": 3,
"requires": true,
"requires": true,
"packages": {
"packages": {
"": {
"": {
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.145.0",
+ "version": "0.146.0",
"license": "MIT",
"license": "MIT",
"bin": {
"bin": {
"openvibe-download": "bin/openvibe-download.js"
"openvibe-download": "bin/openvibe-download.js"
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.145.0",
+ "version": "0.146.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",
@@ -320,6 +320,11 @@
@@ -320,6 +320,11 @@
"import": "./esm/geo.mjs",
"import": "./esm/geo.mjs",
"default": "./src/geo.js"
"default": "./src/geo.js"
},
},
+ "./history": {
+ "types": "./types/history.d.ts",
+ "import": "./esm/history.mjs",
+ "default": "./src/history.js"
+ },
"./openre": {
"./openre": {
"types": "./types/openre.d.ts",
"types": "./types/openre.d.ts",
"browser": null,
"browser": null,
@@ -73,6 +73,7 @@ const ENTRIES = {
@@ -73,6 +73,7 @@ const ENTRIES = {
'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',
+ 'history.mjs': '../src/history.js',
'openre.mjs': '../src/openre.js',
'openre.mjs': '../src/openre.js',
'play.mjs': '../src/play.js',
'play.mjs': '../src/play.js',
'download.mjs': '../src/download.js',
'download.mjs': '../src/download.js',
@@ -37,6 +37,10 @@ const { isActingSubjectId } = require('./core/ids');
@@ -37,6 +37,10 @@ const { isActingSubjectId } = require('./core/ids');
const { paginate } = require('./core/paginate');
const { paginate } = require('./core/paginate');
const enc = encodeURIComponent;
const enc = encodeURIComponent;
+const DEFAULT_BOT_URL = 'https://openvibe.bot';
+// The three views OpenVibe.Bot's embeddable panel renders (docs/controls-embed.md there).
+const EMBED_MODES = ['panel', 'overlay', 'compact'];
+const ROBOT_ID = /^rob_[A-Za-z0-9_-]{4,60}$/;
const DESK_NAME = /^desk-[a-z0-9][a-z0-9-]{0,23}$/;
const DESK_NAME = /^desk-[a-z0-9][a-z0-9-]{0,23}$/;
const randomDesk = () => `desk-${Math.random().toString(36).slice(2, 12).padEnd(10, '0')}`;
const randomDesk = () => `desk-${Math.random().toString(36).slice(2, 12).padEnd(10, '0')}`;
@@ -66,6 +70,19 @@ function createBotClient(client, defaults = {}) {
@@ -66,6 +70,19 @@ function createBotClient(client, defaults = {}) {
const orNull = (p) => p.catch((err) => { if (isOpenVibeError(err) && err.status === 404) return null; throw err; });
const orNull = (p) => p.catch((err) => { if (isOpenVibeError(err) && err.status === 404) return null; throw err; });
const once = { idempotencyKey: false }; // a one-time code is its own idempotency: never repeat the call
const once = { idempotencyKey: false }; // a one-time code is its own idempotency: never repeat the call
+ /**
+ * The address of a robot's embeddable panel, for a page that frames or links it:
+ * `https://openvibe.bot/panel/<rob_…>/embed?mode=panel|overlay|compact`. `panel` is the full panel,
+ * `overlay` only the live controls (transparent; over a video) and `compact` one row for a narrow card.
+ * No request is made; the host page's origin must be in Bot's BOT_EMBED_ORIGINS to frame it.
+ */
+ function embedUrl(robotId, mode = 'panel') {
+ if (typeof robotId !== 'string' || !ROBOT_ID.test(robotId)) throw new TypeError('robotId must be a Bot robot id (rob_…)');
+ if (!EMBED_MODES.includes(mode)) throw new TypeError(`mode must be one of ${EMBED_MODES.join(', ')}`);
+ const base = String(baseUrl || DEFAULT_BOT_URL).replace(/\/+$/, '');
+ return `${base}/panel/${enc(robotId)}/embed?mode=${mode}`;
+ }
+
const robots = {
const robots = {
/** { robots } — a person's own robots; a service must name owner (or act as them). */
/** { robots } — a person's own robots; a service must name owner (or act as them). */
list: (query = {}, o = {}) => call({ path: '/api/v1/robots', query }, o),
list: (query = {}, o = {}) => call({ path: '/api/v1/robots', query }, o),
@@ -207,6 +224,8 @@ function createBotClient(client, defaults = {}) {
@@ -207,6 +224,8 @@ function createBotClient(client, defaults = {}) {
},
},
/** The agent redeems a one-time pairing code (the code is the credential; never retried) -> the pair result. */
/** The agent redeems a one-time pairing code (the code is the credential; never retried) -> the pair result. */
pair: (input = {}, o = {}) => call({ method: 'POST', path: '/api/v1/pair', json: input, ...once }, o),
pair: (input = {}, o = {}) => call({ method: 'POST', path: '/api/v1/pair', json: input, ...once }, o),
+ /** The embeddable panel's address for a robot and view (no request; see the doc comment above). */
+ embedUrl,
/** The same client acting for someone else: as('usr_…') or as({ actingSubject }). */
/** The same client acting for someone else: as('usr_…') or as({ actingSubject }). */
as(who) {
as(who) {
const next = typeof who === 'string' ? { actingSubject: who } : { ...who };
const next = typeof who === 'string' ? { actingSubject: who } : { ...who };
@@ -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.145.0';
+const SDK_VERSION = '0.146.0';
module.exports = {
module.exports = {
createClient,
createClient,
@@ -147,4 +147,25 @@ async function mountFrame(opts = {}) {
@@ -147,4 +147,25 @@ async function mountFrame(opts = {}) {
return { navbar, footer };
return { navbar, footer };
}
}
-module.exports = { mountFrame, frameTags, frameConfig, scriptUrls, DEFAULT_BASE };
+/**
+ * The network's "who is online" read, the data the shared footer strip draws: GET <base>/api/frame/online
+ * (openvibe.network). `site` is the calling site's id (its Frame service id), so the answer carries that
+ * site's own strip items; omit it and `items` is empty. The answer is returned as it is:
+ *
+ * { members, guests, total, people: [{ name, username, avatar, site }], more,
+ * live: { channels, watching, first: [{ name, url }] }, counts: [{ label, value, icon, url }],
+ * items: [{ label, icon, link, value? , line? }], sites, views_7d }
+ *
+ * A guest is a number only: the read never says who they are, where they are or what made them distinct.
+ * Public and CORS-open, cached 30 seconds at the edge. Throws on a non-2xx answer or when fetch is missing.
+ */
+async function online({ base = DEFAULT_BASE, site = '', signal, fetch: fetchImpl = (typeof fetch === 'function' ? fetch : null) } = {}) {
+ const b = baseOf(base);
+ if (typeof fetchImpl !== 'function') throw new TypeError('openvibe-sdk/frame: online needs a fetch implementation');
+ const q = /^[a-z0-9][a-z0-9-]{0,39}$/.test(String(site || '')) ? `?site=${encodeURIComponent(String(site))}` : '';
+ const res = await fetchImpl(`${b}/api/frame/online${q}`, { headers: { accept: 'application/json' }, credentials: 'omit', signal });
+ if (!res || !res.ok) throw new Error(`openvibe-sdk/frame: online answered ${res ? res.status : 'nothing'}`);
+ return await res.json();
+}
+
+module.exports = { mountFrame, frameTags, frameConfig, scriptUrls, online, DEFAULT_BASE };
new file mode 100644
new file mode 100644
@@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
+'use strict';
+
+/** Reader and owner controls for the signed-in person's private cross-site history. */
+function createHistoryClient(client, { baseUrl } = {}) {
+ if (!client || typeof client.json !== 'function') throw new TypeError('createHistoryClient: pass an openvibe-sdk/core client');
+ const call = (opts, o = {}) => client.json({ service: 'network', baseUrl, ...opts, signal: o.signal });
+ const enc = (v) => encodeURIComponent(String(v));
+ return {
+ /** Newest first. query accepts cursor, limit, day, site, kind and q. */
+ list: (query = {}, o = {}) => call({ path: '/api/v1/history', query }, o),
+ /** Search titles with PostgreSQL full text; query is natural language. */
+ find: (q, { limit, ...o } = {}) => call({ path: '/api/v1/history/find', query: { q, limit } }, o),
+ /** Last day or week, using titles and kinds only. */
+ summary: (period = 'day', o = {}) => call({ path: '/api/v1/history/summary', query: { period } }, o),
+ /** Recent rows for the signed-in person's agent; the server enforces the opt-in. */
+ agentRecent: (query = {}, o = {}) => call({ path: '/api/v1/history/agents/recent', query }, o),
+ settings: (patch, o = {}) => call({ method: 'PUT', path: '/api/v1/history/settings', json: patch }, o),
+ remove: (id, o = {}) => call({ method: 'DELETE', path: `/api/v1/history/${enc(id)}` }, o),
+ removeDay: (day, o = {}) => call({ method: 'DELETE', path: `/api/v1/history/day/${enc(day)}` }, o),
+ removeSite: (site, o = {}) => call({ method: 'DELETE', path: `/api/v1/history/site/${enc(site)}` }, o),
+ clear: (o = {}) => call({ method: 'DELETE', path: '/api/v1/history' }, o),
+ };
+}
+
+module.exports = { createHistoryClient };
@@ -33,6 +33,17 @@ function createInventoryClient(client, { baseUrl, subject } = {}) {
@@ -33,6 +33,17 @@ function createInventoryClient(client, { baseUrl, subject } = {}) {
return enc(String(v));
return enc(String(v));
};
};
const person = (s) => check(SUBJECT_RE, s, 'a person is a usr_ subject id');
const person = (s) => check(SUBJECT_RE, s, 'a person is a usr_ subject id');
+ const bool = (value) => typeof value === 'boolean' ? String(value) : value;
+ const holdingsQuery = ({ issuer, site, kind, rarity, equipped, tradable, text, sort, cursor, limit } = {}) =>
+ ({ issuer, site, kind, rarity, equipped: bool(equipped), tradable: bool(tradable), text, sort, cursor, limit });
+ const holdingPages = async function* (path, options = {}) {
+ let cursor = options.cursor;
+ do {
+ const page = await call({ path, query: holdingsQuery({ ...options, cursor }), signal: options.signal });
+ yield page;
+ cursor = page.next_cursor;
+ } while (cursor);
+ };
const kinds = {
const kinds = {
/** Every kind: its schema, slots and the surfaces that render it. */
/** Every kind: its schema, slots and the surfaces that render it. */
@@ -50,6 +61,10 @@ function createInventoryClient(client, { baseUrl, subject } = {}) {
@@ -50,6 +61,10 @@ function createInventoryClient(client, { baseUrl, subject } = {}) {
const people = {
const people = {
/** A person's owned items (public), newest first: { items, next }. */
/** A person's owned items (public), newest first: { items, next }. */
items: (s, { kind, cursor, limit, signal } = {}) => call({ path: `/api/v1/people/${person(s)}/items`, query: { kind, cursor, limit }, signal }),
items: (s, { kind, cursor, limit, signal } = {}) => call({ path: `/api/v1/people/${person(s)}/items`, query: { kind, cursor, limit }, signal }),
+ /** Stacked holdings; one entry per definition, with count and representative instance. */
+ holdings: (s, options = {}) => call({ path: `/api/v1/people/${person(s)}/holdings`, query: holdingsQuery(options), signal: options.signal }),
+ holdingsCount: (s, options = {}) => call({ path: `/api/v1/people/${person(s)}/holdings/count`, query: holdingsQuery(options), signal: options.signal }),
+ holdingPages: (s, options = {}) => holdingPages(`/api/v1/people/${person(s)}/holdings`, options),
/** What a person wears: { subject, slots: { '<kind>:<slot>': { instance_id, definition_id, token?, media_id? } } }. */
/** What a person wears: { subject, slots: { '<kind>:<slot>': { instance_id, definition_id, token?, media_id? } } }. */
equipped: (s, { signal } = {}) => call({ path: `/api/v1/people/${person(s)}/equipped`, signal }),
equipped: (s, { signal } = {}) => call({ path: `/api/v1/people/${person(s)}/equipped`, signal }),
};
};
@@ -63,6 +78,9 @@ function createInventoryClient(client, { baseUrl, subject } = {}) {
@@ -63,6 +78,9 @@ function createInventoryClient(client, { baseUrl, subject } = {}) {
const me = {
const me = {
/** The caller's own items, every state (owned, consumed, revoked). */
/** The caller's own items, every state (owned, consumed, revoked). */
items: ({ kind, cursor, limit, signal } = {}) => call({ path: '/api/v1/me/items', query: { kind, cursor, limit }, signal }),
items: ({ kind, cursor, limit, signal } = {}) => call({ path: '/api/v1/me/items', query: { kind, cursor, limit }, signal }),
+ holdings: (options = {}) => call({ path: '/api/v1/me/holdings', query: holdingsQuery(options), signal: options.signal }),
+ holdingsCount: (options = {}) => call({ path: '/api/v1/me/holdings/count', query: holdingsQuery(options), signal: options.signal }),
+ holdingPages: (options = {}) => holdingPages('/api/v1/me/holdings', options),
/** Wear an item in a slot, or clear the slot with instance_id: null (inventory.equip-request@1). */
/** Wear an item in a slot, or clear the slot with instance_id: null (inventory.equip-request@1). */
equip: ({ kind, slot, instance_id: instanceId }, { signal } = {}) => {
equip: ({ kind, slot, instance_id: instanceId }, { signal } = {}) => {
if (instanceId !== null) check(ID_RE.instance, instanceId, 'instance_id is an inv_ id, or null to clear the slot');
if (instanceId !== null) check(ID_RE.instance, instanceId, 'instance_id is an inv_ id, or null to clear the slot');
@@ -50,6 +50,8 @@ function createQuestClient(client, { baseUrl } = {}) {
@@ -50,6 +50,8 @@ function createQuestClient(client, { baseUrl } = {}) {
appQuests, eventType, projectKey,
appQuests, eventType, projectKey,
/** The network-wide quest for today, or a past YYYY-MM-DD date (quest.daily@1). */
/** The network-wide quest for today, or a past YYYY-MM-DD date (quest.daily@1). */
daily: ({ date, signal } = {}) => call({ path: date ? `/api/v1/daily/${enc(date)}` : '/api/v1/daily', signal }),
daily: ({ date, signal } = {}) => call({ path: date ? `/api/v1/daily/${enc(date)}` : '/api/v1/daily', signal }),
+ /** This signed-in person's guided quest for today (requires a person token). */
+ today: ({ signal } = {}) => call({ path: '/api/v1/quests/today', signal }),
/** The person token's own checklist, completion and streak (quest.daily-progress@1). */
/** The person token's own checklist, completion and streak (quest.daily-progress@1). */
myDaily: ({ signal } = {}) => call({ path: '/api/v1/me/daily', signal }),
myDaily: ({ signal } = {}) => call({ path: '/api/v1/me/daily', signal }),
/** Approved event-backed step types (quest.step-type@1). */
/** Approved event-backed step types (quest.step-type@1). */
@@ -40,6 +40,23 @@ function fabricOf(text, o = {}) {
@@ -40,6 +40,23 @@ function fabricOf(text, o = {}) {
if (o.answer) q.answer = 'true';
if (o.answer) q.answer = 'true';
return q;
return q;
}
}
+const CODE_KEYS = ['lang', 'repo', 'licence', 'limit', 'cursor'];
+const CODE_BODY_KEYS = ['lang', 'repo', 'licence'];
+
+/** The query string of GET /api/v1/search/code. */
+function codeOf(text, o = {}) {
+ const q = { q: String(text == null ? '' : text) };
+ for (const k of CODE_KEYS) if (o[k] !== undefined && o[k] !== null && o[k] !== '') q[k] = o[k];
+ return q;
+}
+
+/** The body of POST /api/v1/search/code/deep; q is required, the filters are optional. */
+function codeBody(text, o = {}) {
+ const body = { q: String(text == null ? '' : text) };
+ for (const k of CODE_BODY_KEYS) if (o[k] !== undefined && o[k] !== null && o[k] !== '') body[k] = o[k];
+ return body;
+}
+
const FACET_KEY = /^[a-z][a-z0-9_]{0,39}$/;
const FACET_KEY = /^[a-z][a-z0-9_]{0,39}$/;
function queryOf(text, o = {}) {
function queryOf(text, o = {}) {
@@ -104,7 +121,23 @@ function createSearchClient(client, defaults = {}) {
@@ -104,7 +121,23 @@ function createSearchClient(client, defaults = {}) {
suggest: (text, o = {}) => call({ path: pathWith('/api/v1/suggest', queryOf(text, { owner: o.owner, type: o.type, limit: o.limit })) }, o),
suggest: (text, o = {}) => call({ path: pathWith('/api/v1/suggest', queryOf(text, { owner: o.owner, type: o.type, limit: o.limit })) }, o),
/** One document by exact id, or null (a document you may not see answers the same as a missing one). */
/** One document by exact id, or null (a document you may not see answers the same as a missing one). */
document: (owner, type, id, o = {}) => orNull(call({ path: `/api/v1/documents/${enc(owner)}/${enc(type)}/${enc(id)}` }, o)),
document: (owner, type, id, o = {}) => orNull(call({ path: `/api/v1/documents/${enc(owner)}/${enc(type)}/${enc(id)}` }, o)),
+ /** { results, sources, next_cursor } — merged public code; `deep: true` asks the cited answer (signed in). */
+ code: (text, o = {}) => (o.deep
+ ? call({ method: 'POST', path: '/api/v1/search/code/deep', json: codeBody(text, o), idempotencyKey: false }, o)
+ : call({ path: pathWith('/api/v1/search/code', codeOf(text, o)) }, o)),
+ /** Every merged code result, page after page (cursor). Deep mode is one answer, not a page stream. */
+ async *codeIterate(text, o = {}) {
+ if (o.deep) throw new TypeError('codeIterate is for pages; use code(text, { deep: true }) for an answer');
+ let cursor = o.cursor || null;
+ let seen = 0;
+ const max = Number.isFinite(o.max) ? o.max : Infinity;
+ do {
+ const page = await call({ path: pathWith('/api/v1/search/code', codeOf(text, { ...o, cursor })) }, o);
+ for (const r of page.results || []) { if (seen++ >= max) return; yield r; }
+ cursor = page.next_cursor || null;
+ } while (cursor);
+ },
};
};
}
}
-module.exports = { createSearchClient, queryOf, fabricOf };
+module.exports = { createSearchClient, queryOf, fabricOf, codeOf };
@@ -242,6 +242,18 @@ run([
@@ -242,6 +242,18 @@ run([
await srv.close();
await srv.close();
}],
}],
+ ['embedUrl: the panel address for a robot and view, from the client\'s Bot origin', async () => {
+ const bot = createBotClient(createClient({ baseUrls: { bot: 'https://openvibe.bot' } }));
+ assert.equal(bot.embedUrl(ROB), `https://openvibe.bot/panel/${ROB}/embed?mode=panel`);
+ assert.equal(bot.embedUrl(ROB, 'overlay'), `https://openvibe.bot/panel/${ROB}/embed?mode=overlay`);
+ assert.equal(bot.embedUrl(ROB, 'compact'), `https://openvibe.bot/panel/${ROB}/embed?mode=compact`);
+ const custom = createBotClient(createClient({ baseUrls: { bot: 'https://bot.example' } }), { baseUrl: 'https://bot.example/' });
+ assert.equal(custom.embedUrl(ROB, 'overlay'), `https://bot.example/panel/${ROB}/embed?mode=overlay`, 'a trailing slash is trimmed');
+ assert.equal(custom.as(USR).embedUrl(ROB), `https://bot.example/panel/${ROB}/embed?mode=panel`, 'as() keeps the origin');
+ assert.throws(() => bot.embedUrl('not-a-robot'), TypeError);
+ assert.throws(() => bot.embedUrl(ROB, 'huge'), TypeError);
+ }],
+
['browser mode: cookie credentials, no Authorization and no X-OV headers', async () => {
['browser mode: cookie credentials, no Authorization and no X-OV headers', async () => {
const srv = await server();
const srv = await server();
const seen = [];
const seen = [];
@@ -2,7 +2,7 @@
@@ -2,7 +2,7 @@
// openvibe-sdk/frame: any app mounts the network's shared navbar, footer, shipped line and themes,
// openvibe-sdk/frame: any app mounts the network's shared navbar, footer, shipped line and themes,
// with its own session endpoints; server-rendered apps get the same markup as strings.
// with its own session endpoints; server-rendered apps get the same markup as strings.
const assert = require('node:assert/strict');
const assert = require('node:assert/strict');
-const { mountFrame, frameTags, frameConfig, scriptUrls } = require('../src/frame');
+const { mountFrame, frameTags, frameConfig, scriptUrls, online } = require('../src/frame');
const opts = {
const opts = {
service: 'myapp',
service: 'myapp',
@@ -66,6 +66,9 @@ function fakeDom() {
@@ -66,6 +66,9 @@ function fakeDom() {
}
}
(async () => {
(async () => {
+ const frameEsm = await import('../esm/frame.mjs');
+ assert.equal(typeof frameEsm.online, 'function', 'the frame ESM subpath exports the online reader');
+
const d = fakeDom();
const d = fakeDom();
const r = await mountFrame({ ...opts, document: d.doc, window: d.win });
const r = await mountFrame({ ...opts, document: d.doc, window: d.win });
assert.ok(r.navbar && r.footer);
assert.ok(r.navbar && r.footer);
@@ -80,5 +83,24 @@ function fakeDom() {
@@ -80,5 +83,24 @@ function fakeDom() {
const r2 = await mountFrame({ document: d2.doc, window: d2.win });
const r2 = await mountFrame({ document: d2.doc, window: d2.win });
assert.deepEqual(r2, { navbar: null, footer: null });
assert.deepEqual(r2, { navbar: null, footer: null });
await assert.rejects(() => mountFrame({ document: null, window: null }), /browser/);
await assert.rejects(() => mountFrame({ document: null, window: null }), /browser/);
+
+ // The online read: the footer strip's data, asked for this site so its own items come back.
+ {
+ const calls = [];
+ const answer = { members: 3, guests: 4, total: 7, people: [], more: 0, live: null, counts: [], items: [], sites: 56, views_7d: 1200 };
+ const fakeFetch = async (url, init) => { calls.push({ url, init }); return { ok: true, status: 200, json: async () => answer }; };
+ assert.deepEqual(await online({ site: 'live', fetch: fakeFetch }), answer);
+ assert.equal(calls[0].url, 'https://openvibe.network/api/frame/online?site=live');
+ assert.equal(calls[0].init.credentials, 'omit');
+ assert.equal(calls[0].init.headers.accept, 'application/json');
+ await online({ site: 'not a site!', fetch: fakeFetch });
+ assert.equal(calls[1].url, 'https://openvibe.network/api/frame/online', 'an invalid id asks as no site, never in the query');
+ await online({ base: 'https://openvibe.network/', site: 'games', fetch: fakeFetch });
+ assert.equal(calls[2].url, 'https://openvibe.network/api/frame/online?site=games');
+ await assert.rejects(() => online({ fetch: async () => ({ ok: false, status: 503 }) }), /answered 503/);
+ await assert.rejects(() => online({ fetch: null }), /needs a fetch/, 'no fetch: a clear error');
+ await assert.rejects(() => online({ base: 'http://evil.example', fetch: fakeFetch }), /https origin/);
+ }
+
console.log('frame: all checks passed');
console.log('frame: all checks passed');
})().catch((e) => { console.error(e); process.exit(1); });
})().catch((e) => { console.error(e); process.exit(1); });
new file mode 100644
new file mode 100644
@@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
+'use strict';
+const assert = require('assert');
+const { createHistoryClient } = require('../src/history');
+
+(async () => {
+ const esm = await import('../esm/history.mjs');
+ assert.strictEqual(esm.createHistoryClient, createHistoryClient, 'the ESM subpath matches CommonJS');
+ const calls = [];
+ const history = createHistoryClient({ json: async (args) => { calls.push(args); return args; } }, { baseUrl: 'https://network.test' });
+ await history.list({ limit: 12, kind: 'video', cursor: 'next' });
+ assert.deepStrictEqual(calls.pop(), { service: 'network', baseUrl: 'https://network.test', path: '/api/v1/history', query: { limit: 12, kind: 'video', cursor: 'next' }, signal: undefined });
+ await history.find('a fox video');
+ assert.deepStrictEqual(calls.pop().query, { q: 'a fox video', limit: undefined });
+ await history.agentRecent({ limit: 7 });
+ assert.strictEqual(calls.pop().path, '/api/v1/history/agents/recent');
+ await history.removeDay('2026-11-10');
+ assert.strictEqual(calls.pop().path, '/api/v1/history/day/2026-11-10');
+ await history.settings({ agents_allowed: true });
+ assert.deepStrictEqual(calls.pop().json, { agents_allowed: true });
+ console.log('history SDK client: all checks passed');
+})().catch((err) => { console.error(err); process.exit(1); });
@@ -22,6 +22,8 @@ const { createInventoryClient } = require('../src/inventory');
@@ -22,6 +22,8 @@ const { createInventoryClient } = require('../src/inventory');
return json(201, { instance: { id: 'inv_01JAB2C3D4E5F6G7H8J9K0MNPQ' }, created: true });
return json(201, { instance: { id: 'inv_01JAB2C3D4E5F6G7H8J9K0MNPQ' }, created: true });
}
}
if (req.url === '/api/v1/definitions' && req.method === 'POST') return json(409, { type: 'about:blank', title: 'exhausted', status: 409, code: 'inventory.supply_exhausted' });
if (req.url === '/api/v1/definitions' && req.method === 'POST') return json(409, { type: 'about:blank', title: 'exhausted', status: 409, code: 'inventory.supply_exhausted' });
+ if (req.url.includes('/holdings/count')) return json(200, { subject: 'test', count: 2 });
+ if (req.url.includes('/holdings')) return json(200, { subject: 'test', holdings: [{ count: 1 }], next_cursor: req.url.includes('cursor=next') ? null : 'next' });
return json(200, { ok: true });
return json(200, { ok: true });
});
});
});
});
@@ -57,6 +59,15 @@ const { createInventoryClient } = require('../src/inventory');
@@ -57,6 +59,15 @@ const { createInventoryClient } = require('../src/inventory');
assert.deepStrictEqual(seen[12].body, { reason: 'refund' });
assert.deepStrictEqual(seen[12].body, { reason: 'refund' });
assert.ok(seen.every((s) => s.subject === null), 'a person\'s own client names nobody');
assert.ok(seen.every((s) => s.subject === null), 'a person\'s own client names nobody');
+ seen.length = 0;
+ assert.strictEqual((await inv.me.holdingsCount({ rarity: 'rare' })).count, 2);
+ assert.strictEqual((await inv.people.holdings(U1, { sort: 'name', limit: 60, equipped: false, tradable: true })).holdings.length, 1);
+ const pages = [];
+ for await (const page of inv.me.holdingPages({ issuer: 'service:live', limit: 1 })) pages.push(page);
+ assert.strictEqual(pages.length, 2);
+ assert(seen.some((s) => s.url.includes('/me/holdings?') && s.url.includes('cursor=next')));
+ assert(seen.some((s) => s.url.includes(`/people/${U1}/holdings?equipped=false&tradable=true&sort=name&limit=60`)));
+
// Grants: the body's idempotency_key makes a retry safe, so a 503 is retried and the item is granted once.
// Grants: the body's idempotency_key makes a retry safe, so a 503 is retried and the item is granted once.
seen.length = 0;
seen.length = 0;
const out = await inv.grants.create({ definition_id: DEF, subject: U1, idempotency_key: 'win-match-42' });
const out = await inv.grants.create({ definition_id: DEF, subject: U1, idempotency_key: 'win-match-42' });
@@ -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, 101); // levels adds the 101st subpath.
+ assert.equal(subpaths.length, 102); // history adds the 102nd subpath after levels.
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 });
@@ -89,7 +89,9 @@ function cases(label, open) {
@@ -89,7 +89,9 @@ function cases(label, open) {
outbox.kick();
outbox.kick();
await new Promise((r) => setTimeout(r, 80));
await new Promise((r) => setTimeout(r, 80));
});
});
- await new Promise((r) => setTimeout(r, 120));
+ // PGlite's claim can take longer than a fixed sleep on loaded CI hosts.
+ const deadline = Date.now() + 2000;
+ while ((inTx.length < 3 || platform.state.events.length < 1) && Date.now() < deadline) await new Promise((r) => setTimeout(r, 20));
await outbox.stop();
await outbox.stop();
assert.equal(platform.state.events.length, 1, 'the event went out');
assert.equal(platform.state.events.length, 1, 'the event went out');
assert.ok(inTx.length >= 3, `the relay kept ticking (${inTx.length} claims)`);
assert.ok(inTx.length >= 3, `the relay kept ticking (${inTx.length} claims)`);
@@ -8,14 +8,15 @@ const client = createQuestClient({ json: async (opts) => { calls.push(opts); ret
@@ -8,14 +8,15 @@ const client = createQuestClient({ json: async (opts) => { calls.push(opts); ret
await client.daily();
await client.daily();
await client.daily({ date: '2026-11-08' });
await client.daily({ date: '2026-11-08' });
await client.myDaily();
await client.myDaily();
+ await client.today();
await client.stepTypes();
await client.stepTypes();
await client.contributeStepType({ id: 'chat', event_type: 'chat.room.message.created', site: 'openvibe.chat', sentence: 'Chat', href: 'https://openvibe.chat/' });
await client.contributeStepType({ id: 'chat', event_type: 'chat.room.message.created', site: 'openvibe.chat', sentence: 'Chat', href: 'https://openvibe.chat/' });
assert.deepEqual(calls.map((x) => [x.method || 'GET', x.path]), [
assert.deepEqual(calls.map((x) => [x.method || 'GET', x.path]), [
- ['GET', '/api/v1/daily'], ['GET', '/api/v1/daily/2026-11-08'], ['GET', '/api/v1/me/daily'],
+ ['GET', '/api/v1/daily'], ['GET', '/api/v1/daily/2026-11-08'], ['GET', '/api/v1/me/daily'], ['GET', '/api/v1/quests/today'],
['GET', '/api/v1/step-types'], ['POST', '/api/v1/step-types'],
['GET', '/api/v1/step-types'], ['POST', '/api/v1/step-types'],
]);
]);
- assert.equal(calls[4].audience, 'openvibe.quest');
- assert.equal(calls[4].json.site, 'openvibe.chat');
- assert.equal(calls[4].idempotencyKey, false);
+ assert.equal(calls[5].audience, 'openvibe.quest');
+ assert.equal(calls[5].json.site, 'openvibe.chat');
+ assert.equal(calls[5].idempotencyKey, false);
process.stdout.write('quest daily SDK methods passed\n');
process.stdout.write('quest daily SDK methods passed\n');
})().catch((err) => { process.stderr.write(String(err.stack || err) + '\n'); process.exit(1); });
})().catch((err) => { process.stderr.write(String(err.stack || err) + '\n'); process.exit(1); });
@@ -3,7 +3,7 @@
@@ -3,7 +3,7 @@
const assert = require('node:assert/strict');
const assert = require('node:assert/strict');
const { stubServer, send, run } = require('./helpers');
const { stubServer, send, run } = require('./helpers');
const { createClient } = require('../src/core');
const { createClient } = require('../src/core');
-const { createSearchClient, queryOf, fabricOf } = require('../src/search');
+const { createSearchClient, queryOf, fabricOf, codeOf } = require('../src/search');
const USR = 'usr_01JAB2C3D4E5F6G7H8J9K0MNPQ';
const USR = 'usr_01JAB2C3D4E5F6G7H8J9K0MNPQ';
@@ -15,7 +15,14 @@ async function search() {
@@ -15,7 +15,14 @@ async function search() {
if (!cursor) return send(res, 200, { results: [{ owner: 'live', type: 'vod', id: '1' }, { owner: 'live', type: 'vod', id: '2' }], next_cursor: 'c2' });
if (!cursor) return send(res, 200, { results: [{ owner: 'live', type: 'vod', id: '1' }, { owner: 'live', type: 'vod', id: '2' }], next_cursor: 'c2' });
return send(res, 200, { results: [{ owner: 'live', type: 'vod', id: '3' }], next_cursor: null });
return send(res, 200, { results: [{ owner: 'live', type: 'vod', id: '3' }], next_cursor: null });
}
}
- if (u.pathname === '/api/v1/query') return send(res, 200, { results: [{ scope: 'web', kind: 'page', title: 'A', url: 'https://example.org/a', provider: 'wikipedia' }], groups: { web: 1 }, answer: null, citations: [], route: { explain: [{ scope: 'web', step: 'wikipedia', ok: true, count: 1 }], cost_usd: 0 }, next_cursor: null });
+ if (u.pathname === '/api/v1/search/code') {
+ const cursor = u.searchParams.get('cursor');
+ const hit = { source: 'github', repo: 'OpenVibers/OpenVibe.Search', path: 'server/api/query.js', ref: 'abc123', line_start: 5, line_end: 5, lines: [{ number: 5, text: 'function createSearcher() {', match: true }], language: 'JavaScript', stars: 12, licence: 'AGPL-3.0', last_commit_at: '2026-11-09T10:00:00Z', permalink: 'https://github.com/OpenVibers/OpenVibe.Search/blob/abc123/server/api/query.js#L5' };
+ if (!cursor) return send(res, 200, { query: { q: u.searchParams.get('q') }, results: [hit], sources: [{ id: 'github', kind: 'code', label: 'GitHub code search', enabled: true, health: 'ok', order: 10, calls: 1, failures: 0 }], next_cursor: 'c2' });
+ return send(res, 200, { query: { q: u.searchParams.get('q') }, results: [{ ...hit, path: 'second.js' }], sources: [], next_cursor: null });
+ }
+ if (u.pathname === '/api/v1/search/code/deep') return send(res, 200, { query: { q: 'x' }, queries: ['createSearcher'], answer: 'It is here [1].', citations: [{ n: 1, repo: 'OpenVibers/OpenVibe.Search', path: 'server/api/query.js', line_start: 5, line_end: 5, permalink: 'https://github.com/OpenVibers/OpenVibe.Search/blob/abc123/server/api/query.js#L5' }], results: [], route: { budget: { max_model_calls: 2 } } });
+ if (u.pathname === '/api/v1/query') return send(res, 200, { results: [{ scope: 'web', kind: 'page', title: 'A', url: 'https://example.org/a', provider: 'wikipedia' }], groups: { web: 1 }, answer: null, citations: [], route: { explain: [{ scope: 'web', step: 'wikipedia', ok: true, count: 1 }], cost_usd: 0 }, next_cursor: null });
if (u.pathname === '/api/v1/query/providers') return send(res, 200, { providers: [{ provider: 'deepseek', scopes: ['web'], price_usd_per_call: 0.00075, available: true, checked_at: '2026-10-28T12:00:00Z' }] });
if (u.pathname === '/api/v1/query/providers') return send(res, 200, { providers: [{ provider: 'deepseek', scopes: ['web'], price_usd_per_call: 0.00075, available: true, checked_at: '2026-10-28T12:00:00Z' }] });
if (u.pathname === '/api/v1/suggest') return send(res, 200, { suggestions: [{ owner: 'live', type: 'channel', id: '327', title: 'JapaneseOldGuy' }] });
if (u.pathname === '/api/v1/suggest') return send(res, 200, { suggestions: [{ owner: 'live', type: 'channel', id: '327', title: 'JapaneseOldGuy' }] });
if (u.pathname.startsWith('/api/v1/documents/live/channel/404')) return send(res, 404, { type: 'about:blank', code: 'search.not_found', status: 404 });
if (u.pathname.startsWith('/api/v1/documents/live/channel/404')) return send(res, 404, { type: 'about:blank', code: 'search.not_found', status: 404 });
@@ -25,6 +32,10 @@ async function search() {
@@ -25,6 +32,10 @@ async function search() {
}
}
run([
run([
+ ['ESM subpath exports code search and code iteration', async () => {
+ const esm = await import('../esm/search.mjs');
+ assert.equal(typeof esm.createSearchClient, 'function');
+ }],
['fabric: scopes go to GET /api/v1/query; query() with scopes is the same call; providers', async () => {
['fabric: scopes go to GET /api/v1/query; query() with scopes is the same call; providers', async () => {
const srv = await search();
const srv = await search();
const s = createSearchClient(createClient({ baseUrls: { search: srv.url } }));
const s = createSearchClient(createClient({ baseUrls: { search: srv.url } }));
@@ -85,4 +96,29 @@ run([
@@ -85,4 +96,29 @@ run([
assert.throws(() => createSearchClient(createClient({ baseUrls: { search: srv.url } })).query('x', { actingSubject: 'nope' }), /actingSubject/);
assert.throws(() => createSearchClient(createClient({ baseUrls: { search: srv.url } })).query('x', { actingSubject: 'nope' }), /actingSubject/);
await srv.close();
await srv.close();
}],
}],
+ ['code: a filter query, pages via codeIterate, and the cited deep answer', async () => {
+ const srv = await search();
+ const s = createSearchClient(createClient({ baseUrls: { search: srv.url } }));
+ const page = await s.code('createSearcher', { repo: 'OpenVibers/OpenVibe.Search', licence: 'AGPL-3.0', limit: 1 });
+ const u = new URL(srv.requests[0].url, 'http://x');
+ assert.equal(u.pathname, '/api/v1/search/code');
+ assert.equal(u.searchParams.get('q'), 'createSearcher');
+ assert.equal(u.searchParams.get('repo'), 'OpenVibers/OpenVibe.Search');
+ assert.equal(u.searchParams.get('licence'), 'AGPL-3.0');
+ assert.equal(u.searchParams.get('limit'), '1');
+ assert.equal(page.results[0].permalink, 'https://github.com/OpenVibers/OpenVibe.Search/blob/abc123/server/api/query.js#L5');
+ const paths = [];
+ for await (const r of s.codeIterate('createSearcher')) paths.push(r.path);
+ assert.deepEqual(paths, ['server/api/query.js', 'second.js']);
+ assert.equal(new URL(srv.requests[2].url, 'http://x').searchParams.get('cursor'), 'c2');
+ const deep = await s.code('Where?', { deep: true, repo: 'OpenVibers/OpenVibe.Search' });
+ assert.equal(deep.answer, 'It is here [1].');
+ const post = srv.requests[3];
+ assert.equal(post.method, 'POST');
+ assert.equal(JSON.parse(post.body.toString()).q, 'Where?');
+ assert.equal(JSON.parse(post.body.toString()).repo, 'OpenVibers/OpenVibe.Search');
+ assert.equal(post.headers['idempotency-key'], undefined, 'a paid run is not retried under an idempotency key');
+ assert.deepEqual(codeOf('x', { lang: 'js', limit: 2 }), { q: 'x', lang: 'js', limit: 2 });
+ await srv.close();
+ }],
]);
]);
@@ -116,12 +116,15 @@ export interface MachinesApi {
@@ -116,12 +116,15 @@ export interface MachinesApi {
chat(id: string, input: MachineChatInput, opts?: CallOptions & { ms?: number }): Promise<MachineChatResult>;
chat(id: string, input: MachineChatInput, opts?: CallOptions & { ms?: number }): Promise<MachineChatResult>;
aiModels(id: string, on: boolean, opts?: CallOptions): Promise<{ machine: Machine }>;
aiModels(id: string, on: boolean, opts?: CallOptions): Promise<{ machine: Machine }>;
}
}
+export type BotEmbedMode = 'panel' | 'overlay' | 'compact';
export interface BotClient {
export interface BotClient {
robots: RobotsApi;
robots: RobotsApi;
machines: MachinesApi;
machines: MachinesApi;
devices: DevicesApi;
devices: DevicesApi;
profiles: ProfilesApi;
profiles: ProfilesApi;
kits: KitsApi;
kits: KitsApi;
+ /** The embeddable panel's address for a robot and view (no request): https://openvibe.bot/panel/<rob_…>/embed?mode=… */
+ embedUrl(robotId: string, mode?: BotEmbedMode): string;
pair(input: PairInput, opts?: CallOptions): Promise<{ device_id: string; credential?: string; robot_id?: string; profile?: Record<string, unknown> | null; [field: string]: unknown }>;
pair(input: PairInput, opts?: CallOptions): Promise<{ device_id: string; credential?: string; robot_id?: string; profile?: Record<string, unknown> | null; [field: string]: unknown }>;
as(who: string | ActingOptions): BotClient;
as(who: string | ActingOptions): BotClient;
headers(): Record<string, string>;
headers(): Record<string, string>;
@@ -24,6 +24,7 @@ import * as jobs from './jobs';
@@ -24,6 +24,7 @@ import * as jobs from './jobs';
import * as tools from './tools';
import * as tools from './tools';
import * as projects from './projects';
import * as projects from './projects';
import * as geo from './geo';
import * as geo from './geo';
+import * as history from './history';
import * as locker from './locker';
import * as locker from './locker';
import * as trust from './trust';
import * as trust from './trust';
import * as levels from './levels';
import * as levels from './levels';
@@ -56,6 +57,7 @@ export * as jobs from './jobs';
@@ -56,6 +57,7 @@ export * as jobs from './jobs';
export * as tools from './tools';
export * as tools from './tools';
export * as projects from './projects';
export * as projects from './projects';
export * as geo from './geo';
export * as geo from './geo';
+export * as history from './history';
export * as locker from './locker';
export * as locker from './locker';
export * as trust from './trust';
export * as trust from './trust';
export * as levels from './levels';
export * as levels from './levels';
@@ -63,6 +65,6 @@ export * as levels from './levels';
@@ -63,6 +65,6 @@ export * as levels from './levels';
declare const sdk: typeof core & {
declare const sdk: typeof core & {
createWalletClient: typeof createWalletClient;
createWalletClient: typeof createWalletClient;
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; locker: typeof locker; trust: typeof trust; levels: typeof levels;
+ 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;
};
};
export default sdk;
export default sdk;
@@ -39,8 +39,29 @@ export interface FrameOptions {
@@ -39,8 +39,29 @@ export interface FrameOptions {
export interface FrameConfig { navbar: Record<string, unknown>; footer: Record<string, unknown> | null }
export interface FrameConfig { navbar: Record<string, unknown>; footer: Record<string, unknown> | null }
export interface FrameUrls { theme: string; navbar: string; footer: string; shipped: string }
export interface FrameUrls { theme: string; navbar: string; footer: string; shipped: string }
+/** One person the network may name: only when their presence setting lets others see them online. */
+export interface FrameOnlinePerson { name: string; username: string; avatar: string | null; site: string | null }
+/** A count that opens what it counts, or a site's own strip item (a number or a fixed line). */
+export interface FrameOnlineItem { label: string; icon: string; link: string; value?: number; line?: string }
+/** GET <base>/api/frame/online — who was on the network in the last thirty minutes. A guest is a number only. */
+export interface FrameOnline {
+ updated: string | null;
+ members: number | null;
+ guests: number | null;
+ total: number | null;
+ people: FrameOnlinePerson[];
+ more: number;
+ live: { channels: number; watching: number; first: Array<{ name: string; url: string }> } | null;
+ counts: Array<{ id?: string; label: string; value: number; icon: string; url: string }>;
+ items: FrameOnlineItem[];
+ sites: number | null;
+ views_7d: number | null;
+}
+
export const DEFAULT_BASE: string;
export const DEFAULT_BASE: string;
export function scriptUrls(opts?: { base?: string }): FrameUrls;
export function scriptUrls(opts?: { base?: string }): FrameUrls;
export function frameConfig(opts?: FrameOptions): FrameConfig;
export function frameConfig(opts?: FrameOptions): FrameConfig;
export function frameTags(opts?: FrameOptions): { head: string; bodyStart: string; bodyEnd: string; config: FrameConfig; urls: FrameUrls };
export function frameTags(opts?: FrameOptions): { head: string; bodyStart: string; bodyEnd: string; config: FrameConfig; urls: FrameUrls };
export function mountFrame(opts?: FrameOptions): Promise<{ navbar: unknown | null; footer: unknown | null }>;
export function mountFrame(opts?: FrameOptions): Promise<{ navbar: unknown | null; footer: unknown | null }>;
+/** The footer strip's read; `site` (the calling site's Frame id) brings that site's own items. */
+export function online(opts?: { base?: string; site?: string; signal?: AbortSignal; fetch?: typeof fetch }): Promise<FrameOnline>;
new file mode 100644
new file mode 100644
@@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
+export interface HistoryRow {
+ id: number; type: string; title: string; url: string; image_url?: string | null;
+ service: string | null; service_label: string | null; created_at: string; updated_at: string;
+}
+export interface HistoryList { items: HistoryRow[]; total: number; next_cursor: string | null; keep_history: boolean; agents_allowed: boolean; }
+export declare function createHistoryClient(client: unknown, options?: { baseUrl?: string }): {
+ list(query?: { cursor?: string; limit?: number; day?: string; site?: string; kind?: string; q?: string }, options?: { signal?: AbortSignal }): Promise<HistoryList>;
+ find(q: string, options?: { limit?: number; signal?: AbortSignal }): Promise<{ query: string; items: HistoryRow[] }>;
+ summary(period?: 'day' | 'week', options?: { signal?: AbortSignal }): Promise<{ period: 'day' | 'week'; items: Array<{ kind: string; title: string }>; text: string }>;
+ agentRecent(query?: { limit?: number }, options?: { signal?: AbortSignal }): Promise<{ items: HistoryRow[] }>;
+ settings(patch: { keep_history?: boolean; agents_allowed?: boolean }, options?: { signal?: AbortSignal }): Promise<{ keep_history: boolean; agents_allowed: boolean }>;
+ remove(id: number | string, options?: { signal?: AbortSignal }): Promise<{ ok: boolean }>;
+ removeDay(day: string, options?: { signal?: AbortSignal }): Promise<{ deleted: number }>;
+ removeSite(site: string, options?: { signal?: AbortSignal }): Promise<{ deleted: number }>;
+ clear(options?: { signal?: AbortSignal }): Promise<{ deleted: number }>;
+};
@@ -58,6 +58,7 @@ export * as play from './play';
@@ -58,6 +58,7 @@ export * as play from './play';
export * as download from './download';
export * as download from './download';
export * as openre from './openre';
export * as openre from './openre';
export * as geo from './geo';
export * as geo from './geo';
+export * as history from './history';
export * as usage from './usage';
export * as usage from './usage';
export * as commerce from './commerce';
export * as commerce from './commerce';
export * as moneyReasons from './money-reasons';
export * as moneyReasons from './money-reasons';
@@ -9,6 +9,9 @@ export interface Definition {
@@ -9,6 +9,9 @@ export interface Definition {
}
}
export interface DefinitionRequest { kind: string; name: string; description?: string; rarity?: Rarity; art?: Definition['art']; attributes?: Record<string, unknown>; supply_cap?: number; [k: string]: unknown }
export interface DefinitionRequest { kind: string; name: string; description?: string; rarity?: Rarity; art?: Definition['art']; attributes?: Record<string, unknown>; supply_cap?: number; [k: string]: unknown }
export interface Instance { id: string; definition_id: string; owner_subject?: string; state?: 'owned' | 'consumed' | 'revoked'; serial?: number | null; [k: string]: unknown }
export interface Instance { id: string; definition_id: string; owner_subject?: string; state?: 'owned' | 'consumed' | 'revoked'; serial?: number | null; [k: string]: unknown }
+export interface Holding { definition: Definition; instance: Instance; count: number; equipped: boolean; tradable: boolean; kind_slots: string[] }
+export interface HoldingsPage { subject: string; holdings: Holding[]; next_cursor: string | null }
+export type HoldingsOptions = Opts & { issuer?: string; site?: string; kind?: string; rarity?: Rarity; equipped?: boolean; tradable?: boolean; text?: string; sort?: 'newest' | 'name' | 'rarity' | 'count'; cursor?: string; limit?: number };
export interface EquippedSet { subject: string; slots: Record<string, { instance_id: string; definition_id: string; token?: string; media_id?: string }>; updated_at: string | null }
export interface EquippedSet { subject: string; slots: Record<string, { instance_id: string; definition_id: string; token?: string; media_id?: string }>; updated_at: string | null }
export interface InventoryClient {
export interface InventoryClient {
kinds: { list(opts?: Opts): Promise<{ kinds: Kind[] }>; get(kindId: string, opts?: Opts): Promise<{ kind: Kind }> };
kinds: { list(opts?: Opts): Promise<{ kinds: Kind[] }>; get(kindId: string, opts?: Opts): Promise<{ kind: Kind }> };
@@ -19,12 +22,18 @@ export interface InventoryClient {
@@ -19,12 +22,18 @@ export interface InventoryClient {
update(id: string, patch: Partial<DefinitionRequest>, opts?: Opts): Promise<{ definition: Definition }>;
update(id: string, patch: Partial<DefinitionRequest>, opts?: Opts): Promise<{ definition: Definition }>;
};
};
people: {
people: {
- items(subject: string, opts?: Opts & { kind?: string; cursor?: string; limit?: number }): Promise<{ items: Instance[]; next: string | null; [k: string]: unknown }>;
+ items(subject: string, opts?: Opts & { kind?: string; cursor?: string; limit?: number }): Promise<{ subject: string; instances: Instance[]; definitions: Record<string, Definition>; next_cursor: string | null }>;
+ holdings(subject: string, opts?: HoldingsOptions): Promise<HoldingsPage>;
+ holdingsCount(subject: string, opts?: HoldingsOptions): Promise<{ subject: string; count: number }>;
+ holdingPages(subject: string, opts?: HoldingsOptions): AsyncIterable<HoldingsPage>;
equipped(subject: string, opts?: Opts): Promise<EquippedSet>;
equipped(subject: string, opts?: Opts): Promise<EquippedSet>;
};
};
equipped(subjects: string[], opts?: Opts): Promise<{ equipped: EquippedSet[] }>;
equipped(subjects: string[], opts?: Opts): Promise<{ equipped: EquippedSet[] }>;
me: {
me: {
- items(opts?: Opts & { kind?: string; cursor?: string; limit?: number }): Promise<{ items: Instance[]; next: string | null; [k: string]: unknown }>;
+ items(opts?: Opts & { kind?: string; cursor?: string; limit?: number }): Promise<{ subject: string; instances: Instance[]; definitions: Record<string, Definition>; next_cursor: string | null }>;
+ holdings(opts?: HoldingsOptions): Promise<HoldingsPage>;
+ holdingsCount(opts?: HoldingsOptions): Promise<{ subject: string; count: number }>;
+ holdingPages(opts?: HoldingsOptions): AsyncIterable<HoldingsPage>;
equip(req: { kind: string; slot: string; instance_id: string | null }, opts?: Opts): Promise<EquippedSet>;
equip(req: { kind: string; slot: string; instance_id: string | null }, opts?: Opts): Promise<EquippedSet>;
};
};
grants: { create(req: { definition_id: string; subject: string; idempotency_key: string; [k: string]: unknown }, opts?: Opts): Promise<{ instance: Instance; created: boolean }> };
grants: { create(req: { definition_id: string; subject: string; idempotency_key: string; [k: string]: unknown }, opts?: Opts): Promise<{ instance: Instance; created: boolean }> };
@@ -16,6 +16,7 @@ export interface AppQuest {
@@ -16,6 +16,7 @@ export interface AppQuest {
}
}
export interface QuestClient {
export interface QuestClient {
daily(opts?: { date?: string; signal?: AbortSignal }): Promise<DailyQuest>;
daily(opts?: { date?: string; signal?: AbortSignal }): Promise<DailyQuest>;
+ today(opts?: { signal?: AbortSignal }): Promise<DailyProgress>;
myDaily(opts?: { signal?: AbortSignal }): Promise<DailyProgress>;
myDaily(opts?: { signal?: AbortSignal }): Promise<DailyProgress>;
stepTypes(opts?: { signal?: AbortSignal }): Promise<{ step_types: QuestStepType[] }>;
stepTypes(opts?: { signal?: AbortSignal }): Promise<{ step_types: QuestStepType[] }>;
contributeStepType(stepType: DailyStepTypeRequest, opts?: { signal?: AbortSignal }): Promise<QuestStepType>;
contributeStepType(stepType: DailyStepTypeRequest, opts?: { signal?: AbortSignal }): Promise<QuestStepType>;
@@ -58,6 +58,34 @@ export interface ProviderOffer {
@@ -58,6 +58,34 @@ export interface ProviderOffer {
available: boolean; reason?: string | null; daily_cap?: number | null; used_today?: number | null;
available: boolean; reason?: string | null; daily_cap?: number | null; used_today?: number | null;
latency_ms_p50?: number | null; answered_rate?: number | null; terms_url?: string | null; checked_at: string;
latency_ms_p50?: number | null; answered_rate?: number | null; terms_url?: string | null; checked_at: string;
}
}
+export interface CodeLine { number: number | null; text: string; match: boolean; }
+export interface CodeResult {
+ source: string; sources?: string[]; repo: string; repo_url?: string | null; path: string; ref?: string | null;
+ line_start: number | null; line_end: number | null; lines: CodeLine[];
+ language?: string | null; stars: number; licence?: string | null;
+ last_commit_at?: string | null; last_commit_source?: string | null; fetched_at?: string | null;
+ permalink: string; exact_identifier?: boolean;
+}
+export interface CodeSource {
+ id: string; kind: 'code'; label: string; cost: string;
+ key?: { name: string; required: boolean; configured: boolean };
+ quota?: { per_minute: number | null; window: string };
+ enabled: boolean; health: string; order: number; calls: number; failures: number;
+ last_ok_at?: string | null; last_error_at?: string | null; last_error?: string | null; last_latency_ms?: number | null;
+}
+export interface CodePage { query: Record<string, unknown>; results: CodeResult[]; sources: CodeSource[]; next_cursor: string | null; took_ms?: number; }
+export interface CodeOptions {
+ lang?: string; repo?: string; licence?: string; limit?: number; cursor?: string | null;
+ actingSubject?: string; signal?: AbortSignal;
+}
+export interface CodeCitation {
+ n: number; repo: string; path: string; line_start: number | null; line_end: number | null;
+ permalink: string; language?: string | null; stars?: number; licence?: string | null;
+}
+export interface CodeDeepAnswer {
+ query: Record<string, unknown>; queries: string[]; answer: string;
+ citations: CodeCitation[]; results: CodeResult[]; route: Record<string, unknown>;
+}
export interface SearchClient {
export interface SearchClient {
query(text: string, opts?: SearchOptions): Promise<SearchPage>;
query(text: string, opts?: SearchOptions): Promise<SearchPage>;
/** With scopes: the Search Fabric. */
/** With scopes: the Search Fabric. */
@@ -67,7 +95,13 @@ export interface SearchClient {
@@ -67,7 +95,13 @@ export interface SearchClient {
iterate(text: string, opts?: SearchOptions & { max?: number }): AsyncGenerator<SearchDocument, void, unknown>;
iterate(text: string, opts?: SearchOptions & { max?: number }): AsyncGenerator<SearchDocument, void, unknown>;
suggest(text: string, opts?: Pick<SearchOptions, 'owner' | 'type' | 'limit' | 'actingSubject' | 'signal'>): Promise<{ suggestions: Array<{ owner: string; type: string; id: string; title: string; canonical_url?: string }> }>;
suggest(text: string, opts?: Pick<SearchOptions, 'owner' | 'type' | 'limit' | 'actingSubject' | 'signal'>): Promise<{ suggestions: Array<{ owner: string; type: string; id: string; title: string; canonical_url?: string }> }>;
document(owner: string, type: string, id: string, opts?: Pick<SearchOptions, 'actingSubject' | 'signal'>): Promise<SearchDocument | null>;
document(owner: string, type: string, id: string, opts?: Pick<SearchOptions, 'actingSubject' | 'signal'>): Promise<SearchDocument | null>;
+ /** Merged, de-duplicated public code from the hosted sources; one page per call. */
+ code(text: string, opts: CodeOptions & { deep: true }): Promise<CodeDeepAnswer>;
+ code(text: string, opts?: CodeOptions): Promise<CodePage>;
+ /** Every code result, page after page. Deep mode is not a page stream. */
+ codeIterate(text: string, opts?: CodeOptions & { max?: number }): AsyncGenerator<CodeResult, void, unknown>;
}
}
export function createSearchClient(client: OpenVibeClient, defaults?: { baseUrl?: string; actingSubject?: string }): SearchClient;
export function createSearchClient(client: OpenVibeClient, defaults?: { baseUrl?: string; actingSubject?: string }): SearchClient;
export function queryOf(text: string, opts?: SearchOptions): Record<string, string | string[] | number>;
export function queryOf(text: string, opts?: SearchOptions): Record<string, string | string[] | number>;
export function fabricOf(text: string, opts?: FabricOptions): Record<string, string | number>;
export function fabricOf(text: string, opts?: FabricOptions): Record<string, string | number>;
+export function codeOf(text: string, opts?: CodeOptions): Record<string, string | number>;