openvibers/openvibe.examples Commits

ai-anthropic and ai-python: OpenVibe AI from the Anthropic SDK and from Python (#55)

OP OpenVibers committed 9 days ago · 6b6eec1ab6 · 13 files, +434 −1

ai-anthropic: the official @anthropic-ai/sdk on https://ai.openvibe.services (POST /v1/messages),
the Network app token (ai.app.run) through the SDK's credentials provider, sent as Bearer.
ai-python: the official openai Python client on /v1, the token from client credentials kept
and renewed a minute early. Smoke tests: the real Anthropic SDK against stand-ins (no
network); the Python script's token request and base URL, compiled when python3 is there.

Changes

ddiff --git a/README.md b/README.md
iindex 6a0dcc2..44bfbc4 100644
---- a/README.md
++++ b/README.md
@@@ -41,6 +41,8 @@ Each example is small, runs on its own and proves one thing with a smoke test.
| [space-deploy](examples/space-deploy) | Deploy a static site to OpenVibe.Space from CI with an app token (space.deploy.create, deployer role): the built folder as one deploy, live on main or a preview for a pull request, a refusal that changes nothing, rollback; a ready GitHub Actions workflow | `openvibe-sdk/space` (`deploys.uploadFiles`, `rollback`), `/auth` |
| [zone-s3](examples/zone-s3) | OpenVibe.Zone's private buckets over S3 with the official AWS SDK v3: put, get, a paged list, a presigned share link (at most 7 days) and delete; path-style, us-east-1, SigV4, an access key scoped to the zone | `@aws-sdk/client-s3`, `@aws-sdk/s3-request-presigner` |
| [ai-chat](examples/ai-chat) | The official `openai` npm client on `https://ai.openvibe.services/v1` with a Network app token: plain and streamed Chat Completions, `x_openvibe` run info, the project's own provider by secret name | `openai` (`baseURL`, `apiKey` as a token function), `openvibe-sdk/auth` |
++| [ai-anthropic](examples/ai-anthropic) | The official `@anthropic-ai/sdk` client on `https://ai.openvibe.services` (POST /v1/messages) with a Network app token through the SDK's `credentials`: the answer names the model that ran and carries `x_openvibe`. |
++| [ai-python](examples/ai-python) | The official `openai` Python client on `https://ai.openvibe.services/v1`, with a Network app token from client credentials kept and renewed a minute early. |
Each example folder has a README, a `.env.example`, its own `package.json` and a
`test/smoke.test.js`. Configuration is environment variables only (`OV_CLIENT_ID`,
@@@ -129,6 +131,8 @@ at the root one: `node --env-file=../../.env upload.js ./logo.png`.
| [chat-bot](examples/chat-bot) | none: a Live API token (`hbt_…`) of a dedicated bot account | Live token scope `chat` | `OV_CHAT_TOKEN`, one of `OV_CHAT_CHANNEL_USER_ID` / `OV_CHAT_STREAM_ID` |
| [mod-manifest](examples/mod-manifest) | none | none | `OV_NETWORK_URL` for `--registry` |
| [ai-chat](examples/ai-chat) | confidential | `ai.app.run` | `OV_CLIENT_ID`, `OV_CLIENT_SECRET`, optional `OV_AI_URL`; own provider: `OV_PROVIDER_SECRET` (a secret's name), `OV_PROVIDER_BASE_URL`, `OV_PROVIDER_MODEL` |
++| [ai-anthropic](examples/ai-anthropic) | confidential | `ai.app.run` | `OV_CLIENT_ID`, `OV_CLIENT_SECRET`, optional `OV_MODEL`, `OV_NETWORK_URL`, `OV_AI_URL` |
++| [ai-python](examples/ai-python) | confidential | `ai.app.run` | `OV_CLIENT_ID`, `OV_CLIENT_SECRET`, optional `OV_MODEL`, `OV_NETWORK_URL`, `OV_AI_URL` |
Rotate a secret in the portal or with `projects.credentials.rotate()`, revoke the app with
`projects.apps.revoke()`. Issued tokens end within their 5-minute lifetime.
ddiff --git a/examples/ai-anthropic/.env.example b/examples/ai-anthropic/.env.example
nnew file mode 100644
iindex 0000000..171163f
---- /dev/null
++++ b/examples/ai-anthropic/.env.example
@@@ -0,0 +1,8 @@
++# A sandbox app of your project (OpenVibe.Services -> Projects -> Apps), holding ai.app.run.
++OV_CLIENT_ID=
++OV_CLIENT_SECRET=
++# Optional: a dynamic name (openvibe, openvibe/economy, ...) or an exact model id from openvibe.network/models.
++# OV_MODEL=openvibe
++# Optional: the Network and AI origins (the defaults are production).
++# OV_NETWORK_URL=https://openvibe.network
++# OV_AI_URL=https://ai.openvibe.services
ddiff --git a/examples/ai-anthropic/README.md b/examples/ai-anthropic/README.md
nnew file mode 100644
iindex 0000000..60cfc38
---- /dev/null
++++ b/examples/ai-anthropic/README.md
@@@ -0,0 +1,31 @@
++# AI with the Anthropic SDK
++
++Use OpenVibe.AI from the official [`@anthropic-ai/sdk`](https://www.npmjs.com/package/@anthropic-ai/sdk) client.
++Point its `baseURL` at `https://ai.openvibe.services` (the SDK adds `/v1/messages`) and give it a Network app token
++through `credentials`.
++
++```bash
++node --env-file=.env ask.js "Say hi in five words"
++# Hi there, nice to meet you!
++# (qwen/qwen3-8b-abliterated:q4_k_m@3f2a1c9, $0.000001)
++```
++
++## What it proves
++
++- **An app token of `openvibe.ai`, scoped `ai.app.run`.** This is the public capability developer apps call AI
++ with; a sandbox app holds it without staff.
++ - Network tokens last five minutes, so the SDK's `credentials` provider asks `openvibe-sdk/auth` for a fresh one.
++ - The SDK sends it as `Authorization: Bearer`, never as an Anthropic `x-api-key`.
++- **`messages.create()` works unchanged.**
++ - `model` is a dynamic name (`openvibe`, `openvibe/economy`, …) or an exact model id from the model list.
++ - The answer's `model` names the model that ran, and `x_openvibe` adds the run, the provider and the cost.
++
++## Run the smoke test
++
++`npm test -w examples/ai-anthropic` runs the real SDK against stand-ins of Network and AI, with no network.
++
++## Run it against the real platform
++
++1. Make a sandbox app in your project on OpenVibe.Services (Projects → Apps). It holds `ai.app.run`.
++2. Copy `.env.example` to `.env` and fill in the app's client id and secret.
++3. Run `node --env-file=.env ask.js "Say hi in five words"`.
ddiff --git a/examples/ai-anthropic/ask.js b/examples/ai-anthropic/ask.js
nnew file mode 100644
iindex 0000000..bd57a46
---- /dev/null
++++ b/examples/ai-anthropic/ask.js
@@@ -0,0 +1,60 @@
++#!/usr/bin/env node
++'use strict';
++/**
++ * AI with the Anthropic SDK: the official `@anthropic-ai/sdk` client, pointed at OpenVibe.AI's Anthropic Messages
++ * endpoint (POST https://ai.openvibe.services/v1/messages).
++ *
++ * node --env-file=.env ask.js "Say hi in five words"
++ *
++ * 1. An app token for openvibe.ai (client credentials, scope ai.app.run: the public capability developer apps
++ * call AI with). Network tokens last five minutes: the SDK's `credentials` provider asks openvibe-sdk for one
++ * before it expires, and the SDK sends it as a Bearer token.
++ * 2. new Anthropic({ baseURL: 'https://ai.openvibe.services', credentials }).
++ * 3. messages.create(): `model` is a dynamic name (openvibe picks within your rules) or an exact model id from
++ * https://openvibe.network/models; the answer names the model that ran.
++ */
++const Anthropic = require('@anthropic-ai/sdk');
++const { createServiceTokenClient } = require('openvibe-sdk/auth');
++
++function loadConfig(env = process.env) {
++ const missing = ['OV_CLIENT_ID', 'OV_CLIENT_SECRET'].filter((k) => !env[k]);
++ if (missing.length) throw Object.assign(new Error(`missing environment variables: ${missing.join(', ')} (see .env.example)`), { code: 'config.missing' });
++ return {
++ network: env.OV_NETWORK_URL || 'https://openvibe.network',
++ clientId: env.OV_CLIENT_ID,
++ clientSecret: env.OV_CLIENT_SECRET,
++ aiUrl: env.OV_AI_URL || 'https://ai.openvibe.services',
++ model: env.OV_MODEL || 'openvibe',
++ };
++}
++
++function createClient(config, { fetch } = {}) {
++ const tokens = createServiceTokenClient({
++ network: config.network, clientId: config.clientId, clientSecret: config.clientSecret, fetch,
++ audience: 'openvibe.ai', scope: 'ai.app.run',
++ });
++ return new Anthropic({
++ baseURL: config.aiUrl,
++ // The SDK caches what this returns until expiresAt; openvibe-sdk refreshes the token a minute early.
++ credentials: async () => ({ token: await tokens.getToken(), expiresAt: Math.floor(Date.now() / 1000) + 60 }),
++ ...(fetch ? { fetch } : {}),
++ });
++}
++
++async function ask(client, config, prompt) {
++ const reply = await client.messages.create({ model: config.model, max_tokens: 512, messages: [{ role: 'user', content: prompt }] });
++ const text = reply.content.filter((b) => b.type === 'text').map((b) => b.text).join('');
++ return { text, model: reply.model, usage: reply.usage, x_openvibe: reply.x_openvibe || null };
++}
++
++if (require.main === module) {
++ (async () => {
++ const config = loadConfig();
++ const prompt = process.argv.slice(2).join(' ') || 'Say hi in five words';
++ const out = await ask(createClient(config), config, prompt);
++ console.log(out.text);
++ console.log(`(${out.model}${out.x_openvibe && out.x_openvibe.cost_usd != null ? `, $${out.x_openvibe.cost_usd}` : ''})`);
++ })().catch((e) => { console.error(e.message || e); process.exit(1); });
++}
++
++module.exports = { loadConfig, createClient, ask };
ddiff --git a/examples/ai-anthropic/package.json b/examples/ai-anthropic/package.json
nnew file mode 100644
iindex 0000000..585984b
---- /dev/null
++++ b/examples/ai-anthropic/package.json
@@@ -0,0 +1,19 @@
++{
++ "name": "openvibe-example-ai-anthropic",
++ "version": "0.1.0",
++ "private": true,
++ "description": "AI with the Anthropic SDK: the official @anthropic-ai/sdk client pointed at ai.openvibe.services (POST /v1/messages), with a Network app token.",
++ "license": "MIT",
++ "main": "ask.js",
++ "scripts": {
++ "start": "node --env-file-if-exists=.env ask.js",
++ "test": "node test/smoke.test.js"
++ },
++ "engines": {
++ "node": ">=22"
++ },
++ "dependencies": {
++ "@anthropic-ai/sdk": "1.4.0",
++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"
++ }
++}
ddiff --git a/examples/ai-anthropic/test/smoke.test.js b/examples/ai-anthropic/test/smoke.test.js
nnew file mode 100644
iindex 0000000..560e50a
---- /dev/null
++++ b/examples/ai-anthropic/test/smoke.test.js
@@@ -0,0 +1,48 @@
++'use strict';
++/**
++ * Smoke test: the Anthropic SDK against stand-ins of Network's /oauth/token and AI's /v1/messages, answering in the
++ * shapes server/api/messages.js answers (a message with content blocks, usage and x_openvibe). No network.
++ */
++const assert = require('node:assert/strict');
++const { loadConfig, createClient, ask } = require('../ask');
++
++globalThis.fetch = () => { throw new Error('network access in a smoke test'); };
++const calls = [];
++const json = (status, o) => new Response(JSON.stringify(o), { status, headers: { 'content-type': 'application/json' } });
++
++async function fakeFetch(url, init = {}) {
++ const u = String(url);
++ const headers = new Headers(init.headers);
++ if (u === 'https://openvibe.network/oauth/token') {
++ const form = new URLSearchParams(String(init.body));
++ calls.push({ kind: 'token', audience: form.get('audience'), scope: form.get('scope') });
++ return json(200, { access_token: 'tok-1', token_type: 'Bearer', expires_in: 300, scope: 'ai.app.run' });
++ }
++ if (u === 'https://ai.openvibe.services/v1/messages') {
++ calls.push({ kind: 'messages', auth: headers.get('authorization'), apiKey: headers.get('x-api-key'), body: JSON.parse(init.body) });
++ return json(200, {
++ id: 'msg_01TEST', type: 'message', role: 'assistant', model: 'qwen/qwen3-8b-abliterated:q4_k_m@3f2a1c9', stop_reason: 'end_turn', stop_sequence: null,
++ content: [{ type: 'text', text: 'Hi there, nice to meet you!' }], usage: { input_tokens: 12, output_tokens: 7 },
++ x_openvibe: { run_id: 'run_01TEST', provider: 'own', fallback_used: false, cost_usd: 0.000001 },
++ });
++ }
++ throw new Error(`unexpected request ${u}`);
++}
++
++(async () => {
++ assert.throws(() => loadConfig({}), /missing environment variables: OV_CLIENT_ID, OV_CLIENT_SECRET/);
++ const config = loadConfig({ OV_CLIENT_ID: 'app_01TEST', OV_CLIENT_SECRET: 'not-a-real-secret' });
++ const client = createClient(config, { fetch: fakeFetch });
++ const out = await ask(client, config, 'Say hi in five words');
++ assert.equal(out.text, 'Hi there, nice to meet you!');
++ assert.equal(out.model, 'qwen/qwen3-8b-abliterated:q4_k_m@3f2a1c9', 'the answer names the model that ran');
++ assert.equal(out.x_openvibe.run_id, 'run_01TEST');
++ const token = calls.find((c) => c.kind === 'token');
++ assert.deepEqual([token.audience, token.scope], ['openvibe.ai', 'ai.app.run'], 'an app token of openvibe.ai, scoped ai.app.run');
++ const m = calls.find((c) => c.kind === 'messages');
++ assert.equal(m.auth, 'Bearer tok-1', 'the Network token as a Bearer token');
++ assert.ok(!m.apiKey, 'no x-api-key: this is not an Anthropic key');
++ assert.equal(m.body.model, 'openvibe');
++ assert.deepEqual(m.body.messages, [{ role: 'user', content: 'Say hi in five words' }]);
++ console.log('ai-anthropic smoke: all checks passed');
++})().catch((e) => { console.error(e); process.exit(1); });
ddiff --git a/examples/ai-python/.env.example b/examples/ai-python/.env.example
nnew file mode 100644
iindex 0000000..4eec9e5
---- /dev/null
++++ b/examples/ai-python/.env.example
@@@ -0,0 +1,7 @@
++# A sandbox app of your project (OpenVibe.Services -> Projects -> Apps), holding ai.app.run.
++OV_CLIENT_ID=
++OV_CLIENT_SECRET=
++# Optional: a dynamic name or an exact model id; the Network and AI origins (the defaults are production).
++# OV_MODEL=openvibe
++# OV_NETWORK_URL=https://openvibe.network
++# OV_AI_URL=https://ai.openvibe.services/v1
ddiff --git a/examples/ai-python/README.md b/examples/ai-python/README.md
nnew file mode 100644
iindex 0000000..6e54619
---- /dev/null
++++ b/examples/ai-python/README.md
@@@ -0,0 +1,32 @@
++# AI from Python
++
++Use OpenVibe.AI from the official [`openai`](https://pypi.org/project/openai/) Python client:
++- point `base_url` at `https://ai.openvibe.services/v1`;
++- give it a Network app token for `openvibe.ai`, scoped `ai.app.run`.
++
++```bash
++pip install openai httpx
++OV_CLIENT_ID=… OV_CLIENT_SECRET=… python3 chat.py "Say hi in five words"
++# Hi there, nice to meet you!
++# (qwen/qwen3-8b-abliterated:q4_k_m@3f2a1c9, $1e-06)
++```
++
++## What it proves
++
++- **The token.** The app's client credentials get a five-minute token from Network. `chat.py` keeps the token and
++ asks again a minute before it ends.
++- **The call.** `chat.completions.create()` works unchanged.
++- **The model.** `model` is a dynamic name or an exact model id.
++- **The answer.** It names the model that ran, and `x_openvibe` (in the response's extra fields) carries the run,
++ the provider and the cost.
++
++## Run the smoke test
++
++`npm test -w examples/ai-python` checks the token request and the base URL, and compiles the script when Python is
++on the machine.
++
++## Run it against the real platform
++
++1. Make a sandbox app in your project on OpenVibe.Services (Projects → Apps). It holds `ai.app.run`.
++2. Install the client: `pip install openai httpx`.
++3. Run `OV_CLIENT_ID=… OV_CLIENT_SECRET=… python3 chat.py "Say hi in five words"`.
ddiff --git a/examples/ai-python/chat.py b/examples/ai-python/chat.py
nnew file mode 100644
iindex 0000000..3c27579
---- /dev/null
++++ b/examples/ai-python/chat.py
@@@ -0,0 +1,52 @@
++#!/usr/bin/env python3
++"""AI from Python: the official `openai` client pointed at OpenVibe.AI.
++
++ pip install openai httpx
++ OV_CLIENT_ID=... OV_CLIENT_SECRET=... python3 chat.py "Say hi in five words"
++
++1. An app token of openvibe.ai from Network (client credentials, scope ai.app.run: the public capability developer
++ apps call AI with). Network tokens last five minutes; this keeps one and asks again a minute before it ends.
++2. OpenAI(base_url="https://ai.openvibe.services/v1", api_key=<the token>).
++3. `model` is a dynamic name (openvibe picks within your rules) or an exact model id from openvibe.network/models;
++ the answer names the model that ran, and `x_openvibe` carries the run, the provider and the cost.
++"""
++import os
++import sys
++import time
++
++import httpx
++from openai import OpenAI
++
++NETWORK = os.environ.get("OV_NETWORK_URL", "https://openvibe.network")
++AI = os.environ.get("OV_AI_URL", "https://ai.openvibe.services/v1")
++_token = {"value": None, "expires": 0.0}
++
++
++def app_token() -> str:
++ """A Network app token of openvibe.ai, reused until a minute before it expires."""
++ if _token["value"] and time.time() < _token["expires"] - 60:
++ return _token["value"]
++ res = httpx.post(f"{NETWORK}/oauth/token", data={
++ "grant_type": "client_credentials",
++ "client_id": os.environ["OV_CLIENT_ID"],
++ "client_secret": os.environ["OV_CLIENT_SECRET"],
++ "audience": "openvibe.ai",
++ "scope": "ai.app.run",
++ }, timeout=10)
++ res.raise_for_status()
++ doc = res.json()
++ _token.update(value=doc["access_token"], expires=time.time() + int(doc.get("expires_in", 300)))
++ return _token["value"]
++
++
++def ask(prompt: str, model: str = os.environ.get("OV_MODEL", "openvibe")):
++ client = OpenAI(base_url=AI, api_key=app_token())
++ reply = client.chat.completions.create(model=model, messages=[{"role": "user", "content": prompt}])
++ extra = (reply.model_extra or {}).get("x_openvibe") or {}
++ return reply.choices[0].message.content, reply.model, extra
++
++
++if __name__ == "__main__":
++ text, model, extra = ask(" ".join(sys.argv[1:]) or "Say hi in five words")
++ print(text)
++ print(f"({model}, ${extra.get('cost_usd')})" if extra.get("cost_usd") is not None else f"({model})")
ddiff --git a/examples/ai-python/package.json b/examples/ai-python/package.json
nnew file mode 100644
iindex 0000000..4aa4d8a
---- /dev/null
++++ b/examples/ai-python/package.json
@@@ -0,0 +1,13 @@
++{
++ "name": "openvibe-example-ai-python",
++ "version": "0.1.0",
++ "private": true,
++ "description": "AI from Python: the official openai Python client pointed at ai.openvibe.services/v1, with a Network app token from client credentials.",
++ "license": "MIT",
++ "scripts": {
++ "test": "node test/smoke.test.js"
++ },
++ "engines": {
++ "node": ">=22"
++ }
++}
ddiff --git a/examples/ai-python/test/smoke.test.js b/examples/ai-python/test/smoke.test.js
nnew file mode 100644
iindex 0000000..fc7f52e
---- /dev/null
++++ b/examples/ai-python/test/smoke.test.js
@@@ -0,0 +1,22 @@
++'use strict';
++/**
++ * Smoke test of the Python example: it compiles (python3 -m py_compile, when Python is on the machine), and it asks
++ * Network the right token (audience openvibe.ai, scope ai.app.run) and calls the right base URL. No network.
++ */
++const assert = require('node:assert/strict');
++const fs = require('node:fs');
++const path = require('node:path');
++const { spawnSync } = require('node:child_process');
++
++const file = path.join(__dirname, '..', 'chat.py');
++const src = fs.readFileSync(file, 'utf8');
++assert.match(src, /"audience": "openvibe\.ai"/);
++assert.match(src, /"scope": "ai\.app\.run"/);
++assert.match(src, /https:\/\/ai\.openvibe\.services\/v1/);
++assert.match(src, /OpenAI\(base_url=AI, api_key=app_token\(\)\)/);
++const py = spawnSync('python3', ['-m', 'py_compile', file], { encoding: 'utf8' });
++if (py.error && py.error.code === 'ENOENT') console.log('ai-python smoke: no python3 here; the source checks passed');
++else {
++ assert.equal(py.status, 0, py.stderr);
++ console.log('ai-python smoke: compiles; all checks passed');
++}
ddiff --git a/package-lock.json b/package-lock.json
iindex 3c7e258..fa8da7d 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -21,6 +21,46 @@
"node": ">=22"
}
},
++ "examples/ai-anthropic": {
++ "name": "openvibe-example-ai-anthropic",
++ "version": "0.1.0",
++ "license": "MIT",
++ "dependencies": {
++ "@anthropic-ai/sdk": "1.4.0",
++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0"
++ },
++ "engines": {
++ "node": ">=22"
++ }
++ },
++ "examples/ai-anthropic/node_modules/openvibe-sdk": {
++ "version": "0.83.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.83.0",
++ "integrity": "sha512-xHFcsZmRVuQX0HlMFA7Kc53GMpdZUXxkcR17whknFgUQznuWCwcnhwS87HnnqsrmnHp8AHC0d5uywLjfOwOYBw==",
++ "license": "MIT",
++ "bin": {
++ "openvibe-download": "bin/openvibe-download.js"
++ },
++ "engines": {
++ "node": ">=20"
++ },
++ "peerDependencies": {
++ "@electric-sql/pglite": "^0.5.8",
++ "iovalkey": "^0.4.0",
++ "pg": "^8.23.0"
++ },
++ "peerDependenciesMeta": {
++ "@electric-sql/pglite": {
++ "optional": true
++ },
++ "iovalkey": {
++ "optional": true
++ },
++ "pg": {
++ "optional": true
++ }
++ }
++ },
"examples/ai-chat": {
"name": "openvibe-example-ai-chat",
"version": "0.1.0",
@@@ -33,6 +73,14 @@
"node": ">=22"
}
},
++ "examples/ai-python": {
++ "name": "openvibe-example-ai-python",
++ "version": "0.1.0",
++ "license": "MIT",
++ "engines": {
++ "node": ">=22"
++ }
++ },
"examples/browser-app": {
"name": "openvibe-example-browser-app",
"version": "0.2.0",
@@@ -191,6 +239,28 @@
"node": ">=22"
}
},
++ "node_modules/@anthropic-ai/sdk": {
++ "version": "1.4.0",
++ "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-1.4.0.tgz",
++ "integrity": "sha512-tyuibnhQL57kLOx5oomY4Ld7jOVOPNC5v3IiyRtR3l+Friv66tI4VbuZYF2NMel/T0WMuQMBFOMKDBUbUh8gPA==",
++ "license": "Apache-2.0",
++ "dependencies": {
++ "@opentelemetry/api": "^1.4.1",
++ "json-schema-to-ts": "^3.1.1",
++ "standardwebhooks": "^1.0.0"
++ },
++ "bin": {
++ "anthropic-ai-sdk": "bin/cli"
++ },
++ "peerDependencies": {
++ "zod": "^3.25.0 || ^4.0.0"
++ },
++ "peerDependenciesMeta": {
++ "zod": {
++ "optional": true
++ }
++ }
++ },
"node_modules/@aws-sdk/checksums": {
"version": "3.1001.5",
"resolved": "https://registry.npmjs.org/@aws-sdk/checksums/-/checksums-3.1001.5.tgz",
@@@ -516,12 +586,30 @@
"node": ">=18.0.0"
}
},
++ "node_modules/@babel/runtime": {
++ "version": "7.29.10",
++ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.10.tgz",
++ "integrity": "sha512-NpugTzbKgGrNe45Ui32cN8vCFFMxVtP/9rtfwv0nP5FRDAjvYBxJcLVpYA9mr1jxP6cnIT7x9LUzuDTbWYYNRA==",
++ "license": "MIT",
++ "engines": {
++ "node": ">=6.9.0"
++ }
++ },
"node_modules/@electric-sql/pglite": {
"version": "0.5.8",
"resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.5.8.tgz",
"integrity": "sha512-n9tsbUOhwx2epK1V0ZG9Ar4SHWUju04dhmzZXiSBXwBoleOvIfals33NAaWgagQVAL4Rbvx/Ptsu3P+pA09f6Q==",
"license": "Apache-2.0"
},
++ "node_modules/@opentelemetry/api": {
++ "version": "1.10.0",
++ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.10.0.tgz",
++ "integrity": "sha512-lMkyes8iHrG+mk6WH7X70MdCLI2RS0FwNBY1PyQ8UemaijE2m5mv5A0Niy3HtbYTFrgkUEQjFMXmSAk2hQDuPQ==",
++ "license": "Apache-2.0",
++ "engines": {
++ "node": ">=8.0.0"
++ }
++ },
"node_modules/@smithy/core": {
"version": "3.36.2",
"resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.36.2.tgz",
@@@ -603,6 +691,12 @@
"node": ">=18.0.0"
}
},
++ "node_modules/@stablelib/base64": {
++ "version": "1.0.1",
++ "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz",
++ "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==",
++ "license": "MIT"
++ },
"node_modules/ajv": {
"version": "8.20.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
@@@ -648,6 +742,12 @@
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"license": "MIT"
},
++ "node_modules/fast-sha256": {
++ "version": "1.3.0",
++ "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz",
++ "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==",
++ "license": "Unlicense"
++ },
"node_modules/fast-uri": {
"version": "3.1.8",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz",
@@@ -664,6 +764,19 @@
],
"license": "BSD-3-Clause"
},
++ "node_modules/json-schema-to-ts": {
++ "version": "3.1.1",
++ "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz",
++ "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==",
++ "license": "MIT",
++ "dependencies": {
++ "@babel/runtime": "^7.18.3",
++ "ts-algebra": "^2.0.0"
++ },
++ "engines": {
++ "node": ">=16"
++ }
++ },
"node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
@@@ -727,10 +840,18 @@
"node": ">=20"
}
},
++ "node_modules/openvibe-example-ai-anthropic": {
++ "resolved": "examples/ai-anthropic",
++ "link": true
++ },
"node_modules/openvibe-example-ai-chat": {
"resolved": "examples/ai-chat",
"link": true
},
++ "node_modules/openvibe-example-ai-python": {
++ "resolved": "examples/ai-python",
++ "link": true
++ },
"node_modules/openvibe-example-browser-app": {
"resolved": "examples/browser-app",
"link": true
@@@ -961,6 +1082,22 @@
"node": ">= 10.x"
}
},
++ "node_modules/standardwebhooks": {
++ "version": "1.1.1",
++ "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.1.1.tgz",
++ "integrity": "sha512-bCbX9ZEyFkWPsRz7Bl3NuQUJohmwGSev/yhr7vhaGPlc4AfIrspIRa6cPTBuI1ItmrTDJ4d/S2hCsfe4+vQGnQ==",
++ "license": "MIT",
++ "dependencies": {
++ "@stablelib/base64": "^1.0.0",
++ "fast-sha256": "^1.3.0"
++ }
++ },
++ "node_modules/ts-algebra": {
++ "version": "2.0.0",
++ "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz",
++ "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==",
++ "license": "MIT"
++ },
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
ddiff --git a/test/structure.test.js b/test/structure.test.js
iindex c7a7e3c..c7754cf 100644
---- a/test/structure.test.js
++++ b/test/structure.test.js
@@@ -11,7 +11,7 @@ const fs = require('node:fs');
const path = require('node:path');
const ROOT = path.join(__dirname, '..');
--const EXPECTED = ['ai-chat', 'browser-app', 'chat-bot', 'event-subscriber', 'game-scores', 'go-live', 'inventory-wear', 'media-uploader', 'mod-manifest', 'node-server-app', 'oauth-app', 'space-deploy', 'tool-job', 'webhook-consumer', 'zone-s3'];
++const EXPECTED = ['ai-anthropic', 'ai-chat', 'ai-python', 'browser-app', 'chat-bot', 'event-subscriber', 'game-scores', 'go-live', 'inventory-wear', 'media-uploader', 'mod-manifest', 'node-server-app', 'oauth-app', 'space-deploy', 'tool-job', 'webhook-consumer', 'zone-s3'];
// The pin every example must share: the repository's own (the pin bot moves them all at once).
const ROOT_PKG = require(path.join(ROOT, 'package.json'));
const SDK = (ROOT_PKG.dependencies || {})['openvibe-sdk'] || (ROOT_PKG.devDependencies || {})['openvibe-sdk'];