openvibers/openvibe.studio Commits
Add llms-full.txt and the agent descriptor (#5)
Changes
nnew file mode 100644@@@ -0,0 +1,22 @@++'use strict';++// What openvibe.studio tells an agent about itself (SEO-2). Every field is something the site really serves; the++// OpenAPI document is the workshop API this site publishes.++module.exports = {++ network: 'OpenVibe',++ site: 'openvibe.studio',++ id: 'studio',++ name: 'OpenVibe.Studio',++ purpose: 'Photo and Light: two open-source creative apps (PhotoCraft, LightCraft) built from pinned release tags and served inside an OpenVibe account.',++ home: 'https://openvibe.studio',++ api: 'https://openvibe.studio/api/v1/studio',++ openapi: 'https://openvibe.studio/openapi.json',++ docs: 'https://openvibe.studio/llms-full.txt',++ llms: 'https://openvibe.studio/llms.txt',++ llmsFull: 'https://openvibe.studio/llms-full.txt',++ sitemap: 'https://openvibe.studio/sitemap.xml',++ policy: {++ robots: 'https://openvibe.network/bot.json',++ auth: ['none', 'Network OIDC'],++ rateLimit: 'The API is rate limited per address; the app bundle files are static and cached.',++ },++};@@@ -8,8 +8,25 @@ const { createPages } = require('./pages');const { createBundleFiles } = require('./bundle-files');const { createMediaClient } = require('./openvibe/media');const { createPicsClient } = require('./openvibe/pics');++const { APPS } = require('./product');++const seo = require('openvibe-shared/seo');++const agentDescriptor = require('./agent-descriptor');const configLib = require('./config');++const SITE_NAME = 'OpenVibe.Studio';++const SUMMARY = 'Photo and Light: two open-source creative apps (PhotoCraft, LightCraft) built from pinned release tags and served inside an OpenVibe account.';++const appText = (id) => {++ const app = APPS.find((one) => one.id === id);++ return app ? `${app.tagline} ${app.what}` : '';++};++// The public pages, for /llms-full.txt: the same pages llms.txt and the sitemap list.++const PUBLIC_PAGES = [++ { title: SITE_NAME, url: '/', text: SUMMARY },++ { title: 'Photo', url: '/photo', text: appText('photo') },++ { title: 'Light', url: '/light', text: appText('light') },++ { title: 'Attribution', url: '/attribution', text: 'The open-source projects Studio hosts, their licences, upstream repositories and the licence files kept with each build.' },++];++function createApp(opts = {}) {const config = opts.config || configLib.load();const sso = opts.sso || createSsoClient({@@@ -48,7 +65,14 @@ function createApp(opts = {}) {'<url><loc>https://openvibe.studio/light</loc></url><url><loc>https://openvibe.studio/attribution</loc></url></urlset>'));{ const legal = require('openvibe-shared/legal'); app.get(legal.PATHS, legal.handler({ id: 'studio', service: 'studio', host: 'openvibe.studio', name: 'OpenVibe.Studio', profile: 'hosting' })); }app.get('/llms.txt', (_req, res) => res.type('text/plain').send(-- '# OpenVibe.Studio\n\nPhoto and Light: two open-source creative apps (PhotoCraft, LightCraft) built from pinned release tags and served inside an OpenVibe account.\n\n- [Home](/)\n- [Photo](/photo)\n- [Light](/light)\n- [Attribution](/attribution)\n- [API docs](/openapi.json)\n'));++ '# OpenVibe.Studio\n\n' + SUMMARY + '\n\n- [Home](/)\n- [Photo](/photo)\n- [Light](/light)\n- [Attribution](/attribution)\n- [API docs](/openapi.json)\n- [Full text for language models](/llms-full.txt)\n- [Agent descriptor](/.well-known/agent.json)\n'));++ // The same pages with their text, for a model that wants the site in one read (llms-full.txt, llmstxt.org).++ app.get('/llms-full.txt', (_req, res) => res.type('text/plain').send(seo.llmsFull({++ site: SITE_NAME, summary: SUMMARY, base: 'https://openvibe.studio',++ sections: [{ title: 'Pages', pages: PUBLIC_PAGES }],++ })));++ // What openvibe.studio tells an agent about itself (SEO-2, openvibe-shared/agent-layer's shape).++ app.get('/.well-known/agent.json', (_req, res) => res.set('Cache-Control', 'public, max-age=3600').json(agentDescriptor));app.get('/openapi.json', (_req, res) => res.sendFile(path.join(__dirname, '..', 'public', 'openapi.json')));app.use('/api/v1/studio', createApi({ sso, media, pics }));// GET /release.json (ADR-016): the navbar's release watch polls it so open tabs learn of a new release (the 11-09@@@ -182,7 +182,26 @@ test('the app catalogue, the attribution page and the small endpoints answer', aassert.match(attributionHtml, /storytold\/lightcraft/);assert.match(attributionHtml, /v0\.19\.0/);assert.equal((await fetch(s.base + '/api/health')).status, 200);-- assert.equal((await fetch(s.base + '/llms.txt')).status, 200);++ const llms = await fetch(s.base + '/llms.txt');++ assert.equal(llms.status, 200);++ const llmsText = await llms.text();++ assert.ok(llmsText.includes('(/llms-full.txt)') && llmsText.includes('(/.well-known/agent.json)'), 'llms.txt names the full text and the agent descriptor');++ const full = await fetch(s.base + '/llms-full.txt');++ assert.equal(full.status, 200);++ assert.match(full.headers.get('content-type'), /^text\/plain/);++ const fullText = await full.text();++ assert.ok(fullText.startsWith('# OpenVibe.Studio\n'), 'llms-full.txt opens with the site heading');++ assert.ok(fullText.length > 500, `llms-full.txt carries the pages (${fullText.length} characters)`);++ for (const page of ['/photo', '/light', '/attribution']) assert.ok(fullText.includes(`URL: https://openvibe.studio${page}`), `llms-full.txt lists ${page}`);++ assert.ok(fullText.includes('PhotoCraft') && fullText.includes('LightCraft'), 'and the apps it serves');++ const agent = await fetch(s.base + '/.well-known/agent.json');++ assert.equal(agent.status, 200);++ assert.match(agent.headers.get('content-type'), /^application\/json/);++ const manifest = await agent.json();++ assert.equal(manifest.id, 'studio');++ assert.equal(manifest.home, 'https://openvibe.studio');++ assert.equal(manifest.llmsFull, 'https://openvibe.studio/llms-full.txt');++ assert.equal(manifest.openapi, 'https://openvibe.studio/openapi.json');assert.equal((await fetch(s.base + '/robots.txt')).status, 200);{ const r = await fetch(s.base + '/release.json'); assert.equal(r.status, 200, 'the navbar release watch polls /release.json'); assert.equal((await r.json()).service, 'studio'); }assert.equal((await fetch(s.base + '/nope')).status, 404);
new file mode 100644
new file mode 100644
@@ -0,0 +1,22 @@
@@ -0,0 +1,22 @@
+'use strict';
+// What openvibe.studio tells an agent about itself (SEO-2). Every field is something the site really serves; the
+// OpenAPI document is the workshop API this site publishes.
+module.exports = {
+ network: 'OpenVibe',
+ site: 'openvibe.studio',
+ id: 'studio',
+ name: 'OpenVibe.Studio',
+ purpose: 'Photo and Light: two open-source creative apps (PhotoCraft, LightCraft) built from pinned release tags and served inside an OpenVibe account.',
+ home: 'https://openvibe.studio',
+ api: 'https://openvibe.studio/api/v1/studio',
+ openapi: 'https://openvibe.studio/openapi.json',
+ docs: 'https://openvibe.studio/llms-full.txt',
+ llms: 'https://openvibe.studio/llms.txt',
+ llmsFull: 'https://openvibe.studio/llms-full.txt',
+ sitemap: 'https://openvibe.studio/sitemap.xml',
+ policy: {
+ robots: 'https://openvibe.network/bot.json',
+ auth: ['none', 'Network OIDC'],
+ rateLimit: 'The API is rate limited per address; the app bundle files are static and cached.',
+ },
+};
@@ -8,8 +8,25 @@ const { createPages } = require('./pages');
@@ -8,8 +8,25 @@ const { createPages } = require('./pages');
const { createBundleFiles } = require('./bundle-files');
const { createBundleFiles } = require('./bundle-files');
const { createMediaClient } = require('./openvibe/media');
const { createMediaClient } = require('./openvibe/media');
const { createPicsClient } = require('./openvibe/pics');
const { createPicsClient } = require('./openvibe/pics');
+const { APPS } = require('./product');
+const seo = require('openvibe-shared/seo');
+const agentDescriptor = require('./agent-descriptor');
const configLib = require('./config');
const configLib = require('./config');
+const SITE_NAME = 'OpenVibe.Studio';
+const SUMMARY = 'Photo and Light: two open-source creative apps (PhotoCraft, LightCraft) built from pinned release tags and served inside an OpenVibe account.';
+const appText = (id) => {
+ const app = APPS.find((one) => one.id === id);
+ return app ? `${app.tagline} ${app.what}` : '';
+};
+// The public pages, for /llms-full.txt: the same pages llms.txt and the sitemap list.
+const PUBLIC_PAGES = [
+ { title: SITE_NAME, url: '/', text: SUMMARY },
+ { title: 'Photo', url: '/photo', text: appText('photo') },
+ { title: 'Light', url: '/light', text: appText('light') },
+ { title: 'Attribution', url: '/attribution', text: 'The open-source projects Studio hosts, their licences, upstream repositories and the licence files kept with each build.' },
+];
+
function createApp(opts = {}) {
function createApp(opts = {}) {
const config = opts.config || configLib.load();
const config = opts.config || configLib.load();
const sso = opts.sso || createSsoClient({
const sso = opts.sso || createSsoClient({
@@ -48,7 +65,14 @@ function createApp(opts = {}) {
@@ -48,7 +65,14 @@ function createApp(opts = {}) {
'<url><loc>https://openvibe.studio/light</loc></url><url><loc>https://openvibe.studio/attribution</loc></url></urlset>'));
'<url><loc>https://openvibe.studio/light</loc></url><url><loc>https://openvibe.studio/attribution</loc></url></urlset>'));
{ const legal = require('openvibe-shared/legal'); app.get(legal.PATHS, legal.handler({ id: 'studio', service: 'studio', host: 'openvibe.studio', name: 'OpenVibe.Studio', profile: 'hosting' })); }
{ const legal = require('openvibe-shared/legal'); app.get(legal.PATHS, legal.handler({ id: 'studio', service: 'studio', host: 'openvibe.studio', name: 'OpenVibe.Studio', profile: 'hosting' })); }
app.get('/llms.txt', (_req, res) => res.type('text/plain').send(
app.get('/llms.txt', (_req, res) => res.type('text/plain').send(
- '# OpenVibe.Studio\n\nPhoto and Light: two open-source creative apps (PhotoCraft, LightCraft) built from pinned release tags and served inside an OpenVibe account.\n\n- [Home](/)\n- [Photo](/photo)\n- [Light](/light)\n- [Attribution](/attribution)\n- [API docs](/openapi.json)\n'));
+ '# OpenVibe.Studio\n\n' + SUMMARY + '\n\n- [Home](/)\n- [Photo](/photo)\n- [Light](/light)\n- [Attribution](/attribution)\n- [API docs](/openapi.json)\n- [Full text for language models](/llms-full.txt)\n- [Agent descriptor](/.well-known/agent.json)\n'));
+ // The same pages with their text, for a model that wants the site in one read (llms-full.txt, llmstxt.org).
+ app.get('/llms-full.txt', (_req, res) => res.type('text/plain').send(seo.llmsFull({
+ site: SITE_NAME, summary: SUMMARY, base: 'https://openvibe.studio',
+ sections: [{ title: 'Pages', pages: PUBLIC_PAGES }],
+ })));
+ // What openvibe.studio tells an agent about itself (SEO-2, openvibe-shared/agent-layer's shape).
+ app.get('/.well-known/agent.json', (_req, res) => res.set('Cache-Control', 'public, max-age=3600').json(agentDescriptor));
app.get('/openapi.json', (_req, res) => res.sendFile(path.join(__dirname, '..', 'public', 'openapi.json')));
app.get('/openapi.json', (_req, res) => res.sendFile(path.join(__dirname, '..', 'public', 'openapi.json')));
app.use('/api/v1/studio', createApi({ sso, media, pics }));
app.use('/api/v1/studio', createApi({ sso, media, pics }));
// GET /release.json (ADR-016): the navbar's release watch polls it so open tabs learn of a new release (the 11-09
// GET /release.json (ADR-016): the navbar's release watch polls it so open tabs learn of a new release (the 11-09
@@ -182,7 +182,26 @@ test('the app catalogue, the attribution page and the small endpoints answer', a
@@ -182,7 +182,26 @@ test('the app catalogue, the attribution page and the small endpoints answer', a
assert.match(attributionHtml, /storytold\/lightcraft/);
assert.match(attributionHtml, /storytold\/lightcraft/);
assert.match(attributionHtml, /v0\.19\.0/);
assert.match(attributionHtml, /v0\.19\.0/);
assert.equal((await fetch(s.base + '/api/health')).status, 200);
assert.equal((await fetch(s.base + '/api/health')).status, 200);
- assert.equal((await fetch(s.base + '/llms.txt')).status, 200);
+ const llms = await fetch(s.base + '/llms.txt');
+ assert.equal(llms.status, 200);
+ const llmsText = await llms.text();
+ assert.ok(llmsText.includes('(/llms-full.txt)') && llmsText.includes('(/.well-known/agent.json)'), 'llms.txt names the full text and the agent descriptor');
+ const full = await fetch(s.base + '/llms-full.txt');
+ assert.equal(full.status, 200);
+ assert.match(full.headers.get('content-type'), /^text\/plain/);
+ const fullText = await full.text();
+ assert.ok(fullText.startsWith('# OpenVibe.Studio\n'), 'llms-full.txt opens with the site heading');
+ assert.ok(fullText.length > 500, `llms-full.txt carries the pages (${fullText.length} characters)`);
+ for (const page of ['/photo', '/light', '/attribution']) assert.ok(fullText.includes(`URL: https://openvibe.studio${page}`), `llms-full.txt lists ${page}`);
+ assert.ok(fullText.includes('PhotoCraft') && fullText.includes('LightCraft'), 'and the apps it serves');
+ const agent = await fetch(s.base + '/.well-known/agent.json');
+ assert.equal(agent.status, 200);
+ assert.match(agent.headers.get('content-type'), /^application\/json/);
+ const manifest = await agent.json();
+ assert.equal(manifest.id, 'studio');
+ assert.equal(manifest.home, 'https://openvibe.studio');
+ assert.equal(manifest.llmsFull, 'https://openvibe.studio/llms-full.txt');
+ assert.equal(manifest.openapi, 'https://openvibe.studio/openapi.json');
assert.equal((await fetch(s.base + '/robots.txt')).status, 200);
assert.equal((await fetch(s.base + '/robots.txt')).status, 200);
{ const r = await fetch(s.base + '/release.json'); assert.equal(r.status, 200, 'the navbar release watch polls /release.json'); assert.equal((await r.json()).service, 'studio'); }
{ const r = await fetch(s.base + '/release.json'); assert.equal(r.status, 200, 'the navbar release watch polls /release.json'); assert.equal((await r.json()).service, 'studio'); }
assert.equal((await fetch(s.base + '/nope')).status, 404);
assert.equal((await fetch(s.base + '/nope')).status, 404);