openvibers/openvibe.services Commits
openvibe-shared v4.108.0 (was v4.107.4) (#208)
Changes
@@@ -287,5 +287,5 @@ Part of the [OpenVibe network](https://openvibe.network). Built in the open by [<!-- versions:start -->- openvibe-contracts: v0.222.0- openvibe-sdk: v0.82.0--- openvibe-shared: v4.107.4++- openvibe-shared: v4.108.0<!-- versions:end -->@@@ -9,11 +9,11 @@"domain": "openvibe.services","contracts": "openvibe-contracts v0.222.0","sdk": "openvibe-sdk v0.82.0",-- "shared": "openvibe-shared v4.107.4",++ "shared": "openvibe-shared v4.108.0","packages": ["openvibe-contracts v0.222.0","openvibe-sdk v0.82.0",-- "openvibe-shared v4.107.4"++ "openvibe-shared v4.108.0"],"capabilities": ["services.resource.read",@@@ -18,7 +18,7 @@"iovalkey": "^0.4.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.107.4",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.108.0","pg": "^8.23.0"},"devDependencies": {@@@ -1209,9 +1209,9 @@}},"node_modules/openvibe-shared": {-- "version": "4.107.4",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.107.4",-- "integrity": "sha512-tThN0m3KMncEHVcBOvE+ysHjsZTlVIDLG1P+/i2Mulmxfc/AUpebAXhpWWrut50OChZ8o+HeePxsSrPI8mUQOQ==",++ "version": "4.108.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.108.0",++ "integrity": "sha512-NPzwjxpzGIpFpIm4uw3+VwHfsxbJ2+cD9yuhRzSwolSwqv2kCmwaVtHJ0DA9gKO3K8PCBfIP3UpKzpuSbYEJ+g==","license": "MIT","dependencies": {"jsonwebtoken": "^9.0.0",@@@ -29,7 +29,7 @@"iovalkey": "^0.4.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.107.4",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.108.0","pg": "^8.23.0"},"devDependencies": {@@@ -203,53 +203,59 @@ function createDiscoveryRoutes(ctx) {r.get('/llms.txt', (_req, res) => {const firstContract = docs.contracts[0] ? docs.contracts[0].id : null;const firstService = (contracts.openapi.index()[0] || {}).service || null;-- const machine = [-- { title: 'Sitemap', url: abs('/sitemap.xml'), note: 'the public pages below, each with a real lastmod' },-- { title: 'robots.txt', url: abs('/robots.txt'), note: 'search and AI crawlers are welcome on the public pages' },-- { title: 'Full text for language models', url: abs('/llms-full.txt'), note: 'every fixed public page, one line each' },-- { title: 'Release metadata (JSON)', url: abs('/release.json'), note: 'this service\'s current release, per ADR-016' },-- { title: 'MCP server (the whole network, read-only)', url: abs('/mcp'), note: 'POST JSON-RPC: openvibe_sites, openvibe_operations, openvibe_get (anonymous GETs of documented operations)' },-- ];++ // The API documents are what an agent came for, so they stay in a main section; the .json URLs are files and++ // keep their URL in either place.++ const machine = [];if (firstContract) machine.push({ title: 'Contract JSON Schema', url: abs(`/docs/contracts/${firstContract}.json`), note: 'append .json to any contract page for its schema' });if (firstService) machine.push({ title: 'Service OpenAPI', url: abs(`/docs/api/${firstService}.json`), note: 'append .json to any API explorer page for its OpenAPI document' });++ // Pages whose Markdown twin answers 404 (llms-twins-probe, 11-03) keep their URL and are marked (HTML).res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(seo.llmsTxt({name: SITE_NAME,++ // Each of this site's own pages is linked at its Markdown twin (AGT-5; openvibe-shared 4.108.0 leaves++ // files, /api/ and /.well-known/ paths and other sites' links as they are).++ markdown: true,++ base: site,summary: 'OpenVibe.Services: the open developer platform of OpenVibe — projects, apps, scoped credentials and capability grants over OpenVibe.Network\'s API, usage and project export, app and mod releases, one resource index over every service, reference documentation generated from the pinned contracts and SDK, and OAuth, webhook and manifest tools.',-- details: 'Every page is server-rendered and readable without JavaScript, and every one is public: no account is needed to read the docs or use the OAuth, webhook and manifest tools. Projects, apps, credentials and grants belong to OpenVibe.Network; the pages that manage them need a signed-in OpenVibe account and are deliberately not listed here or in the sitemap. The reference under /docs is generated at boot from the exact openvibe-contracts and openvibe-sdk versions the platform runs, so it cannot drift from them.',++ details: 'Every page is server-rendered and readable without JavaScript, and every one is public: no account is needed to read the docs or use the OAuth, webhook and manifest tools. Projects, apps, credentials and grants belong to OpenVibe.Network; the pages that manage them need a signed-in OpenVibe account and are deliberately not listed here or in the sitemap. The reference under /docs is generated at boot from the exact openvibe-contracts and openvibe-sdk versions the platform runs, so it cannot drift from them. MCP server: POST https://openvibe.services/mcp (JSON-RPC; tools openvibe_sites, openvibe_operations and openvibe_get, anonymous GETs of documented operations) — an endpoint, not a page.',sections: [{ title: 'Start here', links: [{ title: 'The developer platform', url: abs('/'), note: 'what the platform is for and how an account becomes an integration' },{ title: 'Reference documentation', url: abs('/docs'), note: 'generated from the pinned contracts and SDK' },-- { title: 'What shipped on OpenVibe.Services', url: abs('/updates') },-- { title: 'Transparency', url: abs('/policy/transparency'), note: 'what the portal stores, what it does not, and what works today' },] },{ title: 'Reference', links: [{ title: 'API explorer', url: abs('/docs/api'), note: 'each service\'s OpenAPI document, per page as .json' },-- { title: 'Contracts', url: abs('/docs/contracts'), note: 'schemas, field tables and fixtures, per page as .json' },-- { title: 'Capabilities', url: abs('/docs/capabilities') },-- { title: 'Events', url: abs('/docs/events') },-- { title: 'Services', url: abs('/docs/services') },-- { title: 'Tools', url: abs('/docs/tools') },-- { title: 'Coding harnesses', url: 'https://openvibe.codes/harnesses', note: 'the coding agents OpenVibe.Codes routes a task to (another site)' },-- { title: 'SDK', url: abs('/docs/sdk') },++ { title: 'Contracts', url: abs('/docs/contracts'), html: true, note: 'schemas, field tables and fixtures, per page as .json' },++ { title: 'Capabilities', url: abs('/docs/capabilities'), html: true },++ { title: 'Events', url: abs('/docs/events'), html: true },++ { title: 'Services', url: abs('/docs/services'), html: true },++ { title: 'Tools', url: abs('/docs/tools'), html: true },++ { title: 'SDK', url: abs('/docs/sdk'), html: true },] },{ title: 'Extend OpenVibe', links: [{ title: 'Mod the network', url: abs('/mod'), note: 'every way to extend OpenVibe today, and where packaged mods stand' },] },{ title: 'Developer tools', links: [-- { title: 'OAuth callback helper', url: abs('/oauth'), note: 'authorization code + PKCE, with a callback that never exchanges the code' },-- { title: 'Webhook signature tester', url: abs('/tools/webhooks'), note: 'decides as a receiver requiring signature v2 does' },-- { title: 'Manifest validation', url: abs('/manifests/validate'), note: 'app and mod manifests, validated against the pinned contracts' },-- ] },-- { title: 'Policy', links: [-- { title: 'Policy home', url: abs('/policy') },-- { title: 'Proposals and decisions', url: abs('/policy/rfc') },-- { title: 'Compatibility and deprecation', url: abs('/policy/compatibility') },-- { title: 'Licensing', url: abs('/policy/licensing') },-- ...COMMUNITY.map((g) => ({ title: g.title, url: `${CODES_ORIGIN}/policy/${g.slug}`, note: 'on OpenVibe.Codes' })),++ { title: 'OAuth callback helper', url: abs('/oauth'), html: true, note: 'authorization code + PKCE, with a callback that never exchanges the code' },++ { title: 'Webhook signature tester', url: abs('/tools/webhooks'), html: true, note: 'decides as a receiver requiring signature v2 does' },++ { title: 'Manifest validation', url: abs('/manifests/validate'), html: true, note: 'app and mod manifests, validated against the pinned contracts' },] },{ title: 'Machine-readable', links: machine },],++ // llmstxt.org's "## Optional": what an agent may skip when it needs a shorter context.++ optional: [++ { title: 'What shipped on OpenVibe.Services', url: abs('/updates') },++ { title: 'Sitemap', url: abs('/sitemap.xml'), note: 'the public pages above, each with a real lastmod' },++ { title: 'robots.txt', url: abs('/robots.txt'), note: 'search and AI crawlers are welcome on the public pages' },++ { title: 'Full text for language models', url: abs('/llms-full.txt'), note: 'every fixed public page, one line each' },++ { title: 'Release metadata (JSON)', url: abs('/release.json'), note: 'this service\'s current release, per ADR-016' },++ { title: 'Policy home', url: abs('/policy') },++ { title: 'Proposals and decisions', url: abs('/policy/rfc') },++ { title: 'Compatibility and deprecation', url: abs('/policy/compatibility') },++ { title: 'Licensing', url: abs('/policy/licensing') },++ { title: 'Transparency', url: abs('/policy/transparency'), note: 'what the portal stores, what it does not, and what works today' },++ ...COMMUNITY.map((g) => ({ title: g.title, url: `${CODES_ORIGIN}/policy/${g.slug}`, note: 'on OpenVibe.Codes' })),++ { title: 'Coding harnesses', url: 'https://openvibe.codes/harnesses', note: 'the coding agents OpenVibe.Codes routes a task to (another site)' },++ ],}));});@@@ -47,7 +47,7 @@ const PRIVATE = ['/projects', '/staff', '/auth/', '/oauth/test-callback'];assert.match(r.headers.get('content-type'), /^text\/plain/);assert.match(r.headers.get('cache-control') || '', /public, max-age=\d+/);assert.ok(r.text.startsWith('# OpenVibe.Services'), 'no title');-- assert.ok(r.text.includes('(https://openvibe.services/docs)'), 'no reference link');++ assert.ok(r.text.includes('(https://openvibe.services/docs.md)'), 'no reference link at its twin');assert.ok(r.text.includes('(https://openvibe.codes/harnesses)'), 'llms.txt points at the harness catalog on openvibe.codes');assert.ok(r.text.includes('https://openvibe.services/sitemap.xml'), 'sitemap not listed');assert.ok(r.text.includes('(https://openvibe.services/llms-full.txt)'), 'llms-full.txt not listed');@@@ -55,6 +55,23 @@ const PRIVATE = ['/projects', '/staff', '/auth/', '/oauth/test-callback'];assert.ok(!r.text.includes('https://openvibe.services/api/'), 'api path in llms.txt');});++ await check('llms.txt: every page at its Markdown twin, and an Optional section last (AGT-5)', async () => {++ const r = await t.get('/llms.txt');++ assert.ok(r.text.includes('(https://openvibe.services/index.md)'), 'the home is /index.md');++ assert.ok(/\n## Optional\n/.test(r.text), 'no Optional section');++ assert.strictEqual(r.text.lastIndexOf('\n## '), r.text.indexOf('\n## Optional'), 'Optional is the last section');++ // A page whose twin answers 404 keeps its URL and says (HTML); a file keeps its URL.++ assert.ok(r.text.includes('(https://openvibe.services/docs/contracts) (HTML)'), 'the twin-less contracts page is marked (HTML)');++ assert.ok(!/\]\(https:\/\/openvibe\.services\/mcp\)/.test(r.text), '/mcp must not be a page link');++ assert.ok(/MCP server: POST https:\/\/openvibe\.services\/mcp/.test(r.text), 'the MCP endpoint is named in the details');++ const own = [...r.text.matchAll(/\]\((https:\/\/openvibe\.services[^)\s]*)\)( \(HTML\))?/g)];++ assert.ok(own.length > 10, String(own.length));++ for (const [, url, html] of own) {++ const p = new URL(url).pathname;++ assert.ok(p.endsWith('.md') || /\.[a-z0-9]+$/.test(p) || html, `${url} is a twin, a file or marked (HTML)`);++ }++ });++await check('llms-full.txt: 200 text/plain, the site title, and every page the sitemap lists', async () => {const r = await t.get('/llms-full.txt');assert.strictEqual(r.status, 200);
@@ -287,5 +287,5 @@ Part of the [OpenVibe network](https://openvibe.network). Built in the open by [
@@ -287,5 +287,5 @@ Part of the [OpenVibe network](https://openvibe.network). Built in the open by [
<!-- versions:start -->
<!-- versions:start -->
- openvibe-contracts: v0.222.0
- openvibe-contracts: v0.222.0
- openvibe-sdk: v0.82.0
- openvibe-sdk: v0.82.0
-- openvibe-shared: v4.107.4
+- openvibe-shared: v4.108.0
<!-- versions:end -->
<!-- versions:end -->
@@ -9,11 +9,11 @@
@@ -9,11 +9,11 @@
"domain": "openvibe.services",
"domain": "openvibe.services",
"contracts": "openvibe-contracts v0.222.0",
"contracts": "openvibe-contracts v0.222.0",
"sdk": "openvibe-sdk v0.82.0",
"sdk": "openvibe-sdk v0.82.0",
- "shared": "openvibe-shared v4.107.4",
+ "shared": "openvibe-shared v4.108.0",
"packages": [
"packages": [
"openvibe-contracts v0.222.0",
"openvibe-contracts v0.222.0",
"openvibe-sdk v0.82.0",
"openvibe-sdk v0.82.0",
- "openvibe-shared v4.107.4"
+ "openvibe-shared v4.108.0"
],
],
"capabilities": [
"capabilities": [
"services.resource.read",
"services.resource.read",
@@ -18,7 +18,7 @@
@@ -18,7 +18,7 @@
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.107.4",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.108.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
},
},
"devDependencies": {
"devDependencies": {
@@ -1209,9 +1209,9 @@
@@ -1209,9 +1209,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"node_modules/openvibe-shared": {
- "version": "4.107.4",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.107.4",
- "integrity": "sha512-tThN0m3KMncEHVcBOvE+ysHjsZTlVIDLG1P+/i2Mulmxfc/AUpebAXhpWWrut50OChZ8o+HeePxsSrPI8mUQOQ==",
+ "version": "4.108.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.108.0",
+ "integrity": "sha512-NPzwjxpzGIpFpIm4uw3+VwHfsxbJ2+cD9yuhRzSwolSwqv2kCmwaVtHJ0DA9gKO3K8PCBfIP3UpKzpuSbYEJ+g==",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0",
"jsonwebtoken": "^9.0.0",
@@ -29,7 +29,7 @@
@@ -29,7 +29,7 @@
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.107.4",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.108.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
},
},
"devDependencies": {
"devDependencies": {
@@ -203,53 +203,59 @@ function createDiscoveryRoutes(ctx) {
@@ -203,53 +203,59 @@ function createDiscoveryRoutes(ctx) {
r.get('/llms.txt', (_req, res) => {
r.get('/llms.txt', (_req, res) => {
const firstContract = docs.contracts[0] ? docs.contracts[0].id : null;
const firstContract = docs.contracts[0] ? docs.contracts[0].id : null;
const firstService = (contracts.openapi.index()[0] || {}).service || null;
const firstService = (contracts.openapi.index()[0] || {}).service || null;
- const machine = [
- { title: 'Sitemap', url: abs('/sitemap.xml'), note: 'the public pages below, each with a real lastmod' },
- { title: 'robots.txt', url: abs('/robots.txt'), note: 'search and AI crawlers are welcome on the public pages' },
- { title: 'Full text for language models', url: abs('/llms-full.txt'), note: 'every fixed public page, one line each' },
- { title: 'Release metadata (JSON)', url: abs('/release.json'), note: 'this service\'s current release, per ADR-016' },
- { title: 'MCP server (the whole network, read-only)', url: abs('/mcp'), note: 'POST JSON-RPC: openvibe_sites, openvibe_operations, openvibe_get (anonymous GETs of documented operations)' },
- ];
+ // The API documents are what an agent came for, so they stay in a main section; the .json URLs are files and
+ // keep their URL in either place.
+ const machine = [];
if (firstContract) machine.push({ title: 'Contract JSON Schema', url: abs(`/docs/contracts/${firstContract}.json`), note: 'append .json to any contract page for its schema' });
if (firstContract) machine.push({ title: 'Contract JSON Schema', url: abs(`/docs/contracts/${firstContract}.json`), note: 'append .json to any contract page for its schema' });
if (firstService) machine.push({ title: 'Service OpenAPI', url: abs(`/docs/api/${firstService}.json`), note: 'append .json to any API explorer page for its OpenAPI document' });
if (firstService) machine.push({ title: 'Service OpenAPI', url: abs(`/docs/api/${firstService}.json`), note: 'append .json to any API explorer page for its OpenAPI document' });
+ // Pages whose Markdown twin answers 404 (llms-twins-probe, 11-03) keep their URL and are marked (HTML).
res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(seo.llmsTxt({
res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(seo.llmsTxt({
name: SITE_NAME,
name: SITE_NAME,
+ // Each of this site's own pages is linked at its Markdown twin (AGT-5; openvibe-shared 4.108.0 leaves
+ // files, /api/ and /.well-known/ paths and other sites' links as they are).
+ markdown: true,
+ base: site,
summary: 'OpenVibe.Services: the open developer platform of OpenVibe — projects, apps, scoped credentials and capability grants over OpenVibe.Network\'s API, usage and project export, app and mod releases, one resource index over every service, reference documentation generated from the pinned contracts and SDK, and OAuth, webhook and manifest tools.',
summary: 'OpenVibe.Services: the open developer platform of OpenVibe — projects, apps, scoped credentials and capability grants over OpenVibe.Network\'s API, usage and project export, app and mod releases, one resource index over every service, reference documentation generated from the pinned contracts and SDK, and OAuth, webhook and manifest tools.',
- details: 'Every page is server-rendered and readable without JavaScript, and every one is public: no account is needed to read the docs or use the OAuth, webhook and manifest tools. Projects, apps, credentials and grants belong to OpenVibe.Network; the pages that manage them need a signed-in OpenVibe account and are deliberately not listed here or in the sitemap. The reference under /docs is generated at boot from the exact openvibe-contracts and openvibe-sdk versions the platform runs, so it cannot drift from them.',
+ details: 'Every page is server-rendered and readable without JavaScript, and every one is public: no account is needed to read the docs or use the OAuth, webhook and manifest tools. Projects, apps, credentials and grants belong to OpenVibe.Network; the pages that manage them need a signed-in OpenVibe account and are deliberately not listed here or in the sitemap. The reference under /docs is generated at boot from the exact openvibe-contracts and openvibe-sdk versions the platform runs, so it cannot drift from them. MCP server: POST https://openvibe.services/mcp (JSON-RPC; tools openvibe_sites, openvibe_operations and openvibe_get, anonymous GETs of documented operations) — an endpoint, not a page.',
sections: [
sections: [
{ title: 'Start here', links: [
{ title: 'Start here', links: [
{ title: 'The developer platform', url: abs('/'), note: 'what the platform is for and how an account becomes an integration' },
{ title: 'The developer platform', url: abs('/'), note: 'what the platform is for and how an account becomes an integration' },
{ title: 'Reference documentation', url: abs('/docs'), note: 'generated from the pinned contracts and SDK' },
{ title: 'Reference documentation', url: abs('/docs'), note: 'generated from the pinned contracts and SDK' },
- { title: 'What shipped on OpenVibe.Services', url: abs('/updates') },
- { title: 'Transparency', url: abs('/policy/transparency'), note: 'what the portal stores, what it does not, and what works today' },
] },
] },
{ title: 'Reference', links: [
{ title: 'Reference', links: [
{ title: 'API explorer', url: abs('/docs/api'), note: 'each service\'s OpenAPI document, per page as .json' },
{ title: 'API explorer', url: abs('/docs/api'), note: 'each service\'s OpenAPI document, per page as .json' },
- { title: 'Contracts', url: abs('/docs/contracts'), note: 'schemas, field tables and fixtures, per page as .json' },
- { title: 'Capabilities', url: abs('/docs/capabilities') },
- { title: 'Events', url: abs('/docs/events') },
- { title: 'Services', url: abs('/docs/services') },
- { title: 'Tools', url: abs('/docs/tools') },
- { title: 'Coding harnesses', url: 'https://openvibe.codes/harnesses', note: 'the coding agents OpenVibe.Codes routes a task to (another site)' },
- { title: 'SDK', url: abs('/docs/sdk') },
+ { title: 'Contracts', url: abs('/docs/contracts'), html: true, note: 'schemas, field tables and fixtures, per page as .json' },
+ { title: 'Capabilities', url: abs('/docs/capabilities'), html: true },
+ { title: 'Events', url: abs('/docs/events'), html: true },
+ { title: 'Services', url: abs('/docs/services'), html: true },
+ { title: 'Tools', url: abs('/docs/tools'), html: true },
+ { title: 'SDK', url: abs('/docs/sdk'), html: true },
] },
] },
{ title: 'Extend OpenVibe', links: [
{ title: 'Extend OpenVibe', links: [
{ title: 'Mod the network', url: abs('/mod'), note: 'every way to extend OpenVibe today, and where packaged mods stand' },
{ title: 'Mod the network', url: abs('/mod'), note: 'every way to extend OpenVibe today, and where packaged mods stand' },
] },
] },
{ title: 'Developer tools', links: [
{ title: 'Developer tools', links: [
- { title: 'OAuth callback helper', url: abs('/oauth'), note: 'authorization code + PKCE, with a callback that never exchanges the code' },
- { title: 'Webhook signature tester', url: abs('/tools/webhooks'), note: 'decides as a receiver requiring signature v2 does' },
- { title: 'Manifest validation', url: abs('/manifests/validate'), note: 'app and mod manifests, validated against the pinned contracts' },
- ] },
- { title: 'Policy', links: [
- { title: 'Policy home', url: abs('/policy') },
- { title: 'Proposals and decisions', url: abs('/policy/rfc') },
- { title: 'Compatibility and deprecation', url: abs('/policy/compatibility') },
- { title: 'Licensing', url: abs('/policy/licensing') },
- ...COMMUNITY.map((g) => ({ title: g.title, url: `${CODES_ORIGIN}/policy/${g.slug}`, note: 'on OpenVibe.Codes' })),
+ { title: 'OAuth callback helper', url: abs('/oauth'), html: true, note: 'authorization code + PKCE, with a callback that never exchanges the code' },
+ { title: 'Webhook signature tester', url: abs('/tools/webhooks'), html: true, note: 'decides as a receiver requiring signature v2 does' },
+ { title: 'Manifest validation', url: abs('/manifests/validate'), html: true, note: 'app and mod manifests, validated against the pinned contracts' },
] },
] },
{ title: 'Machine-readable', links: machine },
{ title: 'Machine-readable', links: machine },
],
],
+ // llmstxt.org's "## Optional": what an agent may skip when it needs a shorter context.
+ optional: [
+ { title: 'What shipped on OpenVibe.Services', url: abs('/updates') },
+ { title: 'Sitemap', url: abs('/sitemap.xml'), note: 'the public pages above, each with a real lastmod' },
+ { title: 'robots.txt', url: abs('/robots.txt'), note: 'search and AI crawlers are welcome on the public pages' },
+ { title: 'Full text for language models', url: abs('/llms-full.txt'), note: 'every fixed public page, one line each' },
+ { title: 'Release metadata (JSON)', url: abs('/release.json'), note: 'this service\'s current release, per ADR-016' },
+ { title: 'Policy home', url: abs('/policy') },
+ { title: 'Proposals and decisions', url: abs('/policy/rfc') },
+ { title: 'Compatibility and deprecation', url: abs('/policy/compatibility') },
+ { title: 'Licensing', url: abs('/policy/licensing') },
+ { title: 'Transparency', url: abs('/policy/transparency'), note: 'what the portal stores, what it does not, and what works today' },
+ ...COMMUNITY.map((g) => ({ title: g.title, url: `${CODES_ORIGIN}/policy/${g.slug}`, note: 'on OpenVibe.Codes' })),
+ { title: 'Coding harnesses', url: 'https://openvibe.codes/harnesses', note: 'the coding agents OpenVibe.Codes routes a task to (another site)' },
+ ],
}));
}));
});
});
@@ -47,7 +47,7 @@ const PRIVATE = ['/projects', '/staff', '/auth/', '/oauth/test-callback'];
@@ -47,7 +47,7 @@ const PRIVATE = ['/projects', '/staff', '/auth/', '/oauth/test-callback'];
assert.match(r.headers.get('content-type'), /^text\/plain/);
assert.match(r.headers.get('content-type'), /^text\/plain/);
assert.match(r.headers.get('cache-control') || '', /public, max-age=\d+/);
assert.match(r.headers.get('cache-control') || '', /public, max-age=\d+/);
assert.ok(r.text.startsWith('# OpenVibe.Services'), 'no title');
assert.ok(r.text.startsWith('# OpenVibe.Services'), 'no title');
- assert.ok(r.text.includes('(https://openvibe.services/docs)'), 'no reference link');
+ assert.ok(r.text.includes('(https://openvibe.services/docs.md)'), 'no reference link at its twin');
assert.ok(r.text.includes('(https://openvibe.codes/harnesses)'), 'llms.txt points at the harness catalog on openvibe.codes');
assert.ok(r.text.includes('(https://openvibe.codes/harnesses)'), 'llms.txt points at the harness catalog on openvibe.codes');
assert.ok(r.text.includes('https://openvibe.services/sitemap.xml'), 'sitemap not listed');
assert.ok(r.text.includes('https://openvibe.services/sitemap.xml'), 'sitemap not listed');
assert.ok(r.text.includes('(https://openvibe.services/llms-full.txt)'), 'llms-full.txt not listed');
assert.ok(r.text.includes('(https://openvibe.services/llms-full.txt)'), 'llms-full.txt not listed');
@@ -55,6 +55,23 @@ const PRIVATE = ['/projects', '/staff', '/auth/', '/oauth/test-callback'];
@@ -55,6 +55,23 @@ const PRIVATE = ['/projects', '/staff', '/auth/', '/oauth/test-callback'];
assert.ok(!r.text.includes('https://openvibe.services/api/'), 'api path in llms.txt');
assert.ok(!r.text.includes('https://openvibe.services/api/'), 'api path in llms.txt');
});
});
+ await check('llms.txt: every page at its Markdown twin, and an Optional section last (AGT-5)', async () => {
+ const r = await t.get('/llms.txt');
+ assert.ok(r.text.includes('(https://openvibe.services/index.md)'), 'the home is /index.md');
+ assert.ok(/\n## Optional\n/.test(r.text), 'no Optional section');
+ assert.strictEqual(r.text.lastIndexOf('\n## '), r.text.indexOf('\n## Optional'), 'Optional is the last section');
+ // A page whose twin answers 404 keeps its URL and says (HTML); a file keeps its URL.
+ assert.ok(r.text.includes('(https://openvibe.services/docs/contracts) (HTML)'), 'the twin-less contracts page is marked (HTML)');
+ assert.ok(!/\]\(https:\/\/openvibe\.services\/mcp\)/.test(r.text), '/mcp must not be a page link');
+ assert.ok(/MCP server: POST https:\/\/openvibe\.services\/mcp/.test(r.text), 'the MCP endpoint is named in the details');
+ const own = [...r.text.matchAll(/\]\((https:\/\/openvibe\.services[^)\s]*)\)( \(HTML\))?/g)];
+ assert.ok(own.length > 10, String(own.length));
+ for (const [, url, html] of own) {
+ const p = new URL(url).pathname;
+ assert.ok(p.endsWith('.md') || /\.[a-z0-9]+$/.test(p) || html, `${url} is a twin, a file or marked (HTML)`);
+ }
+ });
+
await check('llms-full.txt: 200 text/plain, the site title, and every page the sitemap lists', async () => {
await check('llms-full.txt: 200 text/plain, the site title, and every page the sitemap lists', async () => {
const r = await t.get('/llms-full.txt');
const r = await t.get('/llms-full.txt');
assert.strictEqual(r.status, 200);
assert.strictEqual(r.status, 200);