openvibers/openvibe.sdk Commits
openvibe-sdk 0.155.0: design.icons.suggest, and public get/search (#237)
Changes
@@@ -1,5 +1,14 @@# Changelog++## 0.155.0 — 2026-11-12++++- Add `design.icons.suggest(label, { context, limit, rank })` to `openvibe-sdk/design`: the best glyphs for a button's++ words by meaning (the glyph's name, its META keywords and its other-set names), with `rank: true` asking a small model++ to order the shortlist when the words are new. The answer is the same shape as `GET /api/v1/glyphs/suggest`, including++ `new`, `ranked`, `model` and, when known, `draft_price`.++- `design.icons.get()` and `.search()` are public now, like the Design API they call: no access token. `.draft()` still++ needs one, and the client still sends a token when it has one.++## 0.154.0 — 2026-11-12- Add the Frame's data client to `openvibe-sdk/frame` (and `sdk.frame` in the root, browser and ESM entries):@@@ -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.154.0++npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.155.0```> Supported browser and server clients for the OpenVibe platform.--**Status:** alpha, v0.154.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.155.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.154.0++npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.155.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).@@@ -3,19 +3,20 @@"stage": "alpha","code": true,"wave": "W2",-- "version": "0.154.0",-- "plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30",++ "version": "0.155.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","contracts": "openvibe-contracts v0.410.0","features": [++ "design.icons.suggest: the best glyphs for a button's words by meaning, with rank:true asking a small model when the words are new; design.icons.get/search are public and draft keeps its token (0.155.0)","ui-layout (openvibe-sdk/ui-layout, also sdk.uiLayout): validate(catalog, layout) returns { ok, reasons[] } in plain words for required parts, 44 px pressed targets, coverage, theme-token tints, styles, bindings and data-only content; applyDefaults(catalog, device?) and merge(layout, patch) build and patch an arrangement without mutating it","menu (openvibe-sdk/menu): menuCard() builds one validated frame.menu-card@1 (plain text, the contract caps, https links on the site own origin or an openvibe.* host, an optional item picture) and menuRoute() is the Express/node:http GET /.well-known/openvibe-menu.json handler (public cache, CORS *, [] and one log line when the cards cannot be built)","cam (openvibe-sdk/cam): defineOvCamera({ src }) injects the <ov-camera> element from openvibe.cam once and resolves to its class, with typed attributes, events and methods (start/stop/capture/flip); browser-only, upload=\"off\" by default","pics (openvibe-sdk/pics): the public gallery and one picture's reads, private uploads with resumable chunks, the owner's explicit publication, likes and albums on OpenVibe.Pics; reads are public, writes take a person's Network token","streams (openvibe-sdk/streams, S7): schedule(), listSchedules(), cancelSchedule() and remindSchedule() for scheduled public agent streams; postStep() and steps.post() from any harness; digest.read() and poll.open()/poll.result() for a Co-pilot service token; onControl() for pause, curtain, mute and end","reports (openvibe-sdk/reports, S7): write() a session report with the person's own token, private by default and idempotent per source and task ref, and read() one the caller may see; examples/cli-agent-stream.js streams any CLI command's run with steps",-- "chat-apps (openvibe-sdk/chat-apps): the three chat apps (Telegram, Discord, Slack) in one place — the delivery checks (Telegram's secret token, Slack's v0 signing, Discord's Ed25519), the button callback (signed once, used once, expiring), each app's payload as the same shape and splitText() for a long answer",++ "chat-apps (openvibe-sdk/chat-apps): the three chat apps (Telegram, Discord, Slack) in one place \u2014 the delivery checks (Telegram's secret token, Slack's v0 signing, Discord's Ed25519), the button callback (signed once, used once, expiring), each app's payload as the same shape and splitText() for a long answer","exchange (openvibe-sdk/exchange): the buyer side (chat, models, offers, presets, usage, receipts) and the seller side (offer, endpointOffers, removeEndpointOffer, earnings, verification) of OpenVibe.Exchange, with the platform webhook signature check; an offer is a Link machine's Network report or a standalone OpenAI-compatible endpoint registered on the public seller API","locker (openvibe-sdk/locker, openvibe-sdk/locker/server): the Locker envelope (locker.envelope@1) for end-to-end encryption, WebCrypto only, in Node and the browser: seal/open for text and bytes, sealStream/openStream, the three locks (people, password, link), person key pairs (ECDH P-256, ECDSA P-256), the eight-word fingerprint, wrapFor and rewrap, recoveryWords for the phrase to write down, opening a people lock by key, inspect for what an envelope says about itself, and the header checks a service runs without a key","the 1.0 core surface is frozen now, before 1.0 (core, auth, events, db, media): docs/one-point-oh.json gives every export its tier, test/surface.test.js pins the exact names, and removing or renaming one needs a major release; createCodeVerifier and pkceChallenge left in 0.86.0",@@@ -39,7 +40,7 @@"valkey, cache, queue, pubsub and a shared limits store on Valkey 9, each with an in-process twin for tests","commerce (openvibe-sdk/commerce): the shared Billing client for Tips and VIP, with intentKey and receipt","events: the PostgreSQL outbox (lease-claimed, several relays per table) and inbox (handler in its transaction)",-- "usage: createUsageReporter (openvibe-sdk/usage), the shared platform.usage-sample@1 reporter — validated readings queued idempotency-keyed in an outbox and relayed to Billing with retry, never dropped, refused readings kept",++ "usage: createUsageReporter (openvibe-sdk/usage), the shared platform.usage-sample@1 reporter \u2014 validated readings queued idempotency-keyed in an outbox and relayed to Billing with retry, never dropped, refused readings kept","the HTTP telemetry kit (openvibe-sdk/service): the platform.telemetry-sample@1 middleware aggregated per route|method|status_class per flush, with active-requests, p95 and event-loop-lag gauges, lifted from Network's server/telemetry.js and server/observability.js","the telemetry sink (openvibe-sdk/telemetry): telemetrySchema() DDL, createPgTelemetrySink() and pruneTelemetry(), a per-service PostgreSQL table every service stores platform.telemetry-sample@1 into","account-data (openvibe-sdk/account-data, ADR-033): telemetry_samples covered once by the SDK (exported as telemetry.json, anonymized on deletion), and uncovered() listing every table with a person column that no table map, task-keyed list or reasoned exemption covers",@@@ -48,8 +49,8 @@"the AI Exchange face of the one planner (openvibe-sdk/placement planAi, off by default): the hard gates (exact artifact, commercial right, privacy class, trust tier, capabilities, context, health, cost ceiling), the expected cost per success in integer nano-USD, sparse quality evidence marked and not trusted, and an explanation that never carries a secret or a host; the money math in openvibe-sdk/cost (nanoUsd, chargeTokens, expectedCostNano, costPerSuccessNano)","consent (openvibe-sdk/consent): the L24 check before any use of a person's content or likeness for AI or data; allowed() and filter() (batches of 500), onChange() drops a subject's answers on network.consent.changed, mock() for tests; fails closed, caches 60 s, server-side","openvibe-sdk/machines: the network's machines hub (list, get, catalogue, pair, rename, roles, revoke) against OpenVibe.Network, for a person and for the apps they consented to",-- "vibes (openvibe-sdk/vibes): Vibes Pay charges (create with the app reference as the Idempotency-Key, get, refund) and constant-time webhook verification (X-OpenVibe-Signature-V2, ±300 s) for developer apps",-- "streams (openvibe-sdk/streams): the Agent Streams plan S1 — start an agent stream on OpenVibe.Live (live.agent-stream@1) with its publish ticket, renew and revoke that ticket (the kill switch), poll state, post the last frame, set the curtain, switch visibility, end it and mint the view ticket; the publish ticket is a bearer of its own on the desk routes"++ "vibes (openvibe-sdk/vibes): Vibes Pay charges (create with the app reference as the Idempotency-Key, get, refund) and constant-time webhook verification (X-OpenVibe-Signature-V2, \u00b1300 s) for developer apps",++ "streams (openvibe-sdk/streams): the Agent Streams plan S1 \u2014 start an agent stream on OpenVibe.Live (live.agent-stream@1) with its publish ticket, renew and revoke that ticket (the kill switch), poll state, post the last frame, set the curtain, switch visibility, end it and mint the view ticket; the publish ticket is a bearer of its own on the desk routes"],"updated": "2026-11-12"}@@@ -1,4 +1,4 @@--// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.154.0, MIT). Do not edit.++// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.155.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, composer, cam, mediaUpload, ui, uiLayout, community, homeFeed, feed, bot, search, bench, meme, vision, themes, elements, design, redact, chat, ai, credentials, jobs, agents, frame, tools, projects, geo, history, userCard, player, agentSheet). No server-only code: no service tokens, JWT verification, events, identity./* eslint-disable */@@@ -67,7 +67,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.154.0';++const SDK_VERSION = '0.155.0';module.exports = {createClient,@@@ -5473,8 +5473,8 @@ function (module, exports) {'use strict';/**-- * OpenVibe.Design: the public component registry (design.component@1; no token) and the glyph API (icons; a token).-- * Browser-safe.++ * OpenVibe.Design: the public component registry (design.component@1; no token) and the glyph API (icons; suggest,++ * get and search are public, a draft needs a token). Browser-safe.*/const DEFAULT_DESIGN_URL = 'https://openvibe.design';const STYLES = new Set(['thin', 'light', 'regular', 'bold', 'solid', 'duotone']);@@@ -5512,12 +5512,20 @@ function createDesignClient({ accessToken = null, baseUrl = DEFAULT_DESIGN_URL,async get(name, { style = 'regular', signal } = {}) {if (!/^[a-z][a-z0-9-]{1,31}$/.test(String(name || ''))) throw new TypeError('icons.get: name must be lowercase letters, digits or dashes');if (!STYLES.has(style)) throw new TypeError('icons.get: unsupported style');-- return request(`/api/v1/glyphs/${encodeURIComponent(name)}?style=${encodeURIComponent(style)}`, { signal });++ return request(`/api/v1/glyphs/${encodeURIComponent(name)}?style=${encodeURIComponent(style)}`, { signal, open: true });},async search(q, { limit = 24, signal } = {}) {const query = String(q || '').trim();if (!query) throw new TypeError('icons.search: q is required');-- return request(`/api/v1/glyphs/search?q=${encodeURIComponent(query)}&limit=${encodeURIComponent(Math.min(Math.max(Number(limit) || 24, 1), 60))}`, { signal });++ return request(`/api/v1/glyphs/search?q=${encodeURIComponent(query)}&limit=${encodeURIComponent(Math.min(Math.max(Number(limit) || 24, 1), 60))}`, { signal, open: true });++ },++ /** The best glyphs for a button's words by meaning; rank asks a small model when the words are new. Public. */++ async suggest(label, { context = '', limit = 8, rank = false, signal } = {}) {++ const words = String(label || '').trim();++ if (!words) throw new TypeError('icons.suggest: label is required');++ const n = Math.min(Math.max(Number(limit) || 8, 1), 24);++ const query = `label=${encodeURIComponent(words)}&limit=${encodeURIComponent(n)}${context ? `&context=${encodeURIComponent(String(context).trim().slice(0, 160))}` : ''}${rank ? '&rank=1' : ''}`;++ return request(`/api/v1/glyphs/suggest?${query}`, { signal, open: true });},});return Object.freeze({@@@ -1,12 +1,12 @@{"name": "openvibe-sdk",-- "version": "0.154.0",++ "version": "0.155.0","lockfileVersion": 3,"requires": true,"packages": {"": {"name": "openvibe-sdk",-- "version": "0.154.0",++ "version": "0.155.0","license": "MIT","bin": {"openvibe-download": "bin/openvibe-download.js"@@@ -1,6 +1,6 @@{"name": "openvibe-sdk",-- "version": "0.154.0",++ "version": "0.155.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",@@@ -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.154.0';++const SDK_VERSION = '0.155.0';module.exports = {createClient,@@@ -1,8 +1,8 @@'use strict';/**-- * OpenVibe.Design: the public component registry (design.component@1; no token) and the glyph API (icons; a token).-- * Browser-safe.++ * OpenVibe.Design: the public component registry (design.component@1; no token) and the glyph API (icons; suggest,++ * get and search are public, a draft needs a token). Browser-safe.*/const DEFAULT_DESIGN_URL = 'https://openvibe.design';const STYLES = new Set(['thin', 'light', 'regular', 'bold', 'solid', 'duotone']);@@@ -40,12 +40,20 @@ function createDesignClient({ accessToken = null, baseUrl = DEFAULT_DESIGN_URL,async get(name, { style = 'regular', signal } = {}) {if (!/^[a-z][a-z0-9-]{1,31}$/.test(String(name || ''))) throw new TypeError('icons.get: name must be lowercase letters, digits or dashes');if (!STYLES.has(style)) throw new TypeError('icons.get: unsupported style');-- return request(`/api/v1/glyphs/${encodeURIComponent(name)}?style=${encodeURIComponent(style)}`, { signal });++ return request(`/api/v1/glyphs/${encodeURIComponent(name)}?style=${encodeURIComponent(style)}`, { signal, open: true });},async search(q, { limit = 24, signal } = {}) {const query = String(q || '').trim();if (!query) throw new TypeError('icons.search: q is required');-- return request(`/api/v1/glyphs/search?q=${encodeURIComponent(query)}&limit=${encodeURIComponent(Math.min(Math.max(Number(limit) || 24, 1), 60))}`, { signal });++ return request(`/api/v1/glyphs/search?q=${encodeURIComponent(query)}&limit=${encodeURIComponent(Math.min(Math.max(Number(limit) || 24, 1), 60))}`, { signal, open: true });++ },++ /** The best glyphs for a button's words by meaning; rank asks a small model when the words are new. Public. */++ async suggest(label, { context = '', limit = 8, rank = false, signal } = {}) {++ const words = String(label || '').trim();++ if (!words) throw new TypeError('icons.suggest: label is required');++ const n = Math.min(Math.max(Number(limit) || 8, 1), 24);++ const query = `label=${encodeURIComponent(words)}&limit=${encodeURIComponent(n)}${context ? `&context=${encodeURIComponent(String(context).trim().slice(0, 160))}` : ''}${rank ? '&rank=1' : ''}`;++ return request(`/api/v1/glyphs/suggest?${query}`, { signal, open: true });},});return Object.freeze({@@@ -11,16 +11,28 @@ const { createDesignClient } = require('../src/design');await client.icons.draft({ name: 'hot-tub', styles: ['thin', 'solid'], sourceSvg: '<path d="M5 12h14"/>' });await client.icons.get('hot-tub', { style: 'duotone' });await client.icons.search('hot tub', { limit: 5 });++ await client.icons.suggest('Deep dive', { context: 'a menu', limit: 5, rank: true });assert.equal(calls[0].init.headers.authorization, 'Bearer test-token');assert.deepEqual(JSON.parse(calls[0].init.body).styles, ['thin', 'solid']);assert.equal(JSON.parse(calls[0].init.body).sourceSvg, '<path d="M5 12h14"/>');assert.match(calls[1].url, /\/api\/v1\/glyphs\/hot-tub\?style=duotone$/);assert.match(calls[2].url, /\/api\/v1\/glyphs\/search\?q=hot%20tub&limit=5$/);++ assert.match(calls[3].url, /\/api\/v1\/glyphs\/suggest\?label=Deep%20dive&limit=5&context=a%20menu&rank=1$/);++ assert.equal(calls[3].init.headers.authorization, 'Bearer test-token', 'a signed-in suggestion is ranked on the person allowance');await assert.rejects(client.icons.get('bad name'), TypeError);++ await assert.rejects(client.icons.suggest(' '), TypeError);const esm = await import('../esm/index.mjs');assert.equal(typeof esm.design.createDesignClient, 'function', 'the ESM root exposes the Design client');++ // suggest, get and search are public: no token, no Authorization header.++ const pubCalls = [];++ const pub = createDesignClient({ fetchImpl: async (url, init) => { pubCalls.push({ url: String(url), init }); return { ok: true, async json() { return { glyphs: [] }; } }; } });++ await pub.icons.suggest('go live');++ await pub.icons.get('heart');++ await pub.icons.search('gear');++ assert.ok(pubCalls.every((c) => !c.init.headers.authorization), 'public glyph calls carry no token');++ await assert.rejects(pub.icons.draft({ name: 'hot-tub' }), /needs accessToken/);const designEsm = await import('../esm/design.mjs');const esmClient = designEsm.createDesignClient({ accessToken: 'test-token', fetchImpl: async () => ({ ok: true, async json() { return {}; } }) });await esmClient.icons.get('heart', { style: 'bold' });-- console.log('Design SDK icons: draft, get and search use the authenticated glyph API');++ console.log('Design SDK icons: draft uses the authenticated API; suggest, get and search are public');})().catch((err) => { console.error(err); process.exit(1); });@@@ -38,7 +38,7 @@ run([const signed = createDesignClient({ baseUrl: srv.url, accessToken: 'user-jwt' });await signed.components();assert.equal(srv.requests[3].headers.authorization, 'Bearer user-jwt', 'a token, when given, goes along');-- await assert.rejects(() => d.icons.get('heart'), /needs accessToken/, 'the icon API still needs a token');++ await assert.rejects(() => d.icons.draft({ name: 'hot-tub' }), /needs accessToken/, 'a glyph draft still needs a token');await srv.close();}],['a bad name is refused before a request; a server error still throws', async () => {@@@ -2,6 +2,17 @@ export type GlyphStyle = 'thin' | 'light' | 'regular' | 'bold' | 'solid' | 'duotexport interface DesignGlyph { name: string; style: GlyphStyle; svg: string; solid: string; strokeWidth: number | null; underlay: string; underlayOpacity: number; }export interface DesignGlyphReference { name: string; set: string; setName: string; author: string; license: string; licenseUrl: string; preview: string; }export interface DesignGlyphDraft { id: string; name: string; svg: string; solid_svg: string | null; styles: GlyphStyle[]; references: DesignGlyphReference[]; status: string; }++export interface DesignGlyphSuggestion { name: string; score: number; tags: string[]; why: string[]; }++export interface DesignGlyphSuggestionResult {++ label: string;++ context: string;++ glyphs: DesignGlyphSuggestion[];++ new: boolean;++ ranked: boolean;++ model: string | null;++ cached?: boolean;++ draft_price?: { usd: number | null; model: string | null; provider: string | null; tokens: { in: number; out: number }; note: string };++}export interface DesignComponent {schema: 'design.component@1';name: string;@@@ -21,12 +32,13 @@ export interface DesignComponent {examples: Array<{ title: string; props: Record<string, unknown> }>;}--/** accessToken is needed for icons only; the component registry is public. */++/** accessToken is needed for icons.draft only; suggest, get, search and the component registry are public. */export declare function createDesignClient(options?: { accessToken?: string; baseUrl?: string; fetchImpl?: typeof fetch; timeoutMs?: number }): {icons: {draft(input: { name: string; sentence?: string; styles?: GlyphStyle[]; sourceSvg?: string }, opts?: { signal?: AbortSignal }): Promise<{ kept: DesignGlyphDraft[]; refused: string[] }>;get(name: string, opts?: { style?: GlyphStyle; signal?: AbortSignal }): Promise<DesignGlyph>;search(q: string, opts?: { limit?: number; signal?: AbortSignal }): Promise<{ query: string; glyphs: DesignGlyph[] }>;++ suggest(label: string, opts?: { context?: string; limit?: number; rank?: boolean; signal?: AbortSignal }): Promise<DesignGlyphSuggestionResult>;};components(opts?: { signal?: AbortSignal }): Promise<{ schema: 'design.component@1'; components: DesignComponent[]; kitComponents: unknown[] }>;component(name: string, opts?: { signal?: AbortSignal }): Promise<{ schema: 'design.component@1'; component: DesignComponent; examples: DesignComponent['examples'] } | null>;
@@ -1,5 +1,14 @@
@@ -1,5 +1,14 @@
# Changelog
# Changelog
+## 0.155.0 — 2026-11-12
+
+- Add `design.icons.suggest(label, { context, limit, rank })` to `openvibe-sdk/design`: the best glyphs for a button's
+ words by meaning (the glyph's name, its META keywords and its other-set names), with `rank: true` asking a small model
+ to order the shortlist when the words are new. The answer is the same shape as `GET /api/v1/glyphs/suggest`, including
+ `new`, `ranked`, `model` and, when known, `draft_price`.
+- `design.icons.get()` and `.search()` are public now, like the Design API they call: no access token. `.draft()` still
+ needs one, and the client still sends a token when it has one.
+
## 0.154.0 — 2026-11-12
## 0.154.0 — 2026-11-12
- Add the Frame's data client to `openvibe-sdk/frame` (and `sdk.frame` in the root, browser and ESM entries):
- Add the Frame's data client to `openvibe-sdk/frame` (and `sdk.frame` in the root, browser and ESM entries):
@@ -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.154.0
+npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.155.0
```
```
> Supported browser and server clients for the OpenVibe platform.
> Supported browser and server clients for the OpenVibe platform.
-**Status:** alpha, v0.154.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.155.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.154.0
+npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.155.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).
@@ -3,19 +3,20 @@
@@ -3,19 +3,20 @@
"stage": "alpha",
"stage": "alpha",
"code": true,
"code": true,
"wave": "W2",
"wave": "W2",
- "version": "0.154.0",
- "plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30",
+ "version": "0.155.0",
+ "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",
"contracts": "openvibe-contracts v0.410.0",
"contracts": "openvibe-contracts v0.410.0",
"features": [
"features": [
+ "design.icons.suggest: the best glyphs for a button's words by meaning, with rank:true asking a small model when the words are new; design.icons.get/search are public and draft keeps its token (0.155.0)",
"ui-layout (openvibe-sdk/ui-layout, also sdk.uiLayout): validate(catalog, layout) returns { ok, reasons[] } in plain words for required parts, 44 px pressed targets, coverage, theme-token tints, styles, bindings and data-only content; applyDefaults(catalog, device?) and merge(layout, patch) build and patch an arrangement without mutating it",
"ui-layout (openvibe-sdk/ui-layout, also sdk.uiLayout): validate(catalog, layout) returns { ok, reasons[] } in plain words for required parts, 44 px pressed targets, coverage, theme-token tints, styles, bindings and data-only content; applyDefaults(catalog, device?) and merge(layout, patch) build and patch an arrangement without mutating it",
"menu (openvibe-sdk/menu): menuCard() builds one validated frame.menu-card@1 (plain text, the contract caps, https links on the site own origin or an openvibe.* host, an optional item picture) and menuRoute() is the Express/node:http GET /.well-known/openvibe-menu.json handler (public cache, CORS *, [] and one log line when the cards cannot be built)",
"menu (openvibe-sdk/menu): menuCard() builds one validated frame.menu-card@1 (plain text, the contract caps, https links on the site own origin or an openvibe.* host, an optional item picture) and menuRoute() is the Express/node:http GET /.well-known/openvibe-menu.json handler (public cache, CORS *, [] and one log line when the cards cannot be built)",
"cam (openvibe-sdk/cam): defineOvCamera({ src }) injects the <ov-camera> element from openvibe.cam once and resolves to its class, with typed attributes, events and methods (start/stop/capture/flip); browser-only, upload=\"off\" by default",
"cam (openvibe-sdk/cam): defineOvCamera({ src }) injects the <ov-camera> element from openvibe.cam once and resolves to its class, with typed attributes, events and methods (start/stop/capture/flip); browser-only, upload=\"off\" by default",
"pics (openvibe-sdk/pics): the public gallery and one picture's reads, private uploads with resumable chunks, the owner's explicit publication, likes and albums on OpenVibe.Pics; reads are public, writes take a person's Network token",
"pics (openvibe-sdk/pics): the public gallery and one picture's reads, private uploads with resumable chunks, the owner's explicit publication, likes and albums on OpenVibe.Pics; reads are public, writes take a person's Network token",
"streams (openvibe-sdk/streams, S7): schedule(), listSchedules(), cancelSchedule() and remindSchedule() for scheduled public agent streams; postStep() and steps.post() from any harness; digest.read() and poll.open()/poll.result() for a Co-pilot service token; onControl() for pause, curtain, mute and end",
"streams (openvibe-sdk/streams, S7): schedule(), listSchedules(), cancelSchedule() and remindSchedule() for scheduled public agent streams; postStep() and steps.post() from any harness; digest.read() and poll.open()/poll.result() for a Co-pilot service token; onControl() for pause, curtain, mute and end",
"reports (openvibe-sdk/reports, S7): write() a session report with the person's own token, private by default and idempotent per source and task ref, and read() one the caller may see; examples/cli-agent-stream.js streams any CLI command's run with steps",
"reports (openvibe-sdk/reports, S7): write() a session report with the person's own token, private by default and idempotent per source and task ref, and read() one the caller may see; examples/cli-agent-stream.js streams any CLI command's run with steps",
- "chat-apps (openvibe-sdk/chat-apps): the three chat apps (Telegram, Discord, Slack) in one place — the delivery checks (Telegram's secret token, Slack's v0 signing, Discord's Ed25519), the button callback (signed once, used once, expiring), each app's payload as the same shape and splitText() for a long answer",
+ "chat-apps (openvibe-sdk/chat-apps): the three chat apps (Telegram, Discord, Slack) in one place \u2014 the delivery checks (Telegram's secret token, Slack's v0 signing, Discord's Ed25519), the button callback (signed once, used once, expiring), each app's payload as the same shape and splitText() for a long answer",
"exchange (openvibe-sdk/exchange): the buyer side (chat, models, offers, presets, usage, receipts) and the seller side (offer, endpointOffers, removeEndpointOffer, earnings, verification) of OpenVibe.Exchange, with the platform webhook signature check; an offer is a Link machine's Network report or a standalone OpenAI-compatible endpoint registered on the public seller API",
"exchange (openvibe-sdk/exchange): the buyer side (chat, models, offers, presets, usage, receipts) and the seller side (offer, endpointOffers, removeEndpointOffer, earnings, verification) of OpenVibe.Exchange, with the platform webhook signature check; an offer is a Link machine's Network report or a standalone OpenAI-compatible endpoint registered on the public seller API",
"locker (openvibe-sdk/locker, openvibe-sdk/locker/server): the Locker envelope (locker.envelope@1) for end-to-end encryption, WebCrypto only, in Node and the browser: seal/open for text and bytes, sealStream/openStream, the three locks (people, password, link), person key pairs (ECDH P-256, ECDSA P-256), the eight-word fingerprint, wrapFor and rewrap, recoveryWords for the phrase to write down, opening a people lock by key, inspect for what an envelope says about itself, and the header checks a service runs without a key",
"locker (openvibe-sdk/locker, openvibe-sdk/locker/server): the Locker envelope (locker.envelope@1) for end-to-end encryption, WebCrypto only, in Node and the browser: seal/open for text and bytes, sealStream/openStream, the three locks (people, password, link), person key pairs (ECDH P-256, ECDSA P-256), the eight-word fingerprint, wrapFor and rewrap, recoveryWords for the phrase to write down, opening a people lock by key, inspect for what an envelope says about itself, and the header checks a service runs without a key",
"the 1.0 core surface is frozen now, before 1.0 (core, auth, events, db, media): docs/one-point-oh.json gives every export its tier, test/surface.test.js pins the exact names, and removing or renaming one needs a major release; createCodeVerifier and pkceChallenge left in 0.86.0",
"the 1.0 core surface is frozen now, before 1.0 (core, auth, events, db, media): docs/one-point-oh.json gives every export its tier, test/surface.test.js pins the exact names, and removing or renaming one needs a major release; createCodeVerifier and pkceChallenge left in 0.86.0",
@@ -39,7 +40,7 @@
@@ -39,7 +40,7 @@
"valkey, cache, queue, pubsub and a shared limits store on Valkey 9, each with an in-process twin for tests",
"valkey, cache, queue, pubsub and a shared limits store on Valkey 9, each with an in-process twin for tests",
"commerce (openvibe-sdk/commerce): the shared Billing client for Tips and VIP, with intentKey and receipt",
"commerce (openvibe-sdk/commerce): the shared Billing client for Tips and VIP, with intentKey and receipt",
"events: the PostgreSQL outbox (lease-claimed, several relays per table) and inbox (handler in its transaction)",
"events: the PostgreSQL outbox (lease-claimed, several relays per table) and inbox (handler in its transaction)",
- "usage: createUsageReporter (openvibe-sdk/usage), the shared platform.usage-sample@1 reporter — validated readings queued idempotency-keyed in an outbox and relayed to Billing with retry, never dropped, refused readings kept",
+ "usage: createUsageReporter (openvibe-sdk/usage), the shared platform.usage-sample@1 reporter \u2014 validated readings queued idempotency-keyed in an outbox and relayed to Billing with retry, never dropped, refused readings kept",
"the HTTP telemetry kit (openvibe-sdk/service): the platform.telemetry-sample@1 middleware aggregated per route|method|status_class per flush, with active-requests, p95 and event-loop-lag gauges, lifted from Network's server/telemetry.js and server/observability.js",
"the HTTP telemetry kit (openvibe-sdk/service): the platform.telemetry-sample@1 middleware aggregated per route|method|status_class per flush, with active-requests, p95 and event-loop-lag gauges, lifted from Network's server/telemetry.js and server/observability.js",
"the telemetry sink (openvibe-sdk/telemetry): telemetrySchema() DDL, createPgTelemetrySink() and pruneTelemetry(), a per-service PostgreSQL table every service stores platform.telemetry-sample@1 into",
"the telemetry sink (openvibe-sdk/telemetry): telemetrySchema() DDL, createPgTelemetrySink() and pruneTelemetry(), a per-service PostgreSQL table every service stores platform.telemetry-sample@1 into",
"account-data (openvibe-sdk/account-data, ADR-033): telemetry_samples covered once by the SDK (exported as telemetry.json, anonymized on deletion), and uncovered() listing every table with a person column that no table map, task-keyed list or reasoned exemption covers",
"account-data (openvibe-sdk/account-data, ADR-033): telemetry_samples covered once by the SDK (exported as telemetry.json, anonymized on deletion), and uncovered() listing every table with a person column that no table map, task-keyed list or reasoned exemption covers",
@@ -48,8 +49,8 @@
@@ -48,8 +49,8 @@
"the AI Exchange face of the one planner (openvibe-sdk/placement planAi, off by default): the hard gates (exact artifact, commercial right, privacy class, trust tier, capabilities, context, health, cost ceiling), the expected cost per success in integer nano-USD, sparse quality evidence marked and not trusted, and an explanation that never carries a secret or a host; the money math in openvibe-sdk/cost (nanoUsd, chargeTokens, expectedCostNano, costPerSuccessNano)",
"the AI Exchange face of the one planner (openvibe-sdk/placement planAi, off by default): the hard gates (exact artifact, commercial right, privacy class, trust tier, capabilities, context, health, cost ceiling), the expected cost per success in integer nano-USD, sparse quality evidence marked and not trusted, and an explanation that never carries a secret or a host; the money math in openvibe-sdk/cost (nanoUsd, chargeTokens, expectedCostNano, costPerSuccessNano)",
"consent (openvibe-sdk/consent): the L24 check before any use of a person's content or likeness for AI or data; allowed() and filter() (batches of 500), onChange() drops a subject's answers on network.consent.changed, mock() for tests; fails closed, caches 60 s, server-side",
"consent (openvibe-sdk/consent): the L24 check before any use of a person's content or likeness for AI or data; allowed() and filter() (batches of 500), onChange() drops a subject's answers on network.consent.changed, mock() for tests; fails closed, caches 60 s, server-side",
"openvibe-sdk/machines: the network's machines hub (list, get, catalogue, pair, rename, roles, revoke) against OpenVibe.Network, for a person and for the apps they consented to",
"openvibe-sdk/machines: the network's machines hub (list, get, catalogue, pair, rename, roles, revoke) against OpenVibe.Network, for a person and for the apps they consented to",
- "vibes (openvibe-sdk/vibes): Vibes Pay charges (create with the app reference as the Idempotency-Key, get, refund) and constant-time webhook verification (X-OpenVibe-Signature-V2, ±300 s) for developer apps",
- "streams (openvibe-sdk/streams): the Agent Streams plan S1 — start an agent stream on OpenVibe.Live (live.agent-stream@1) with its publish ticket, renew and revoke that ticket (the kill switch), poll state, post the last frame, set the curtain, switch visibility, end it and mint the view ticket; the publish ticket is a bearer of its own on the desk routes"
+ "vibes (openvibe-sdk/vibes): Vibes Pay charges (create with the app reference as the Idempotency-Key, get, refund) and constant-time webhook verification (X-OpenVibe-Signature-V2, \u00b1300 s) for developer apps",
+ "streams (openvibe-sdk/streams): the Agent Streams plan S1 \u2014 start an agent stream on OpenVibe.Live (live.agent-stream@1) with its publish ticket, renew and revoke that ticket (the kill switch), poll state, post the last frame, set the curtain, switch visibility, end it and mint the view ticket; the publish ticket is a bearer of its own on the desk routes"
],
],
"updated": "2026-11-12"
"updated": "2026-11-12"
}
}
@@ -1,4 +1,4 @@
@@ -1,4 +1,4 @@
-// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.154.0, MIT). Do not edit.
+// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.155.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, composer, cam, mediaUpload, ui, uiLayout, community, homeFeed, feed, bot, search, bench, meme, vision, themes, elements, design, redact, chat, ai, credentials, jobs, agents, frame, tools, projects, geo, history, userCard, player, agentSheet). No server-only code: no service tokens, JWT verification, events, identity.
// (auth, locker, registry, trust, levels, modules, realtime, media, composer, cam, mediaUpload, ui, uiLayout, community, homeFeed, feed, bot, search, bench, meme, vision, themes, elements, design, redact, chat, ai, credentials, jobs, agents, frame, tools, projects, geo, history, userCard, player, agentSheet). No server-only code: no service tokens, JWT verification, events, identity.
/* eslint-disable */
/* eslint-disable */
@@ -67,7 +67,7 @@ const { paginate, offsetPager } = __load(7);
@@ -67,7 +67,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.154.0';
+const SDK_VERSION = '0.155.0';
module.exports = {
module.exports = {
createClient,
createClient,
@@ -5473,8 +5473,8 @@ function (module, exports) {
@@ -5473,8 +5473,8 @@ function (module, exports) {
'use strict';
'use strict';
/**
/**
- * OpenVibe.Design: the public component registry (design.component@1; no token) and the glyph API (icons; a token).
- * Browser-safe.
+ * OpenVibe.Design: the public component registry (design.component@1; no token) and the glyph API (icons; suggest,
+ * get and search are public, a draft needs a token). Browser-safe.
*/
*/
const DEFAULT_DESIGN_URL = 'https://openvibe.design';
const DEFAULT_DESIGN_URL = 'https://openvibe.design';
const STYLES = new Set(['thin', 'light', 'regular', 'bold', 'solid', 'duotone']);
const STYLES = new Set(['thin', 'light', 'regular', 'bold', 'solid', 'duotone']);
@@ -5512,12 +5512,20 @@ function createDesignClient({ accessToken = null, baseUrl = DEFAULT_DESIGN_URL,
@@ -5512,12 +5512,20 @@ function createDesignClient({ accessToken = null, baseUrl = DEFAULT_DESIGN_URL,
async get(name, { style = 'regular', signal } = {}) {
async get(name, { style = 'regular', signal } = {}) {
if (!/^[a-z][a-z0-9-]{1,31}$/.test(String(name || ''))) throw new TypeError('icons.get: name must be lowercase letters, digits or dashes');
if (!/^[a-z][a-z0-9-]{1,31}$/.test(String(name || ''))) throw new TypeError('icons.get: name must be lowercase letters, digits or dashes');
if (!STYLES.has(style)) throw new TypeError('icons.get: unsupported style');
if (!STYLES.has(style)) throw new TypeError('icons.get: unsupported style');
- return request(`/api/v1/glyphs/${encodeURIComponent(name)}?style=${encodeURIComponent(style)}`, { signal });
+ return request(`/api/v1/glyphs/${encodeURIComponent(name)}?style=${encodeURIComponent(style)}`, { signal, open: true });
},
},
async search(q, { limit = 24, signal } = {}) {
async search(q, { limit = 24, signal } = {}) {
const query = String(q || '').trim();
const query = String(q || '').trim();
if (!query) throw new TypeError('icons.search: q is required');
if (!query) throw new TypeError('icons.search: q is required');
- return request(`/api/v1/glyphs/search?q=${encodeURIComponent(query)}&limit=${encodeURIComponent(Math.min(Math.max(Number(limit) || 24, 1), 60))}`, { signal });
+ return request(`/api/v1/glyphs/search?q=${encodeURIComponent(query)}&limit=${encodeURIComponent(Math.min(Math.max(Number(limit) || 24, 1), 60))}`, { signal, open: true });
+ },
+ /** The best glyphs for a button's words by meaning; rank asks a small model when the words are new. Public. */
+ async suggest(label, { context = '', limit = 8, rank = false, signal } = {}) {
+ const words = String(label || '').trim();
+ if (!words) throw new TypeError('icons.suggest: label is required');
+ const n = Math.min(Math.max(Number(limit) || 8, 1), 24);
+ const query = `label=${encodeURIComponent(words)}&limit=${encodeURIComponent(n)}${context ? `&context=${encodeURIComponent(String(context).trim().slice(0, 160))}` : ''}${rank ? '&rank=1' : ''}`;
+ return request(`/api/v1/glyphs/suggest?${query}`, { signal, open: true });
},
},
});
});
return Object.freeze({
return Object.freeze({
@@ -1,12 +1,12 @@
@@ -1,12 +1,12 @@
{
{
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.154.0",
+ "version": "0.155.0",
"lockfileVersion": 3,
"lockfileVersion": 3,
"requires": true,
"requires": true,
"packages": {
"packages": {
"": {
"": {
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.154.0",
+ "version": "0.155.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.154.0",
+ "version": "0.155.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",
@@ -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.154.0';
+const SDK_VERSION = '0.155.0';
module.exports = {
module.exports = {
createClient,
createClient,
@@ -1,8 +1,8 @@
@@ -1,8 +1,8 @@
'use strict';
'use strict';
/**
/**
- * OpenVibe.Design: the public component registry (design.component@1; no token) and the glyph API (icons; a token).
- * Browser-safe.
+ * OpenVibe.Design: the public component registry (design.component@1; no token) and the glyph API (icons; suggest,
+ * get and search are public, a draft needs a token). Browser-safe.
*/
*/
const DEFAULT_DESIGN_URL = 'https://openvibe.design';
const DEFAULT_DESIGN_URL = 'https://openvibe.design';
const STYLES = new Set(['thin', 'light', 'regular', 'bold', 'solid', 'duotone']);
const STYLES = new Set(['thin', 'light', 'regular', 'bold', 'solid', 'duotone']);
@@ -40,12 +40,20 @@ function createDesignClient({ accessToken = null, baseUrl = DEFAULT_DESIGN_URL,
@@ -40,12 +40,20 @@ function createDesignClient({ accessToken = null, baseUrl = DEFAULT_DESIGN_URL,
async get(name, { style = 'regular', signal } = {}) {
async get(name, { style = 'regular', signal } = {}) {
if (!/^[a-z][a-z0-9-]{1,31}$/.test(String(name || ''))) throw new TypeError('icons.get: name must be lowercase letters, digits or dashes');
if (!/^[a-z][a-z0-9-]{1,31}$/.test(String(name || ''))) throw new TypeError('icons.get: name must be lowercase letters, digits or dashes');
if (!STYLES.has(style)) throw new TypeError('icons.get: unsupported style');
if (!STYLES.has(style)) throw new TypeError('icons.get: unsupported style');
- return request(`/api/v1/glyphs/${encodeURIComponent(name)}?style=${encodeURIComponent(style)}`, { signal });
+ return request(`/api/v1/glyphs/${encodeURIComponent(name)}?style=${encodeURIComponent(style)}`, { signal, open: true });
},
},
async search(q, { limit = 24, signal } = {}) {
async search(q, { limit = 24, signal } = {}) {
const query = String(q || '').trim();
const query = String(q || '').trim();
if (!query) throw new TypeError('icons.search: q is required');
if (!query) throw new TypeError('icons.search: q is required');
- return request(`/api/v1/glyphs/search?q=${encodeURIComponent(query)}&limit=${encodeURIComponent(Math.min(Math.max(Number(limit) || 24, 1), 60))}`, { signal });
+ return request(`/api/v1/glyphs/search?q=${encodeURIComponent(query)}&limit=${encodeURIComponent(Math.min(Math.max(Number(limit) || 24, 1), 60))}`, { signal, open: true });
+ },
+ /** The best glyphs for a button's words by meaning; rank asks a small model when the words are new. Public. */
+ async suggest(label, { context = '', limit = 8, rank = false, signal } = {}) {
+ const words = String(label || '').trim();
+ if (!words) throw new TypeError('icons.suggest: label is required');
+ const n = Math.min(Math.max(Number(limit) || 8, 1), 24);
+ const query = `label=${encodeURIComponent(words)}&limit=${encodeURIComponent(n)}${context ? `&context=${encodeURIComponent(String(context).trim().slice(0, 160))}` : ''}${rank ? '&rank=1' : ''}`;
+ return request(`/api/v1/glyphs/suggest?${query}`, { signal, open: true });
},
},
});
});
return Object.freeze({
return Object.freeze({
@@ -11,16 +11,28 @@ const { createDesignClient } = require('../src/design');
@@ -11,16 +11,28 @@ const { createDesignClient } = require('../src/design');
await client.icons.draft({ name: 'hot-tub', styles: ['thin', 'solid'], sourceSvg: '<path d="M5 12h14"/>' });
await client.icons.draft({ name: 'hot-tub', styles: ['thin', 'solid'], sourceSvg: '<path d="M5 12h14"/>' });
await client.icons.get('hot-tub', { style: 'duotone' });
await client.icons.get('hot-tub', { style: 'duotone' });
await client.icons.search('hot tub', { limit: 5 });
await client.icons.search('hot tub', { limit: 5 });
+ await client.icons.suggest('Deep dive', { context: 'a menu', limit: 5, rank: true });
assert.equal(calls[0].init.headers.authorization, 'Bearer test-token');
assert.equal(calls[0].init.headers.authorization, 'Bearer test-token');
assert.deepEqual(JSON.parse(calls[0].init.body).styles, ['thin', 'solid']);
assert.deepEqual(JSON.parse(calls[0].init.body).styles, ['thin', 'solid']);
assert.equal(JSON.parse(calls[0].init.body).sourceSvg, '<path d="M5 12h14"/>');
assert.equal(JSON.parse(calls[0].init.body).sourceSvg, '<path d="M5 12h14"/>');
assert.match(calls[1].url, /\/api\/v1\/glyphs\/hot-tub\?style=duotone$/);
assert.match(calls[1].url, /\/api\/v1\/glyphs\/hot-tub\?style=duotone$/);
assert.match(calls[2].url, /\/api\/v1\/glyphs\/search\?q=hot%20tub&limit=5$/);
assert.match(calls[2].url, /\/api\/v1\/glyphs\/search\?q=hot%20tub&limit=5$/);
+ assert.match(calls[3].url, /\/api\/v1\/glyphs\/suggest\?label=Deep%20dive&limit=5&context=a%20menu&rank=1$/);
+ assert.equal(calls[3].init.headers.authorization, 'Bearer test-token', 'a signed-in suggestion is ranked on the person allowance');
await assert.rejects(client.icons.get('bad name'), TypeError);
await assert.rejects(client.icons.get('bad name'), TypeError);
+ await assert.rejects(client.icons.suggest(' '), TypeError);
const esm = await import('../esm/index.mjs');
const esm = await import('../esm/index.mjs');
assert.equal(typeof esm.design.createDesignClient, 'function', 'the ESM root exposes the Design client');
assert.equal(typeof esm.design.createDesignClient, 'function', 'the ESM root exposes the Design client');
+ // suggest, get and search are public: no token, no Authorization header.
+ const pubCalls = [];
+ const pub = createDesignClient({ fetchImpl: async (url, init) => { pubCalls.push({ url: String(url), init }); return { ok: true, async json() { return { glyphs: [] }; } }; } });
+ await pub.icons.suggest('go live');
+ await pub.icons.get('heart');
+ await pub.icons.search('gear');
+ assert.ok(pubCalls.every((c) => !c.init.headers.authorization), 'public glyph calls carry no token');
+ await assert.rejects(pub.icons.draft({ name: 'hot-tub' }), /needs accessToken/);
const designEsm = await import('../esm/design.mjs');
const designEsm = await import('../esm/design.mjs');
const esmClient = designEsm.createDesignClient({ accessToken: 'test-token', fetchImpl: async () => ({ ok: true, async json() { return {}; } }) });
const esmClient = designEsm.createDesignClient({ accessToken: 'test-token', fetchImpl: async () => ({ ok: true, async json() { return {}; } }) });
await esmClient.icons.get('heart', { style: 'bold' });
await esmClient.icons.get('heart', { style: 'bold' });
- console.log('Design SDK icons: draft, get and search use the authenticated glyph API');
+ console.log('Design SDK icons: draft uses the authenticated API; suggest, get and search are public');
})().catch((err) => { console.error(err); process.exit(1); });
})().catch((err) => { console.error(err); process.exit(1); });
@@ -38,7 +38,7 @@ run([
@@ -38,7 +38,7 @@ run([
const signed = createDesignClient({ baseUrl: srv.url, accessToken: 'user-jwt' });
const signed = createDesignClient({ baseUrl: srv.url, accessToken: 'user-jwt' });
await signed.components();
await signed.components();
assert.equal(srv.requests[3].headers.authorization, 'Bearer user-jwt', 'a token, when given, goes along');
assert.equal(srv.requests[3].headers.authorization, 'Bearer user-jwt', 'a token, when given, goes along');
- await assert.rejects(() => d.icons.get('heart'), /needs accessToken/, 'the icon API still needs a token');
+ await assert.rejects(() => d.icons.draft({ name: 'hot-tub' }), /needs accessToken/, 'a glyph draft still needs a token');
await srv.close();
await srv.close();
}],
}],
['a bad name is refused before a request; a server error still throws', async () => {
['a bad name is refused before a request; a server error still throws', async () => {
@@ -2,6 +2,17 @@ export type GlyphStyle = 'thin' | 'light' | 'regular' | 'bold' | 'solid' | 'duot
@@ -2,6 +2,17 @@ export type GlyphStyle = 'thin' | 'light' | 'regular' | 'bold' | 'solid' | 'duot
export interface DesignGlyph { name: string; style: GlyphStyle; svg: string; solid: string; strokeWidth: number | null; underlay: string; underlayOpacity: number; }
export interface DesignGlyph { name: string; style: GlyphStyle; svg: string; solid: string; strokeWidth: number | null; underlay: string; underlayOpacity: number; }
export interface DesignGlyphReference { name: string; set: string; setName: string; author: string; license: string; licenseUrl: string; preview: string; }
export interface DesignGlyphReference { name: string; set: string; setName: string; author: string; license: string; licenseUrl: string; preview: string; }
export interface DesignGlyphDraft { id: string; name: string; svg: string; solid_svg: string | null; styles: GlyphStyle[]; references: DesignGlyphReference[]; status: string; }
export interface DesignGlyphDraft { id: string; name: string; svg: string; solid_svg: string | null; styles: GlyphStyle[]; references: DesignGlyphReference[]; status: string; }
+export interface DesignGlyphSuggestion { name: string; score: number; tags: string[]; why: string[]; }
+export interface DesignGlyphSuggestionResult {
+ label: string;
+ context: string;
+ glyphs: DesignGlyphSuggestion[];
+ new: boolean;
+ ranked: boolean;
+ model: string | null;
+ cached?: boolean;
+ draft_price?: { usd: number | null; model: string | null; provider: string | null; tokens: { in: number; out: number }; note: string };
+}
export interface DesignComponent {
export interface DesignComponent {
schema: 'design.component@1';
schema: 'design.component@1';
name: string;
name: string;
@@ -21,12 +32,13 @@ export interface DesignComponent {
@@ -21,12 +32,13 @@ export interface DesignComponent {
examples: Array<{ title: string; props: Record<string, unknown> }>;
examples: Array<{ title: string; props: Record<string, unknown> }>;
}
}
-/** accessToken is needed for icons only; the component registry is public. */
+/** accessToken is needed for icons.draft only; suggest, get, search and the component registry are public. */
export declare function createDesignClient(options?: { accessToken?: string; baseUrl?: string; fetchImpl?: typeof fetch; timeoutMs?: number }): {
export declare function createDesignClient(options?: { accessToken?: string; baseUrl?: string; fetchImpl?: typeof fetch; timeoutMs?: number }): {
icons: {
icons: {
draft(input: { name: string; sentence?: string; styles?: GlyphStyle[]; sourceSvg?: string }, opts?: { signal?: AbortSignal }): Promise<{ kept: DesignGlyphDraft[]; refused: string[] }>;
draft(input: { name: string; sentence?: string; styles?: GlyphStyle[]; sourceSvg?: string }, opts?: { signal?: AbortSignal }): Promise<{ kept: DesignGlyphDraft[]; refused: string[] }>;
get(name: string, opts?: { style?: GlyphStyle; signal?: AbortSignal }): Promise<DesignGlyph>;
get(name: string, opts?: { style?: GlyphStyle; signal?: AbortSignal }): Promise<DesignGlyph>;
search(q: string, opts?: { limit?: number; signal?: AbortSignal }): Promise<{ query: string; glyphs: DesignGlyph[] }>;
search(q: string, opts?: { limit?: number; signal?: AbortSignal }): Promise<{ query: string; glyphs: DesignGlyph[] }>;
+ suggest(label: string, opts?: { context?: string; limit?: number; rank?: boolean; signal?: AbortSignal }): Promise<DesignGlyphSuggestionResult>;
};
};
components(opts?: { signal?: AbortSignal }): Promise<{ schema: 'design.component@1'; components: DesignComponent[]; kitComponents: unknown[] }>;
components(opts?: { signal?: AbortSignal }): Promise<{ schema: 'design.component@1'; components: DesignComponent[]; kitComponents: unknown[] }>;
component(name: string, opts?: { signal?: AbortSignal }): Promise<{ schema: 'design.component@1'; component: DesignComponent; examples: DesignComponent['examples'] } | null>;
component(name: string, opts?: { signal?: AbortSignal }): Promise<{ schema: 'design.component@1'; component: DesignComponent; examples: DesignComponent['examples'] } | null>;