openvibers/openvibe.chat Commits
/vibe, /developers and /updates have the Markdown twins they name (#347)
Changes
@@@ -3,7 +3,9 @@// page's own route in this router, anonymously and in-process, so a twin cannot drift from its page and a// signed-in view is never used. Only pages whose rendered robots tag starts with "index" get a twin: private// servers and rooms, /messages, /settings and the SEO-7 gated pages stay noindex and have none.--const PUBLIC_PATH_RE = /^\/(?:$|servers$|rooms$|s\/[a-z0-9-]+(?:\/[a-z0-9-]+)?$|r\/[a-z0-9-]+$)/i;++// /vibe, /developers and /updates are public pages too: each named a twin in its head and Link header that answered++// 404 (11-03, tools/llms-twins-probe.js).++const PUBLIC_PATH_RE = /^\/(?:$|servers$|rooms$|vibe$|developers$|updates$|s\/[a-z0-9-]+(?:\/[a-z0-9-]+)?$|r\/[a-z0-9-]+$)/i;/** An anonymous GET request for one path, as the router's handlers expect it. */function anonymousRequest(path, query = '') {@@@ -121,25 +121,24 @@ function createDiscoveryRoutes({ config }) {router.get('/llms.txt', (req, res) => {res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(seo.llmsTxt({name: 'OpenVibe.Chat',-- // AGT-5: own pages link their Markdown twins (openvibe-shared 4.108.0); Vibe, Developers and Updates have none-- // and are marked HTML. ## Optional is what an agent may skip.++ // AGT-5: own pages link their Markdown twins (openvibe-shared 4.108.0). ## Optional is what an agent may skip.markdown: true,base: site,summary: 'OpenVibe.Chat: Vibe, OpenVibe\'s free assistant, global chat for the whole OpenVibe network, the chat of every stream on OpenVibe.Live, servers with text and voice channels, rooms, direct messages, calls and AI members.',details: 'One OpenVibe account works here and on every other OpenVibe site. Anyone can read the global chat and the public rooms without an account; posting needs a signed-in person. The global chat and the public room pages are readable as plain HTML without JavaScript and update live over the site\'s own WebSocket. Direct messages, room settings and the API are per-person and are never listed here.',sections: [{ title: 'Start here', links: [-- { title: 'Vibe', url: abs('/vibe'), html: true, note: 'OpenVibe\'s assistant: searches the web and OpenVibe, reads pages and pictures, remembers what it is asked to, sets up recurring work; chats are private to the signed-in person' },++ { title: 'Vibe', url: abs('/vibe'), note: 'OpenVibe\'s assistant: searches the web and OpenVibe, reads pages and pictures, remembers what it is asked to, sets up recurring work; chats are private to the signed-in person' },{ title: 'Global chat', url: abs('/'), note: 'one room for everyone on OpenVibe, newest messages at the end' },{ title: 'Rooms', url: abs('/rooms'), note: 'every public room, with its topic and activity' },{ title: 'Servers', url: abs('/servers'), note: 'public servers: groups with text and voice channels, roles and invites' },] },{ title: 'Build with it', links: [-- { title: 'Developers', url: abs('/developers'), html: true, note: 'a support inbox or a room on any website with one tag, the visitor API, openvibe-sdk/chat, feeds from Claude Code, signed webhooks, the limits' },++ { title: 'Developers', url: abs('/developers'), note: 'a support inbox or a room on any website with one tag, the visitor API, openvibe-sdk/chat, feeds from Claude Code, signed webhooks, the limits' },] },],optional: [-- { title: 'What shipped on OpenVibe.Chat', url: abs('/updates'), html: true },++ { title: 'What shipped on OpenVibe.Chat', url: abs('/updates') },{ title: 'Sitemap', url: abs('/sitemap.xml'), note: 'the public pages above' },{ title: 'robots.txt', url: abs('/robots.txt') },{ title: 'Full text for language models', url: abs('/llms-full.txt') },@@@ -43,12 +43,23 @@ t('/llms.txt describes the site, its public pages and its machine-readable endpoassert.ok(r.text.includes(of_), `mentions ${of_}`);}for (const re of PRIVATE) assert.ok(!re.test(r.text), `no private path in llms.txt (${re})`);-- // AGT-5: a page with a twin is linked at it; Vibe, Developers and Updates have none and are marked HTML; Optional last.++ // AGT-5: every page is linked at its twin; Optional last.assert.ok(r.text.includes(`(${SITE}/index.md)`) && r.text.includes(`(${SITE}/rooms.md)`), 'own pages link their twins');-- assert.ok(r.text.includes(`(${SITE}/vibe) (HTML)`) && r.text.includes(`(${SITE}/updates) (HTML)`));++ assert.ok(r.text.includes(`(${SITE}/vibe.md)`) && r.text.includes(`(${SITE}/updates.md)`) && r.text.includes(`(${SITE}/developers.md)`), 'Vibe, Developers and Updates have twins now');assert.ok(r.text.includes('\n## Optional\n') && r.text.lastIndexOf('\n## ') === r.text.indexOf('\n## Optional\n'), 'Optional is last');});++t('every page that names a twin has one: /vibe, /developers and /updates too (11-03)', async () => {++ for (const p of ['/vibe', '/developers', '/updates', '/']) {++ const page = await fetch(`${h.base}${p}`, { headers: { accept: 'text/html' }, redirect: 'manual' });++ const named = /text\/markdown/.test(page.headers.get('link') || '');++ const twin = await req(p === '/' ? '/index.md' : `${p}.md`);++ assert.ok(named, `${p} names a twin`);++ assert.strictEqual(twin.status, 200, `${p}'s twin answers`);++ assert.match(twin.type, /^text\/markdown/);++ }++});++t('/llms-full.txt carries every public page and what it is, at least 500 characters', async () => {const r = await req('/llms-full.txt');assert.strictEqual(r.status, 200);
@@ -3,7 +3,9 @@
@@ -3,7 +3,9 @@
// page's own route in this router, anonymously and in-process, so a twin cannot drift from its page and a
// page's own route in this router, anonymously and in-process, so a twin cannot drift from its page and a
// signed-in view is never used. Only pages whose rendered robots tag starts with "index" get a twin: private
// signed-in view is never used. Only pages whose rendered robots tag starts with "index" get a twin: private
// servers and rooms, /messages, /settings and the SEO-7 gated pages stay noindex and have none.
// servers and rooms, /messages, /settings and the SEO-7 gated pages stay noindex and have none.
-const PUBLIC_PATH_RE = /^\/(?:$|servers$|rooms$|s\/[a-z0-9-]+(?:\/[a-z0-9-]+)?$|r\/[a-z0-9-]+$)/i;
+// /vibe, /developers and /updates are public pages too: each named a twin in its head and Link header that answered
+// 404 (11-03, tools/llms-twins-probe.js).
+const PUBLIC_PATH_RE = /^\/(?:$|servers$|rooms$|vibe$|developers$|updates$|s\/[a-z0-9-]+(?:\/[a-z0-9-]+)?$|r\/[a-z0-9-]+$)/i;
/** An anonymous GET request for one path, as the router's handlers expect it. */
/** An anonymous GET request for one path, as the router's handlers expect it. */
function anonymousRequest(path, query = '') {
function anonymousRequest(path, query = '') {
@@ -121,25 +121,24 @@ function createDiscoveryRoutes({ config }) {
@@ -121,25 +121,24 @@ function createDiscoveryRoutes({ config }) {
router.get('/llms.txt', (req, res) => {
router.get('/llms.txt', (req, res) => {
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: 'OpenVibe.Chat',
name: 'OpenVibe.Chat',
- // AGT-5: own pages link their Markdown twins (openvibe-shared 4.108.0); Vibe, Developers and Updates have none
- // and are marked HTML. ## Optional is what an agent may skip.
+ // AGT-5: own pages link their Markdown twins (openvibe-shared 4.108.0). ## Optional is what an agent may skip.
markdown: true,
markdown: true,
base: site,
base: site,
summary: 'OpenVibe.Chat: Vibe, OpenVibe\'s free assistant, global chat for the whole OpenVibe network, the chat of every stream on OpenVibe.Live, servers with text and voice channels, rooms, direct messages, calls and AI members.',
summary: 'OpenVibe.Chat: Vibe, OpenVibe\'s free assistant, global chat for the whole OpenVibe network, the chat of every stream on OpenVibe.Live, servers with text and voice channels, rooms, direct messages, calls and AI members.',
details: 'One OpenVibe account works here and on every other OpenVibe site. Anyone can read the global chat and the public rooms without an account; posting needs a signed-in person. The global chat and the public room pages are readable as plain HTML without JavaScript and update live over the site\'s own WebSocket. Direct messages, room settings and the API are per-person and are never listed here.',
details: 'One OpenVibe account works here and on every other OpenVibe site. Anyone can read the global chat and the public rooms without an account; posting needs a signed-in person. The global chat and the public room pages are readable as plain HTML without JavaScript and update live over the site\'s own WebSocket. Direct messages, room settings and the API are per-person and are never listed here.',
sections: [
sections: [
{ title: 'Start here', links: [
{ title: 'Start here', links: [
- { title: 'Vibe', url: abs('/vibe'), html: true, note: 'OpenVibe\'s assistant: searches the web and OpenVibe, reads pages and pictures, remembers what it is asked to, sets up recurring work; chats are private to the signed-in person' },
+ { title: 'Vibe', url: abs('/vibe'), note: 'OpenVibe\'s assistant: searches the web and OpenVibe, reads pages and pictures, remembers what it is asked to, sets up recurring work; chats are private to the signed-in person' },
{ title: 'Global chat', url: abs('/'), note: 'one room for everyone on OpenVibe, newest messages at the end' },
{ title: 'Global chat', url: abs('/'), note: 'one room for everyone on OpenVibe, newest messages at the end' },
{ title: 'Rooms', url: abs('/rooms'), note: 'every public room, with its topic and activity' },
{ title: 'Rooms', url: abs('/rooms'), note: 'every public room, with its topic and activity' },
{ title: 'Servers', url: abs('/servers'), note: 'public servers: groups with text and voice channels, roles and invites' },
{ title: 'Servers', url: abs('/servers'), note: 'public servers: groups with text and voice channels, roles and invites' },
] },
] },
{ title: 'Build with it', links: [
{ title: 'Build with it', links: [
- { title: 'Developers', url: abs('/developers'), html: true, note: 'a support inbox or a room on any website with one tag, the visitor API, openvibe-sdk/chat, feeds from Claude Code, signed webhooks, the limits' },
+ { title: 'Developers', url: abs('/developers'), note: 'a support inbox or a room on any website with one tag, the visitor API, openvibe-sdk/chat, feeds from Claude Code, signed webhooks, the limits' },
] },
] },
],
],
optional: [
optional: [
- { title: 'What shipped on OpenVibe.Chat', url: abs('/updates'), html: true },
+ { title: 'What shipped on OpenVibe.Chat', url: abs('/updates') },
{ title: 'Sitemap', url: abs('/sitemap.xml'), note: 'the public pages above' },
{ title: 'Sitemap', url: abs('/sitemap.xml'), note: 'the public pages above' },
{ title: 'robots.txt', url: abs('/robots.txt') },
{ title: 'robots.txt', url: abs('/robots.txt') },
{ title: 'Full text for language models', url: abs('/llms-full.txt') },
{ title: 'Full text for language models', url: abs('/llms-full.txt') },
@@ -43,12 +43,23 @@ t('/llms.txt describes the site, its public pages and its machine-readable endpo
@@ -43,12 +43,23 @@ t('/llms.txt describes the site, its public pages and its machine-readable endpo
assert.ok(r.text.includes(of_), `mentions ${of_}`);
assert.ok(r.text.includes(of_), `mentions ${of_}`);
}
}
for (const re of PRIVATE) assert.ok(!re.test(r.text), `no private path in llms.txt (${re})`);
for (const re of PRIVATE) assert.ok(!re.test(r.text), `no private path in llms.txt (${re})`);
- // AGT-5: a page with a twin is linked at it; Vibe, Developers and Updates have none and are marked HTML; Optional last.
+ // AGT-5: every page is linked at its twin; Optional last.
assert.ok(r.text.includes(`(${SITE}/index.md)`) && r.text.includes(`(${SITE}/rooms.md)`), 'own pages link their twins');
assert.ok(r.text.includes(`(${SITE}/index.md)`) && r.text.includes(`(${SITE}/rooms.md)`), 'own pages link their twins');
- assert.ok(r.text.includes(`(${SITE}/vibe) (HTML)`) && r.text.includes(`(${SITE}/updates) (HTML)`));
+ assert.ok(r.text.includes(`(${SITE}/vibe.md)`) && r.text.includes(`(${SITE}/updates.md)`) && r.text.includes(`(${SITE}/developers.md)`), 'Vibe, Developers and Updates have twins now');
assert.ok(r.text.includes('\n## Optional\n') && r.text.lastIndexOf('\n## ') === r.text.indexOf('\n## Optional\n'), 'Optional is last');
assert.ok(r.text.includes('\n## Optional\n') && r.text.lastIndexOf('\n## ') === r.text.indexOf('\n## Optional\n'), 'Optional is last');
});
});
+t('every page that names a twin has one: /vibe, /developers and /updates too (11-03)', async () => {
+ for (const p of ['/vibe', '/developers', '/updates', '/']) {
+ const page = await fetch(`${h.base}${p}`, { headers: { accept: 'text/html' }, redirect: 'manual' });
+ const named = /text\/markdown/.test(page.headers.get('link') || '');
+ const twin = await req(p === '/' ? '/index.md' : `${p}.md`);
+ assert.ok(named, `${p} names a twin`);
+ assert.strictEqual(twin.status, 200, `${p}'s twin answers`);
+ assert.match(twin.type, /^text\/markdown/);
+ }
+});
+
t('/llms-full.txt carries every public page and what it is, at least 500 characters', async () => {
t('/llms-full.txt carries every public page and what it is, at least 500 characters', async () => {
const r = await req('/llms-full.txt');
const r = await req('/llms-full.txt');
assert.strictEqual(r.status, 200);
assert.strictEqual(r.status, 200);