openvibers/openvibe.codes Commits
Agent layer: /.well-known/agent.json and Markdown twins of the public pages (#146)
Changes
@@@ -17,7 +17,7 @@"iovalkey": "^0.4.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.232.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.33.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.34.0","pg": "^8.23.0"},"bin": {@@@ -30,6 +30,12 @@"node": ">=22.0.0"}},++ "node_modules/@acemir/cssom": {++ "version": "0.9.32",++ "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.32.tgz",++ "integrity": "sha512-OijrAUu7rrJhSIoHNg6LMPS5e344nwUHA+1EFYYjrh64D+GmytUsfBQjci45TjT+rd5Spsp8B5xsZDT+TgyXXQ==",++ "license": "MIT"++ },"node_modules/@electric-sql/pglite": {"version": "0.5.8","resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.5.8.tgz",@@@ -119,6 +125,19 @@"npm": "1.2.8000 || >= 1.4.16"}},++ "node_modules/boolbase": {++ "version": "2.0.0",++ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-2.0.0.tgz",++ "integrity": "sha512-DkVaaQHymRhpYEYo9x1oo7Q7B0Y6KJUsjm3c9eTyFDby4MHLBTwZ6ZDWBel5zrYxj1WsZgC5oLpiz+93MluXeA==",++ "license": "ISC",++ "engines": {++ "node": ">=20.19.0"++ },++ "funding": {++ "type": "github",++ "url": "https://github.com/sponsors/fb55"++ }++ },"node_modules/buffer-equal-constant-time": {"version": "1.0.1","resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",@@@ -221,6 +240,39 @@"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==","license": "MIT"},++ "node_modules/css-select": {++ "version": "7.0.0",++ "resolved": "https://registry.npmjs.org/css-select/-/css-select-7.0.0.tgz",++ "integrity": "sha512-snmjEVXy+1LnwXdxhYvTMj1d9tOh4HxkA1YmoayVBeeyR2C14Pum7fcxJIm4SswYspVy866eYNwlH6xC3/VH5g==",++ "license": "BSD-2-Clause",++ "dependencies": {++ "boolbase": "^2.0.0",++ "css-what": "^8.0.0",++ "domhandler": "^6.0.1",++ "domutils": "^4.0.2",++ "nth-check": "^3.0.1"++ },++ "engines": {++ "node": ">=20.19.0"++ },++ "funding": {++ "type": "github",++ "url": "https://github.com/sponsors/fb55"++ }++ },++ "node_modules/css-what": {++ "version": "8.0.0",++ "resolved": "https://registry.npmjs.org/css-what/-/css-what-8.0.0.tgz",++ "integrity": "sha512-DH0Bqq3DNp5tdOReuNyAA+Ev4Y2GS5FMbZpeTLP6C4CDi0h5nL0BmUPChXw3o/qbHLDWHl49sbNqQVY7bMSDdw==",++ "license": "BSD-2-Clause",++ "engines": {++ "node": ">=20.19.0"++ },++ "funding": {++ "type": "github",++ "url": "https://github.com/sponsors/fb55"++ }++ },"node_modules/debug": {"version": "2.6.9","resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",@@@ -258,6 +310,73 @@"npm": "1.2.8000 || >= 1.4.16"}},++ "node_modules/dom-serializer": {++ "version": "3.1.1",++ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-3.1.1.tgz",++ "integrity": "sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw==",++ "license": "MIT",++ "dependencies": {++ "domelementtype": "^3.0.0",++ "domhandler": "^6.0.0",++ "entities": "^8.0.0"++ },++ "engines": {++ "node": ">=20.19.0"++ },++ "funding": {++ "type": "github",++ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"++ }++ },++ "node_modules/domelementtype": {++ "version": "3.0.0",++ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-3.0.0.tgz",++ "integrity": "sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg==",++ "funding": [++ {++ "type": "github",++ "url": "https://github.com/sponsors/fb55"++ }++ ],++ "license": "BSD-2-Clause",++ "engines": {++ "node": ">=20.19.0"++ }++ },++ "node_modules/domhandler": {++ "version": "6.0.1",++ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-6.0.1.tgz",++ "integrity": "sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg==",++ "license": "BSD-2-Clause",++ "dependencies": {++ "domelementtype": "^3.0.0"++ },++ "engines": {++ "node": ">=20.19.0"++ },++ "funding": {++ "type": "github",++ "url": "https://github.com/fb55/domhandler?sponsor=1"++ }++ },++ "node_modules/domutils": {++ "version": "4.0.2",++ "resolved": "https://registry.npmjs.org/domutils/-/domutils-4.0.2.tgz",++ "integrity": "sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA==",++ "license": "BSD-2-Clause",++ "dependencies": {++ "dom-serializer": "^3.0.0",++ "domelementtype": "^3.0.0",++ "domhandler": "^6.0.0"++ },++ "engines": {++ "node": ">=20.19.0"++ },++ "funding": {++ "type": "github",++ "url": "https://github.com/fb55/domutils?sponsor=1"++ }++ },"node_modules/dotenv": {"version": "16.6.1","resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",@@@ -308,6 +427,18 @@"node": ">= 0.8"}},++ "node_modules/entities": {++ "version": "8.1.0",++ "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz",++ "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==",++ "license": "BSD-2-Clause",++ "engines": {++ "node": ">=20.19.0"++ },++ "funding": {++ "url": "https://github.com/fb55/entities?sponsor=1"++ }++ },"node_modules/es-define-property": {"version": "1.0.1","resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",@@@ -566,6 +697,110 @@"url": "https://github.com/sponsors/EvanHahn"}},++ "node_modules/html-escaper": {++ "version": "3.0.3",++ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",++ "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==",++ "license": "MIT"++ },++ "node_modules/htmlparser2": {++ "version": "10.1.0",++ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz",++ "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==",++ "funding": [++ "https://github.com/fb55/htmlparser2?sponsor=1",++ {++ "type": "github",++ "url": "https://github.com/sponsors/fb55"++ }++ ],++ "license": "MIT",++ "dependencies": {++ "domelementtype": "^2.3.0",++ "domhandler": "^5.0.3",++ "domutils": "^3.2.2",++ "entities": "^7.0.1"++ }++ },++ "node_modules/htmlparser2/node_modules/dom-serializer": {++ "version": "2.0.0",++ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",++ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",++ "license": "MIT",++ "dependencies": {++ "domelementtype": "^2.3.0",++ "domhandler": "^5.0.2",++ "entities": "^4.2.0"++ },++ "funding": {++ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"++ }++ },++ "node_modules/htmlparser2/node_modules/dom-serializer/node_modules/entities": {++ "version": "4.5.0",++ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",++ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",++ "license": "BSD-2-Clause",++ "engines": {++ "node": ">=0.12"++ },++ "funding": {++ "url": "https://github.com/fb55/entities?sponsor=1"++ }++ },++ "node_modules/htmlparser2/node_modules/domelementtype": {++ "version": "2.3.0",++ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",++ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",++ "funding": [++ {++ "type": "github",++ "url": "https://github.com/sponsors/fb55"++ }++ ],++ "license": "BSD-2-Clause"++ },++ "node_modules/htmlparser2/node_modules/domhandler": {++ "version": "5.0.3",++ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",++ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",++ "license": "BSD-2-Clause",++ "dependencies": {++ "domelementtype": "^2.3.0"++ },++ "engines": {++ "node": ">= 4"++ },++ "funding": {++ "url": "https://github.com/fb55/domhandler?sponsor=1"++ }++ },++ "node_modules/htmlparser2/node_modules/domutils": {++ "version": "3.2.2",++ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",++ "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",++ "license": "BSD-2-Clause",++ "dependencies": {++ "dom-serializer": "^2.0.0",++ "domelementtype": "^2.3.0",++ "domhandler": "^5.0.3"++ },++ "funding": {++ "url": "https://github.com/fb55/domutils?sponsor=1"++ }++ },++ "node_modules/htmlparser2/node_modules/entities": {++ "version": "7.0.1",++ "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",++ "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",++ "license": "BSD-2-Clause",++ "engines": {++ "node": ">=0.12"++ },++ "funding": {++ "url": "https://github.com/fb55/entities?sponsor=1"++ }++ },"node_modules/http-errors": {"version": "2.0.1","resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",@@@ -711,6 +946,30 @@"safe-buffer": "^5.0.1"}},++ "node_modules/linkedom": {++ "version": "0.18.14",++ "resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.18.14.tgz",++ "integrity": "sha512-BRUnkOm4YR5vo+qI3j0EbungB0vZPSCue5AdnTQRaxAGSbsOZiBFRO0rh4zlo8qXUhli8pxvnpEXDklMnlq2Rg==",++ "license": "ISC",++ "dependencies": {++ "@acemir/cssom": "^0.9.32",++ "css-select": "^7.0.0",++ "html-escaper": "^3.0.3",++ "htmlparser2": "^10.1.0",++ "uhyphen": "^0.2.0"++ },++ "engines": {++ "node": ">=16"++ },++ "peerDependencies": {++ "canvas": ">= 2"++ },++ "peerDependenciesMeta": {++ "canvas": {++ "optional": true++ }++ }++ },"node_modules/lodash.defaults": {"version": "4.2.0","resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",@@@ -849,6 +1108,22 @@"node": ">= 0.6"}},++ "node_modules/nth-check": {++ "version": "3.0.1",++ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-3.0.1.tgz",++ "integrity": "sha512-GX0gsdbGVCgnRgbeGaubfjpBXyYRWOOCVeYh08bSQvDZqxz5ndXs1OTfAt/h36G1xvI94YIspsI0sVFqAV9+RQ==",++ "license": "BSD-2-Clause",++ "dependencies": {++ "boolbase": "^2.0.0"++ },++ "engines": {++ "node": ">=20.19.0"++ },++ "funding": {++ "type": "github",++ "url": "https://github.com/fb55/nth-check?sponsor=1"++ }++ },"node_modules/object-inspect": {"version": "1.13.4","resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",@@@ -918,12 +1193,13 @@}},"node_modules/openvibe-shared": {-- "version": "4.33.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.33.0",-- "integrity": "sha512-t9XvhrQ+lQebOrFTjMCzI6/7/i4iQn9dmMr/8HVTNW1XoV1exyJa7IH/HfyyKuLxjA8xaowrDfD28LwKIibBNg==",++ "version": "4.34.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.34.0",++ "integrity": "sha512-Rj5uRO3MbWkyIYAm+vK4Y94ugDNYEjEgGkYnPjSej4yMqhalpaPCkU96dzphMGiOUGP1PNWsiKhwaBj8tewgBg==","license": "MIT","dependencies": {-- "jsonwebtoken": "^9.0.0"++ "jsonwebtoken": "^9.0.0",++ "linkedom": "^0.18.13"},"bin": {"openvibe-build-assets": "scripts/build-assets.js"@@@ -1366,6 +1642,12 @@"node": ">= 0.6"}},++ "node_modules/uhyphen": {++ "version": "0.2.0",++ "resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz",++ "integrity": "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==",++ "license": "ISC"++ },"node_modules/unpipe": {"version": "1.0.0","resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",@@@ -32,7 +32,7 @@"iovalkey": "^0.4.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.232.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.33.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.34.0","pg": "^8.23.0"},"engines": {nnew file mode 100644@@@ -0,0 +1,17 @@++'use strict';++// What openvibe.codes tells an agent about itself (openvibe-shared/agent-layer): /.well-known/agent.json.++// Every field is something this site really serves; /start is the harness's own documentation and the JSON API++// is the public catalog and router, so both are named. A run's page is a person's run, never advertised here.++module.exports = {++ id: 'codes',++ name: 'OpenVibe.Codes',++ purpose: 'The open coding-agent harness: one router over Claude Code, Codex, OpenCode, Command Code, Aider, the DeepSeek API and your own models, with hand-offs between agents, and the way to improve OpenVibe itself.',++ home: 'https://openvibe.codes',++ site: 'openvibe.codes',++ api: '/api/v1',++ docs: '/start',++ policy: {++ auth: ['none', 'Network OIDC'],++ rateLimit: '300 page requests a minute per address; 60 public API requests a minute',++ },++};nnew file mode 100644@@@ -0,0 +1,28 @@++'use strict';++// The Markdown twins of openvibe.codes' public pages (openvibe-shared/agent-layer), rendered by the same++// page builders the routes use (createPageRoutes().publicViews + render/layout.js's renderPage), so a++// twin cannot drift from its page. Anonymous only; a run's page is a person's run and has no twin.++const { renderPage } = require('./render/layout');++++function createAgentPages(ctx, publicViews) {++ const views = publicViews;++ const twin = (page, path) => (page ? { html: renderPage({ ...page, viewer: { kind: 'anonymous' }, config: ctx.config, path }) } : null);++++ async function renderPublicPage(path) {++ if (path === '/') return twin(views.home(), path);++ if (path === '/start') return twin(views.start(), path);++ if (path === '/harnesses') return twin(views.harnesses(), path);++ if (path === '/improve') return twin(views.improve(), path);++ if (path === '/updates') return twin(views.updates(), path);++ if (path === '/policy') return twin(views.policy(), path);++ let m = /^\/policy\/([^/]+)$/.exec(path);++ if (m) return twin(views.policyDoc(decodeURIComponent(m[1])), path);++ m = /^\/improve\/([^/]+)\/([^/]+)$/.exec(path);++ if (m) return twin(await views.improveRepo(decodeURIComponent(m[1]), decodeURIComponent(m[2])), path);++ return null;++ }++++ return { renderPublicPage };++}++++module.exports = { createAgentPages };@@@ -175,7 +175,21 @@ async function createApp(opts = {}) {// ── Pages ───────────────────────────────────────────────app.use(rateLimit({ windowMs: 60_000, limit: Number(process.env.CODES_RATE_LIMIT_PER_MIN) || 300, standardHeaders: true, legacyHeaders: false })); // per address; tests raise it-- app.use(createPageRoutes(ctx));++ const pageRoutes = createPageRoutes(ctx);++ // The agent layer (openvibe-shared/agent-layer): /.well-known/agent.json and the Markdown twins of the public++ // pages. The pages themselves keep their own routes — a signed-in visitor's account bar and forms are theirs++ // — so only the manifest and twin requests reach this mount; the pages advertise the twin themselves.++ const agentLayer = express();++ require('openvibe-shared/agent-layer').mount(agentLayer, {++ descriptor: require('./agent-descriptor'),++ render: require('./agent-pages').createAgentPages(ctx, pageRoutes.publicViews).renderPublicPage,++ });++ app.use((req, res, next) => {++ const twin = req.path.endsWith('.md') || req.query.format === 'markdown' || String(req.headers.accept || '').includes('text/markdown');++ if (req.path !== '/.well-known/agent.json' && !twin) return next();++ return agentLayer(req, res, next);++ });++ app.use(pageRoutes);app.use(createRunRoutes({ config, store, sso, harnesses, run, tasks, sessions, keys, fetchImpl: fetchImpl || globalThis.fetch, log }));app.use((req, res) => send(res, 404, { viewer: req.viewer, config, path: req.originalUrl, title: 'Not found', body: html`<h1>Not found</h1><p>No page here. Try <a href="/harnesses">the agents</a>, <a href="/improve">Improve OpenVibe</a>, or the developer platform at <a href="https://openvibe.services">openvibe.services</a>.</p>` }));@@@ -94,6 +94,7 @@ function createDiscoveryRoutes(ctx) {r.get('/robots.txt', (_req, res) => {res.type('text/plain').set('Cache-Control', TEXT).send('# openvibe.codes: the public pages are for search and AI crawlers; sign-in and the API are not.\n'++ + '# Agent manifest: https://openvibe.codes/.well-known/agent.json; every public page has a .md twin.\n'+ seo.robotsTxt({ sitemaps: [abs('/sitemap.xml')], disallow: DISALLOW }));});@@@ -84,7 +84,7 @@ function createPageRoutes(ctx) {const gitReader = home.createGitReader({ internalUrl: config.git.internalUrl, publicUrl: config.git.publicUrl, enabled: config.nodeEnv !== 'test' });// ── Home: code with any agent ───────────────────────────-- r.get('/', (req, res) => {++ function homePage() {const offers = harnesses.list();const tasks = Object.keys(placement.TASKS);// What a run prints (bin/openvibe-codes.js), as an example: Codex hits its limit and Claude Code finishes.@@@ -99,8 +99,8 @@ run run_20261008… · ~/my-project→ Edit test/date.test.jsAdded test/date.test.js: three cases, all pass.✔ done (48 s · $0.0400 · 31k in / 1.2k out)`;-- page(req, res, {-- index: true, cache: PUBLIC_CACHE,++ return {++ agent: true, index: true, cache: PUBLIC_CACHE,jsonLd: [...homeJsonLd(config), seo.jsonLd.faq(home.faqPairs())],styles: [showcase.STYLESHEET],body: html`${raw(showcase.hero({@@@ -142,11 +142,12 @@ ${table(['Piece', 'State'], [${raw(home.faq())}${raw(showcase.network({ service: 'codes' }))}${raw(showcase.cta({ title: 'Building an app instead?', text: 'Projects, keys, grants and the API docs of every OpenVibe service live on OpenVibe.Services.', actions: [{ label: 'Open the developer platform', href: SERVICES_ORIGIN, primary: true }, { label: 'Meet the agents', href: '/harnesses' }] }))}`,-- });-- });++ };++ }++ r.get('/', (req, res) => page(req, res, homePage()));// ── Getting started: the harness on your own machine (bin/openvibe-codes.js, harness/) ──-- r.get('/start', (req, res) => {++ function startPage() {const example = `$ openvibe-codes agents✔ claude-code Claude Code ~/.local/bin/claude✔ codex Codex ~/.local/bin/codex@@@ -169,8 +170,8 @@ for await (const event of harness.run({ prompt: 'fix the failing test', cwd: '.'{"type":"user","message":{"content":[{"type":"tool_result","content":"…","is_error":false}]}}{"type":"system","subtype":"handoff","from":"codex","reason":"You have hit your usage limit."}{"type":"result","subtype":"success","is_error":false,"result":"…","total_cost_usd":0.04,"usage":{…}}`;-- page(req, res, {-- index: true, cache: PUBLIC_CACHE,++ return {++ agent: true, index: true, cache: PUBLIC_CACHE,title: 'Get started with the coding-agent harness',description: 'Install openvibe-codes and run coding tasks on Claude Code, Codex, OpenCode, Command Code, Aider, the DeepSeek API or your own model, with hand-offs between them, from your terminal or your own code.',crumbs: [{ label: 'Get started' }],@@@ -222,8 +223,9 @@ ${table(['Level', 'What the agent may do'], [<p><code>harness.agents()</code>, <code>harness.route({ task })</code> and <code>harness.run({ prompt, cwd, agent, permission, handoff })</code> are what the command line uses. The router is the same one behind <a href="/api/v1/harnesses"><code>/api/v1/harnesses/route</code></a>.</p><h2 id="next">What comes next</h2><p>Tasks from the web already run on your own machine (<a href="/run">Run on your machine</a>). Next: a hosted runner on OpenVibe.Run workers for people who would rather not run agents themselves, with budgets; OpenVibe.Actor using Codes for its coding work; and <a href="/improve">Improve OpenVibe</a> as a guided run: pick a repository, describe the change, get a pull request.</p>`,-- });-- });++ };++ }++ r.get('/start', (req, res) => page(req, res, startPage()));// ── The agent catalog (server/data/harness-offers.json, validated at boot) ──// No address target that is a local path is ever shown — only the address kind.@@@ -236,10 +238,10 @@ ${table(['Level', 'What the agent may do'], [const usd = (n) => `$${Number(n || 0).toFixed(2)}`;const duration = (n) => (n >= 3600 && n % 3600 === 0 ? `${n / 3600} h` : `${n} s`);const count = (n) => Number(n).toLocaleString('en-US');-- r.get('/harnesses', (req, res) => {++ function harnessesPage() {const offers = harnesses.list();-- page(req, res, {-- index: true, cache: PUBLIC_CACHE,++ return {++ agent: true, index: true, cache: PUBLIC_CACHE,title: 'The coding agents',description: 'Claude Code, Codex, OpenCode, Command Code, Aider and the DeepSeek API: every coding harness OpenVibe.Codes routes a task to, with its models, capabilities, prices and limits.',crumbs: [{ label: 'Agents' }],@@@ -262,8 +264,9 @@ ${offers.map((o) => html`<h3>${o.name} <code>${o.id}</code></h3>${o.agents.lengt`${usd(a.price_per_1k_tokens.fresh_usd)} fresh, ${usd(a.price_per_1k_tokens.cached_usd)} cached, ${usd(a.price_per_1k_tokens.output_usd)} output`,])): html`<p class="muted">no models yet</p>`}`)}`,-- });-- });++ };++ }++ r.get('/harnesses', (req, res) => page(req, res, harnessesPage()));// ── Improve OpenVibe ─────────────────────────────────────const knownRepo = (owner, name) => {@@@ -312,11 +315,11 @@ ${offers.map((o) => html`<h3>${o.name} <code>${o.id}</code></h3>${o.agents.lengtconst repoForgejoUrl = (owner, name) => `${config.git.publicUrl}/${owner}/${name}`;const repoCloneUrl = (owner, name) => `${config.git.publicUrl}/${owner}/${name}.git`;-- r.get('/improve', (req, res) => {++ function improvePage() {const { running, libraries } = repoGroups();const repoCards = (list) => html`<ul class="cards">${list.map((m) => html`<li><a href="/improve/${m.repository}"><strong>${m.repository.split('/')[1]}</strong></a><span>${(m.site && (m.site.tagline || m.site.what)) || m.name}</span></li>`)}</ul>`;-- page(req, res, {-- index: true, cache: PUBLIC_CACHE,++ return {++ agent: true, index: true, cache: PUBLIC_CACHE,title: 'Improve OpenVibe',description: 'OpenVibe is open source: every service and library is on GitHub. Pick a repository, describe the change, and send a pull request; the harness makes the change on your machine.',crumbs: [{ label: 'Improve OpenVibe' }],@@@ -333,19 +336,20 @@ ${offers.map((o) => html`<h3>${o.name} <code>${o.id}</code></h3>${o.agents.lengt${repoCards(running)}<h2>Shared libraries</h2>${repoCards(libraries)}`,-- });-- });++ };++ }++ r.get('/improve', (req, res) => page(req, res, improvePage()));-- r.get('/improve/:owner/:name', async (req, res) => {-- const { owner, name } = req.params;++ async function improveRepoPage(owner, name, { signed = false } = {}) {const meta = repoMeta().find((m) => m.repository === `${owner}/${name}`);-- if (!meta) return page(req, res, { title: 'Not found', body: html`<h1>Not found</h1><p>No such OpenVibe repository. <a href="/improve">All repositories</a></p>` }, 404);++ if (!meta) return null;let forgejoInfo = null;let forgejoError = null;-- try { forgejoInfo = await ctx.forgejo.getRepo(owner, name); } catch (e) { forgejoError = e.code === 'forgejo.unconfigured' ? null : e.message; }++ if (signed) {++ try { forgejoInfo = await ctx.forgejo.getRepo(owner, name); } catch (e) { forgejoError = e.code === 'forgejo.unconfigured' ? null : e.message; }++ }const agents = harnesses.list();const opt = (v, label, sel) => html`<option value="${v}"${sel ? raw(' selected') : ''}>${label}</option>`;-- const signed = signedIn(req);const form = signed ? html`<form class="run-card run-form" method="post" action="/improve/start"><input type="hidden" name="repo" value="${repoForgejoUrl(owner, name)}"><input type="hidden" name="ref" value="${(forgejoInfo && forgejoInfo.default_branch) || 'main'}">@@@ -359,9 +363,9 @@ ${forgejoError ? html`<p class="run-notice run-notice-error" role="alert">OpenVi<label class="run-check"><input type="checkbox" name="push" value="1" checked> Push the change to a new branch on OpenVibe Git <small>(needs a token on your machine; never the branch it started from)</small></label><label class="run-check"><input type="checkbox" name="pull_request" value="1" checked> and open a pull request</label><button class="run-btn run-btn-primary" type="submit">Start on my machine</button>--</form>` : html`<p><a class="run-btn run-btn-primary" href="/auth/login?next=${encodeURIComponent(req.originalUrl)}">Sign in to start a run</a></p>`;-- page(req, res, {-- index: true, cache: PUBLIC_CACHE,++</form>` : html`<p><a class="run-btn run-btn-primary" href="/auth/login?next=${encodeURIComponent(`/improve/${owner}/${name}`)}">Sign in to start a run</a></p>`;++ return {++ agent: true, index: true, cache: PUBLIC_CACHE,title: `Improve ${name}`,description: `Contribute to ${meta.name}: describe a change and run it on your machine through OpenVibe.Codes.`,crumbs: [{ label: 'Improve OpenVibe', href: '/improve' }, { label: meta.name }],@@@ -376,7 +380,12 @@ ${forgejoError ? html`<p class="run-notice run-notice-error" role="alert">OpenVi<li>You review the result, then the maintainers review the pull request.</li></ol>${form}`,-- });++ };++ }++ r.get('/improve/:owner/:name', async (req, res) => {++ const out = await improveRepoPage(req.params.owner, req.params.name, { signed: signedIn(req) });++ if (!out) return page(req, res, { title: 'Not found', body: html`<h1>Not found</h1><p>No such OpenVibe repository. <a href="/improve">All repositories</a></p>` }, 404);++ page(req, res, out);});r.post('/improve/start', express.urlencoded({ extended: false, limit: '32kb' }), async (req, res) => {@@@ -424,25 +433,33 @@ ${form}`,// ── Community documents and the update log ───────────────// What shipped on OpenVibe.Codes: the shared update log every OpenVibe site has.-- r.get('/updates', (req, res) => page(req, res, { index: true, cache: PUBLIC_CACHE, title: 'What shipped on OpenVibe.Codes', body: raw(frame.updatesBody({ service: 'codes', siteName: 'OpenVibe.Codes' }) + `<script src="${ovServe.url('shipped.js')}" defer></script>`) }));-- r.get('/policy', (req, res) => page(req, res, {-- index: true, cache: PUBLIC_CACHE, title: 'Community', crumbs: [{ label: 'Community' }],++ const updatesPage = () => ({ agent: true, index: true, cache: PUBLIC_CACHE, title: 'What shipped on OpenVibe.Codes', body: raw(frame.updatesBody({ service: 'codes', siteName: 'OpenVibe.Codes' }) + `<script src="${ovServe.url('shipped.js')}" defer></script>`) });++ r.get('/updates', (req, res) => page(req, res, updatesPage()));++ const policyIndexPage = () => ({++ agent: true, index: true, cache: PUBLIC_CACHE, title: 'Community', crumbs: [{ label: 'Community' }],body: html`<h1>Community</h1><p>The rules for everyone who builds OpenVibe and everything published on it: the repositories, and the apps and releases on OpenVibe.Services.</p><ul class="cards">${governance.map((g) => html`<li><a href="/policy/${g.slug}"><strong>${g.title}</strong></a><span>${g.blurb}${g.draft ? ' (draft, pending owner review)' : ''}</span></li>`)}</ul><p class="muted">The platform's own policy (how a contract or capability changes, compatibility and deprecation, licensing, transparency) is on <a href="${SERVICES_ORIGIN}/policy">OpenVibe.Services</a>.</p>`,-- }));++ });++ r.get('/policy', (req, res) => page(req, res, policyIndexPage()));-- for (const g of governance) {-- r.get(`/policy/${g.slug}`, (req, res) => page(req, res, {-- index: !g.draft, cache: PUBLIC_CACHE, title: g.title, crumbs: [{ label: 'Community', href: '/policy' }, { label: g.title }],++ const policyDocPage = (g) => ({++ agent: true, index: !g.draft, cache: PUBLIC_CACHE, title: g.title, crumbs: [{ label: 'Community', href: '/policy' }, { label: g.title }],body: html`<h1>${g.title}</h1>${g.draft ? notice(html`<strong>Draft — pending owner review.</strong> This is a proposal, not yet in effect. It takes effect when the owner of the OpenVibers organization approves it.`, 'warn') : ''}<article class="prose">${raw(markdown(g.body, { headingOffset: 0 }))}</article><p class="muted small">Source: <a href="${GITHUB}/OpenVibe.Codes/blob/main/${g.file}"><code>${g.file}</code></a>. Changes go through a pull request that the owner approves.</p>`,-- }));-- }++ });++ for (const g of governance) r.get(`/policy/${g.slug}`, (req, res) => page(req, res, policyDocPage(g)));++++ // The public pages the agent layer's Markdown twins (server/agent-pages.js) render with the same builders.++ r.publicViews = {++ home: homePage, start: startPage, harnesses: harnessesPage, improve: improvePage,++ improveRepo: improveRepoPage, updates: updatesPage, policy: policyIndexPage,++ policyDoc: (slug) => { const g = governance.find((x) => x.slug === String(slug || '')); return g ? policyDocPage(g) : null; },++ };// ── Discovery: robots.txt, sitemap.xml, llms.txt, llms-full.txt (http/discovery.js, openvibe-shared/seo) ──r.use(createDiscoveryRoutes({ ...ctx, governance }));@@@ -90,6 +90,10 @@ function renderPage(o) {description: o.description || 'The open coding-agent harness: Claude Code, Codex, OpenCode, Command Code, Aider, the DeepSeek API and your own models behind one router, and the way to improve OpenVibe itself.',canonical,robots: o.index ? 'index, follow' : 'noindex, nofollow',++ // A page with a Markdown twin (o.agent) advertises it in the head and the Link header; the twin itself++ // is server/agent-pages.js.++ agentLayer: o.agent === true,++ llms: `${String(o.config.baseUrl).replace(/\/+$/, '')}/llms.txt`,jsonLd: (o.jsonLd || []).filter(Boolean),home: '/',navLinks: NAV,@@@ -128,6 +132,10 @@ function send(res, status, o) {const personal = o.viewer && o.viewer.kind === 'user';if (!res.get('Cache-Control')) res.set('Cache-Control', !personal && o.cache ? o.cache : cache.htmlHeaders({ private: true }));res.set('Vary', 'Cookie');++ if (o.agent) {++ const site = String(o.config.baseUrl).replace(/\/+$/, '');++ res.set('Link', require('openvibe-shared/agent-layer').linkHeaderTags(`${site}${String(o.path || '/').split('?')[0]}`, `${site}/llms.txt`).header);++ }res.status(status).type('html').send(renderPage(o));}
@@ -17,7 +17,7 @@
@@ -17,7 +17,7 @@
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.232.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.232.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.33.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.34.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
},
},
"bin": {
"bin": {
@@ -30,6 +30,12 @@
@@ -30,6 +30,12 @@
"node": ">=22.0.0"
"node": ">=22.0.0"
}
}
},
},
+ "node_modules/@acemir/cssom": {
+ "version": "0.9.32",
+ "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.32.tgz",
+ "integrity": "sha512-OijrAUu7rrJhSIoHNg6LMPS5e344nwUHA+1EFYYjrh64D+GmytUsfBQjci45TjT+rd5Spsp8B5xsZDT+TgyXXQ==",
+ "license": "MIT"
+ },
"node_modules/@electric-sql/pglite": {
"node_modules/@electric-sql/pglite": {
"version": "0.5.8",
"version": "0.5.8",
"resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.5.8.tgz",
"resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.5.8.tgz",
@@ -119,6 +125,19 @@
@@ -119,6 +125,19 @@
"npm": "1.2.8000 || >= 1.4.16"
"npm": "1.2.8000 || >= 1.4.16"
}
}
},
},
+ "node_modules/boolbase": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-2.0.0.tgz",
+ "integrity": "sha512-DkVaaQHymRhpYEYo9x1oo7Q7B0Y6KJUsjm3c9eTyFDby4MHLBTwZ6ZDWBel5zrYxj1WsZgC5oLpiz+93MluXeA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
"node_modules/buffer-equal-constant-time": {
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
@@ -221,6 +240,39 @@
@@ -221,6 +240,39 @@
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
"license": "MIT"
"license": "MIT"
},
},
+ "node_modules/css-select": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/css-select/-/css-select-7.0.0.tgz",
+ "integrity": "sha512-snmjEVXy+1LnwXdxhYvTMj1d9tOh4HxkA1YmoayVBeeyR2C14Pum7fcxJIm4SswYspVy866eYNwlH6xC3/VH5g==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^2.0.0",
+ "css-what": "^8.0.0",
+ "domhandler": "^6.0.1",
+ "domutils": "^4.0.2",
+ "nth-check": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "node_modules/css-what": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/css-what/-/css-what-8.0.0.tgz",
+ "integrity": "sha512-DH0Bqq3DNp5tdOReuNyAA+Ev4Y2GS5FMbZpeTLP6C4CDi0h5nL0BmUPChXw3o/qbHLDWHl49sbNqQVY7bMSDdw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
"node_modules/debug": {
"node_modules/debug": {
"version": "2.6.9",
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -258,6 +310,73 @@
@@ -258,6 +310,73 @@
"npm": "1.2.8000 || >= 1.4.16"
"npm": "1.2.8000 || >= 1.4.16"
}
}
},
},
+ "node_modules/dom-serializer": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-3.1.1.tgz",
+ "integrity": "sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw==",
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^3.0.0",
+ "domhandler": "^6.0.0",
+ "entities": "^8.0.0"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+ }
+ },
+ "node_modules/domelementtype": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-3.0.0.tgz",
+ "integrity": "sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
+ "node_modules/domhandler": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-6.0.1.tgz",
+ "integrity": "sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "domelementtype": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
+ }
+ },
+ "node_modules/domutils": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-4.0.2.tgz",
+ "integrity": "sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "dom-serializer": "^3.0.0",
+ "domelementtype": "^3.0.0",
+ "domhandler": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/fb55/domutils?sponsor=1"
+ }
+ },
"node_modules/dotenv": {
"node_modules/dotenv": {
"version": "16.6.1",
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
@@ -308,6 +427,18 @@
@@ -308,6 +427,18 @@
"node": ">= 0.8"
"node": ">= 0.8"
}
}
},
},
+ "node_modules/entities": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz",
+ "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"node_modules/es-define-property": {
"node_modules/es-define-property": {
"version": "1.0.1",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -566,6 +697,110 @@
@@ -566,6 +697,110 @@
"url": "https://github.com/sponsors/EvanHahn"
"url": "https://github.com/sponsors/EvanHahn"
}
}
},
},
+ "node_modules/html-escaper": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",
+ "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==",
+ "license": "MIT"
+ },
+ "node_modules/htmlparser2": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz",
+ "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==",
+ "funding": [
+ "https://github.com/fb55/htmlparser2?sponsor=1",
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3",
+ "domutils": "^3.2.2",
+ "entities": "^7.0.1"
+ }
+ },
+ "node_modules/htmlparser2/node_modules/dom-serializer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
+ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.2",
+ "entities": "^4.2.0"
+ },
+ "funding": {
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+ }
+ },
+ "node_modules/htmlparser2/node_modules/dom-serializer/node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/htmlparser2/node_modules/domelementtype": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
+ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/htmlparser2/node_modules/domhandler": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
+ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "domelementtype": "^2.3.0"
+ },
+ "engines": {
+ "node": ">= 4"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
+ }
+ },
+ "node_modules/htmlparser2/node_modules/domutils": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
+ "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "dom-serializer": "^2.0.0",
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domutils?sponsor=1"
+ }
+ },
+ "node_modules/htmlparser2/node_modules/entities": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
+ "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"node_modules/http-errors": {
"node_modules/http-errors": {
"version": "2.0.1",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
@@ -711,6 +946,30 @@
@@ -711,6 +946,30 @@
"safe-buffer": "^5.0.1"
"safe-buffer": "^5.0.1"
}
}
},
},
+ "node_modules/linkedom": {
+ "version": "0.18.14",
+ "resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.18.14.tgz",
+ "integrity": "sha512-BRUnkOm4YR5vo+qI3j0EbungB0vZPSCue5AdnTQRaxAGSbsOZiBFRO0rh4zlo8qXUhli8pxvnpEXDklMnlq2Rg==",
+ "license": "ISC",
+ "dependencies": {
+ "@acemir/cssom": "^0.9.32",
+ "css-select": "^7.0.0",
+ "html-escaper": "^3.0.3",
+ "htmlparser2": "^10.1.0",
+ "uhyphen": "^0.2.0"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "peerDependencies": {
+ "canvas": ">= 2"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
"node_modules/lodash.defaults": {
"node_modules/lodash.defaults": {
"version": "4.2.0",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
@@ -849,6 +1108,22 @@
@@ -849,6 +1108,22 @@
"node": ">= 0.6"
"node": ">= 0.6"
}
}
},
},
+ "node_modules/nth-check": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-3.0.1.tgz",
+ "integrity": "sha512-GX0gsdbGVCgnRgbeGaubfjpBXyYRWOOCVeYh08bSQvDZqxz5ndXs1OTfAt/h36G1xvI94YIspsI0sVFqAV9+RQ==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/fb55/nth-check?sponsor=1"
+ }
+ },
"node_modules/object-inspect": {
"node_modules/object-inspect": {
"version": "1.13.4",
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
@@ -918,12 +1193,13 @@
@@ -918,12 +1193,13 @@
}
}
},
},
"node_modules/openvibe-shared": {
"node_modules/openvibe-shared": {
- "version": "4.33.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.33.0",
- "integrity": "sha512-t9XvhrQ+lQebOrFTjMCzI6/7/i4iQn9dmMr/8HVTNW1XoV1exyJa7IH/HfyyKuLxjA8xaowrDfD28LwKIibBNg==",
+ "version": "4.34.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.34.0",
+ "integrity": "sha512-Rj5uRO3MbWkyIYAm+vK4Y94ugDNYEjEgGkYnPjSej4yMqhalpaPCkU96dzphMGiOUGP1PNWsiKhwaBj8tewgBg==",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
- "jsonwebtoken": "^9.0.0"
+ "jsonwebtoken": "^9.0.0",
+ "linkedom": "^0.18.13"
},
},
"bin": {
"bin": {
"openvibe-build-assets": "scripts/build-assets.js"
"openvibe-build-assets": "scripts/build-assets.js"
@@ -1366,6 +1642,12 @@
@@ -1366,6 +1642,12 @@
"node": ">= 0.6"
"node": ">= 0.6"
}
}
},
},
+ "node_modules/uhyphen": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz",
+ "integrity": "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==",
+ "license": "ISC"
+ },
"node_modules/unpipe": {
"node_modules/unpipe": {
"version": "1.0.0",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -32,7 +32,7 @@
@@ -32,7 +32,7 @@
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.232.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.232.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.33.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.34.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
},
},
"engines": {
"engines": {
new file mode 100644
new file mode 100644
@@ -0,0 +1,17 @@
@@ -0,0 +1,17 @@
+'use strict';
+// What openvibe.codes tells an agent about itself (openvibe-shared/agent-layer): /.well-known/agent.json.
+// Every field is something this site really serves; /start is the harness's own documentation and the JSON API
+// is the public catalog and router, so both are named. A run's page is a person's run, never advertised here.
+module.exports = {
+ id: 'codes',
+ name: 'OpenVibe.Codes',
+ purpose: 'The open coding-agent harness: one router over Claude Code, Codex, OpenCode, Command Code, Aider, the DeepSeek API and your own models, with hand-offs between agents, and the way to improve OpenVibe itself.',
+ home: 'https://openvibe.codes',
+ site: 'openvibe.codes',
+ api: '/api/v1',
+ docs: '/start',
+ policy: {
+ auth: ['none', 'Network OIDC'],
+ rateLimit: '300 page requests a minute per address; 60 public API requests a minute',
+ },
+};
new file mode 100644
new file mode 100644
@@ -0,0 +1,28 @@
@@ -0,0 +1,28 @@
+'use strict';
+// The Markdown twins of openvibe.codes' public pages (openvibe-shared/agent-layer), rendered by the same
+// page builders the routes use (createPageRoutes().publicViews + render/layout.js's renderPage), so a
+// twin cannot drift from its page. Anonymous only; a run's page is a person's run and has no twin.
+const { renderPage } = require('./render/layout');
+
+function createAgentPages(ctx, publicViews) {
+ const views = publicViews;
+ const twin = (page, path) => (page ? { html: renderPage({ ...page, viewer: { kind: 'anonymous' }, config: ctx.config, path }) } : null);
+
+ async function renderPublicPage(path) {
+ if (path === '/') return twin(views.home(), path);
+ if (path === '/start') return twin(views.start(), path);
+ if (path === '/harnesses') return twin(views.harnesses(), path);
+ if (path === '/improve') return twin(views.improve(), path);
+ if (path === '/updates') return twin(views.updates(), path);
+ if (path === '/policy') return twin(views.policy(), path);
+ let m = /^\/policy\/([^/]+)$/.exec(path);
+ if (m) return twin(views.policyDoc(decodeURIComponent(m[1])), path);
+ m = /^\/improve\/([^/]+)\/([^/]+)$/.exec(path);
+ if (m) return twin(await views.improveRepo(decodeURIComponent(m[1]), decodeURIComponent(m[2])), path);
+ return null;
+ }
+
+ return { renderPublicPage };
+}
+
+module.exports = { createAgentPages };
@@ -175,7 +175,21 @@ async function createApp(opts = {}) {
@@ -175,7 +175,21 @@ async function createApp(opts = {}) {
// ── Pages ───────────────────────────────────────────────
// ── Pages ───────────────────────────────────────────────
app.use(rateLimit({ windowMs: 60_000, limit: Number(process.env.CODES_RATE_LIMIT_PER_MIN) || 300, standardHeaders: true, legacyHeaders: false })); // per address; tests raise it
app.use(rateLimit({ windowMs: 60_000, limit: Number(process.env.CODES_RATE_LIMIT_PER_MIN) || 300, standardHeaders: true, legacyHeaders: false })); // per address; tests raise it
- app.use(createPageRoutes(ctx));
+ const pageRoutes = createPageRoutes(ctx);
+ // The agent layer (openvibe-shared/agent-layer): /.well-known/agent.json and the Markdown twins of the public
+ // pages. The pages themselves keep their own routes — a signed-in visitor's account bar and forms are theirs
+ // — so only the manifest and twin requests reach this mount; the pages advertise the twin themselves.
+ const agentLayer = express();
+ require('openvibe-shared/agent-layer').mount(agentLayer, {
+ descriptor: require('./agent-descriptor'),
+ render: require('./agent-pages').createAgentPages(ctx, pageRoutes.publicViews).renderPublicPage,
+ });
+ app.use((req, res, next) => {
+ const twin = req.path.endsWith('.md') || req.query.format === 'markdown' || String(req.headers.accept || '').includes('text/markdown');
+ if (req.path !== '/.well-known/agent.json' && !twin) return next();
+ return agentLayer(req, res, next);
+ });
+ app.use(pageRoutes);
app.use(createRunRoutes({ config, store, sso, harnesses, run, tasks, sessions, keys, fetchImpl: fetchImpl || globalThis.fetch, log }));
app.use(createRunRoutes({ config, store, sso, harnesses, run, tasks, sessions, keys, fetchImpl: fetchImpl || globalThis.fetch, log }));
app.use((req, res) => send(res, 404, { viewer: req.viewer, config, path: req.originalUrl, title: 'Not found', body: html`<h1>Not found</h1><p>No page here. Try <a href="/harnesses">the agents</a>, <a href="/improve">Improve OpenVibe</a>, or the developer platform at <a href="https://openvibe.services">openvibe.services</a>.</p>` }));
app.use((req, res) => send(res, 404, { viewer: req.viewer, config, path: req.originalUrl, title: 'Not found', body: html`<h1>Not found</h1><p>No page here. Try <a href="/harnesses">the agents</a>, <a href="/improve">Improve OpenVibe</a>, or the developer platform at <a href="https://openvibe.services">openvibe.services</a>.</p>` }));
@@ -94,6 +94,7 @@ function createDiscoveryRoutes(ctx) {
@@ -94,6 +94,7 @@ function createDiscoveryRoutes(ctx) {
r.get('/robots.txt', (_req, res) => {
r.get('/robots.txt', (_req, res) => {
res.type('text/plain').set('Cache-Control', TEXT).send(
res.type('text/plain').set('Cache-Control', TEXT).send(
'# openvibe.codes: the public pages are for search and AI crawlers; sign-in and the API are not.\n'
'# openvibe.codes: the public pages are for search and AI crawlers; sign-in and the API are not.\n'
+ + '# Agent manifest: https://openvibe.codes/.well-known/agent.json; every public page has a .md twin.\n'
+ seo.robotsTxt({ sitemaps: [abs('/sitemap.xml')], disallow: DISALLOW }));
+ seo.robotsTxt({ sitemaps: [abs('/sitemap.xml')], disallow: DISALLOW }));
});
});
@@ -84,7 +84,7 @@ function createPageRoutes(ctx) {
@@ -84,7 +84,7 @@ function createPageRoutes(ctx) {
const gitReader = home.createGitReader({ internalUrl: config.git.internalUrl, publicUrl: config.git.publicUrl, enabled: config.nodeEnv !== 'test' });
const gitReader = home.createGitReader({ internalUrl: config.git.internalUrl, publicUrl: config.git.publicUrl, enabled: config.nodeEnv !== 'test' });
// ── Home: code with any agent ───────────────────────────
// ── Home: code with any agent ───────────────────────────
- r.get('/', (req, res) => {
+ function homePage() {
const offers = harnesses.list();
const offers = harnesses.list();
const tasks = Object.keys(placement.TASKS);
const tasks = Object.keys(placement.TASKS);
// What a run prints (bin/openvibe-codes.js), as an example: Codex hits its limit and Claude Code finishes.
// What a run prints (bin/openvibe-codes.js), as an example: Codex hits its limit and Claude Code finishes.
@@ -99,8 +99,8 @@ run run_20261008… · ~/my-project
@@ -99,8 +99,8 @@ run run_20261008… · ~/my-project
→ Edit test/date.test.js
→ Edit test/date.test.js
Added test/date.test.js: three cases, all pass.
Added test/date.test.js: three cases, all pass.
✔ done (48 s · $0.0400 · 31k in / 1.2k out)`;
✔ done (48 s · $0.0400 · 31k in / 1.2k out)`;
- page(req, res, {
- index: true, cache: PUBLIC_CACHE,
+ return {
+ agent: true, index: true, cache: PUBLIC_CACHE,
jsonLd: [...homeJsonLd(config), seo.jsonLd.faq(home.faqPairs())],
jsonLd: [...homeJsonLd(config), seo.jsonLd.faq(home.faqPairs())],
styles: [showcase.STYLESHEET],
styles: [showcase.STYLESHEET],
body: html`${raw(showcase.hero({
body: html`${raw(showcase.hero({
@@ -142,11 +142,12 @@ ${table(['Piece', 'State'], [
@@ -142,11 +142,12 @@ ${table(['Piece', 'State'], [
${raw(home.faq())}
${raw(home.faq())}
${raw(showcase.network({ service: 'codes' }))}
${raw(showcase.network({ service: 'codes' }))}
${raw(showcase.cta({ title: 'Building an app instead?', text: 'Projects, keys, grants and the API docs of every OpenVibe service live on OpenVibe.Services.', actions: [{ label: 'Open the developer platform', href: SERVICES_ORIGIN, primary: true }, { label: 'Meet the agents', href: '/harnesses' }] }))}`,
${raw(showcase.cta({ title: 'Building an app instead?', text: 'Projects, keys, grants and the API docs of every OpenVibe service live on OpenVibe.Services.', actions: [{ label: 'Open the developer platform', href: SERVICES_ORIGIN, primary: true }, { label: 'Meet the agents', href: '/harnesses' }] }))}`,
- });
- });
+ };
+ }
+ r.get('/', (req, res) => page(req, res, homePage()));
// ── Getting started: the harness on your own machine (bin/openvibe-codes.js, harness/) ──
// ── Getting started: the harness on your own machine (bin/openvibe-codes.js, harness/) ──
- r.get('/start', (req, res) => {
+ function startPage() {
const example = `$ openvibe-codes agents
const example = `$ openvibe-codes agents
✔ claude-code Claude Code ~/.local/bin/claude
✔ claude-code Claude Code ~/.local/bin/claude
✔ codex Codex ~/.local/bin/codex
✔ codex Codex ~/.local/bin/codex
@@ -169,8 +170,8 @@ for await (const event of harness.run({ prompt: 'fix the failing test', cwd: '.'
@@ -169,8 +170,8 @@ for await (const event of harness.run({ prompt: 'fix the failing test', cwd: '.'
{"type":"user","message":{"content":[{"type":"tool_result","content":"…","is_error":false}]}}
{"type":"user","message":{"content":[{"type":"tool_result","content":"…","is_error":false}]}}
{"type":"system","subtype":"handoff","from":"codex","reason":"You have hit your usage limit."}
{"type":"system","subtype":"handoff","from":"codex","reason":"You have hit your usage limit."}
{"type":"result","subtype":"success","is_error":false,"result":"…","total_cost_usd":0.04,"usage":{…}}`;
{"type":"result","subtype":"success","is_error":false,"result":"…","total_cost_usd":0.04,"usage":{…}}`;
- page(req, res, {
- index: true, cache: PUBLIC_CACHE,
+ return {
+ agent: true, index: true, cache: PUBLIC_CACHE,
title: 'Get started with the coding-agent harness',
title: 'Get started with the coding-agent harness',
description: 'Install openvibe-codes and run coding tasks on Claude Code, Codex, OpenCode, Command Code, Aider, the DeepSeek API or your own model, with hand-offs between them, from your terminal or your own code.',
description: 'Install openvibe-codes and run coding tasks on Claude Code, Codex, OpenCode, Command Code, Aider, the DeepSeek API or your own model, with hand-offs between them, from your terminal or your own code.',
crumbs: [{ label: 'Get started' }],
crumbs: [{ label: 'Get started' }],
@@ -222,8 +223,9 @@ ${table(['Level', 'What the agent may do'], [
@@ -222,8 +223,9 @@ ${table(['Level', 'What the agent may do'], [
<p><code>harness.agents()</code>, <code>harness.route({ task })</code> and <code>harness.run({ prompt, cwd, agent, permission, handoff })</code> are what the command line uses. The router is the same one behind <a href="/api/v1/harnesses"><code>/api/v1/harnesses/route</code></a>.</p>
<p><code>harness.agents()</code>, <code>harness.route({ task })</code> and <code>harness.run({ prompt, cwd, agent, permission, handoff })</code> are what the command line uses. The router is the same one behind <a href="/api/v1/harnesses"><code>/api/v1/harnesses/route</code></a>.</p>
<h2 id="next">What comes next</h2>
<h2 id="next">What comes next</h2>
<p>Tasks from the web already run on your own machine (<a href="/run">Run on your machine</a>). Next: a hosted runner on OpenVibe.Run workers for people who would rather not run agents themselves, with budgets; OpenVibe.Actor using Codes for its coding work; and <a href="/improve">Improve OpenVibe</a> as a guided run: pick a repository, describe the change, get a pull request.</p>`,
<p>Tasks from the web already run on your own machine (<a href="/run">Run on your machine</a>). Next: a hosted runner on OpenVibe.Run workers for people who would rather not run agents themselves, with budgets; OpenVibe.Actor using Codes for its coding work; and <a href="/improve">Improve OpenVibe</a> as a guided run: pick a repository, describe the change, get a pull request.</p>`,
- });
- });
+ };
+ }
+ r.get('/start', (req, res) => page(req, res, startPage()));
// ── The agent catalog (server/data/harness-offers.json, validated at boot) ──
// ── The agent catalog (server/data/harness-offers.json, validated at boot) ──
// No address target that is a local path is ever shown — only the address kind.
// No address target that is a local path is ever shown — only the address kind.
@@ -236,10 +238,10 @@ ${table(['Level', 'What the agent may do'], [
@@ -236,10 +238,10 @@ ${table(['Level', 'What the agent may do'], [
const usd = (n) => `$${Number(n || 0).toFixed(2)}`;
const usd = (n) => `$${Number(n || 0).toFixed(2)}`;
const duration = (n) => (n >= 3600 && n % 3600 === 0 ? `${n / 3600} h` : `${n} s`);
const duration = (n) => (n >= 3600 && n % 3600 === 0 ? `${n / 3600} h` : `${n} s`);
const count = (n) => Number(n).toLocaleString('en-US');
const count = (n) => Number(n).toLocaleString('en-US');
- r.get('/harnesses', (req, res) => {
+ function harnessesPage() {
const offers = harnesses.list();
const offers = harnesses.list();
- page(req, res, {
- index: true, cache: PUBLIC_CACHE,
+ return {
+ agent: true, index: true, cache: PUBLIC_CACHE,
title: 'The coding agents',
title: 'The coding agents',
description: 'Claude Code, Codex, OpenCode, Command Code, Aider and the DeepSeek API: every coding harness OpenVibe.Codes routes a task to, with its models, capabilities, prices and limits.',
description: 'Claude Code, Codex, OpenCode, Command Code, Aider and the DeepSeek API: every coding harness OpenVibe.Codes routes a task to, with its models, capabilities, prices and limits.',
crumbs: [{ label: 'Agents' }],
crumbs: [{ label: 'Agents' }],
@@ -262,8 +264,9 @@ ${offers.map((o) => html`<h3>${o.name} <code>${o.id}</code></h3>${o.agents.lengt
@@ -262,8 +264,9 @@ ${offers.map((o) => html`<h3>${o.name} <code>${o.id}</code></h3>${o.agents.lengt
`${usd(a.price_per_1k_tokens.fresh_usd)} fresh, ${usd(a.price_per_1k_tokens.cached_usd)} cached, ${usd(a.price_per_1k_tokens.output_usd)} output`,
`${usd(a.price_per_1k_tokens.fresh_usd)} fresh, ${usd(a.price_per_1k_tokens.cached_usd)} cached, ${usd(a.price_per_1k_tokens.output_usd)} output`,
]))
]))
: html`<p class="muted">no models yet</p>`}`)}`,
: html`<p class="muted">no models yet</p>`}`)}`,
- });
- });
+ };
+ }
+ r.get('/harnesses', (req, res) => page(req, res, harnessesPage()));
// ── Improve OpenVibe ─────────────────────────────────────
// ── Improve OpenVibe ─────────────────────────────────────
const knownRepo = (owner, name) => {
const knownRepo = (owner, name) => {
@@ -312,11 +315,11 @@ ${offers.map((o) => html`<h3>${o.name} <code>${o.id}</code></h3>${o.agents.lengt
@@ -312,11 +315,11 @@ ${offers.map((o) => html`<h3>${o.name} <code>${o.id}</code></h3>${o.agents.lengt
const repoForgejoUrl = (owner, name) => `${config.git.publicUrl}/${owner}/${name}`;
const repoForgejoUrl = (owner, name) => `${config.git.publicUrl}/${owner}/${name}`;
const repoCloneUrl = (owner, name) => `${config.git.publicUrl}/${owner}/${name}.git`;
const repoCloneUrl = (owner, name) => `${config.git.publicUrl}/${owner}/${name}.git`;
- r.get('/improve', (req, res) => {
+ function improvePage() {
const { running, libraries } = repoGroups();
const { running, libraries } = repoGroups();
const repoCards = (list) => html`<ul class="cards">${list.map((m) => html`<li><a href="/improve/${m.repository}"><strong>${m.repository.split('/')[1]}</strong></a><span>${(m.site && (m.site.tagline || m.site.what)) || m.name}</span></li>`)}</ul>`;
const repoCards = (list) => html`<ul class="cards">${list.map((m) => html`<li><a href="/improve/${m.repository}"><strong>${m.repository.split('/')[1]}</strong></a><span>${(m.site && (m.site.tagline || m.site.what)) || m.name}</span></li>`)}</ul>`;
- page(req, res, {
- index: true, cache: PUBLIC_CACHE,
+ return {
+ agent: true, index: true, cache: PUBLIC_CACHE,
title: 'Improve OpenVibe',
title: 'Improve OpenVibe',
description: 'OpenVibe is open source: every service and library is on GitHub. Pick a repository, describe the change, and send a pull request; the harness makes the change on your machine.',
description: 'OpenVibe is open source: every service and library is on GitHub. Pick a repository, describe the change, and send a pull request; the harness makes the change on your machine.',
crumbs: [{ label: 'Improve OpenVibe' }],
crumbs: [{ label: 'Improve OpenVibe' }],
@@ -333,19 +336,20 @@ ${offers.map((o) => html`<h3>${o.name} <code>${o.id}</code></h3>${o.agents.lengt
@@ -333,19 +336,20 @@ ${offers.map((o) => html`<h3>${o.name} <code>${o.id}</code></h3>${o.agents.lengt
${repoCards(running)}
${repoCards(running)}
<h2>Shared libraries</h2>
<h2>Shared libraries</h2>
${repoCards(libraries)}`,
${repoCards(libraries)}`,
- });
- });
+ };
+ }
+ r.get('/improve', (req, res) => page(req, res, improvePage()));
- r.get('/improve/:owner/:name', async (req, res) => {
- const { owner, name } = req.params;
+ async function improveRepoPage(owner, name, { signed = false } = {}) {
const meta = repoMeta().find((m) => m.repository === `${owner}/${name}`);
const meta = repoMeta().find((m) => m.repository === `${owner}/${name}`);
- if (!meta) return page(req, res, { title: 'Not found', body: html`<h1>Not found</h1><p>No such OpenVibe repository. <a href="/improve">All repositories</a></p>` }, 404);
+ if (!meta) return null;
let forgejoInfo = null;
let forgejoInfo = null;
let forgejoError = null;
let forgejoError = null;
- try { forgejoInfo = await ctx.forgejo.getRepo(owner, name); } catch (e) { forgejoError = e.code === 'forgejo.unconfigured' ? null : e.message; }
+ if (signed) {
+ try { forgejoInfo = await ctx.forgejo.getRepo(owner, name); } catch (e) { forgejoError = e.code === 'forgejo.unconfigured' ? null : e.message; }
+ }
const agents = harnesses.list();
const agents = harnesses.list();
const opt = (v, label, sel) => html`<option value="${v}"${sel ? raw(' selected') : ''}>${label}</option>`;
const opt = (v, label, sel) => html`<option value="${v}"${sel ? raw(' selected') : ''}>${label}</option>`;
- const signed = signedIn(req);
const form = signed ? html`<form class="run-card run-form" method="post" action="/improve/start">
const form = signed ? html`<form class="run-card run-form" method="post" action="/improve/start">
<input type="hidden" name="repo" value="${repoForgejoUrl(owner, name)}">
<input type="hidden" name="repo" value="${repoForgejoUrl(owner, name)}">
<input type="hidden" name="ref" value="${(forgejoInfo && forgejoInfo.default_branch) || 'main'}">
<input type="hidden" name="ref" value="${(forgejoInfo && forgejoInfo.default_branch) || 'main'}">
@@ -359,9 +363,9 @@ ${forgejoError ? html`<p class="run-notice run-notice-error" role="alert">OpenVi
@@ -359,9 +363,9 @@ ${forgejoError ? html`<p class="run-notice run-notice-error" role="alert">OpenVi
<label class="run-check"><input type="checkbox" name="push" value="1" checked> Push the change to a new branch on OpenVibe Git <small>(needs a token on your machine; never the branch it started from)</small></label>
<label class="run-check"><input type="checkbox" name="push" value="1" checked> Push the change to a new branch on OpenVibe Git <small>(needs a token on your machine; never the branch it started from)</small></label>
<label class="run-check"><input type="checkbox" name="pull_request" value="1" checked> and open a pull request</label>
<label class="run-check"><input type="checkbox" name="pull_request" value="1" checked> and open a pull request</label>
<button class="run-btn run-btn-primary" type="submit">Start on my machine</button>
<button class="run-btn run-btn-primary" type="submit">Start on my machine</button>
-</form>` : html`<p><a class="run-btn run-btn-primary" href="/auth/login?next=${encodeURIComponent(req.originalUrl)}">Sign in to start a run</a></p>`;
- page(req, res, {
- index: true, cache: PUBLIC_CACHE,
+</form>` : html`<p><a class="run-btn run-btn-primary" href="/auth/login?next=${encodeURIComponent(`/improve/${owner}/${name}`)}">Sign in to start a run</a></p>`;
+ return {
+ agent: true, index: true, cache: PUBLIC_CACHE,
title: `Improve ${name}`,
title: `Improve ${name}`,
description: `Contribute to ${meta.name}: describe a change and run it on your machine through OpenVibe.Codes.`,
description: `Contribute to ${meta.name}: describe a change and run it on your machine through OpenVibe.Codes.`,
crumbs: [{ label: 'Improve OpenVibe', href: '/improve' }, { label: meta.name }],
crumbs: [{ label: 'Improve OpenVibe', href: '/improve' }, { label: meta.name }],
@@ -376,7 +380,12 @@ ${forgejoError ? html`<p class="run-notice run-notice-error" role="alert">OpenVi
@@ -376,7 +380,12 @@ ${forgejoError ? html`<p class="run-notice run-notice-error" role="alert">OpenVi
<li>You review the result, then the maintainers review the pull request.</li>
<li>You review the result, then the maintainers review the pull request.</li>
</ol>
</ol>
${form}`,
${form}`,
- });
+ };
+ }
+ r.get('/improve/:owner/:name', async (req, res) => {
+ const out = await improveRepoPage(req.params.owner, req.params.name, { signed: signedIn(req) });
+ if (!out) return page(req, res, { title: 'Not found', body: html`<h1>Not found</h1><p>No such OpenVibe repository. <a href="/improve">All repositories</a></p>` }, 404);
+ page(req, res, out);
});
});
r.post('/improve/start', express.urlencoded({ extended: false, limit: '32kb' }), async (req, res) => {
r.post('/improve/start', express.urlencoded({ extended: false, limit: '32kb' }), async (req, res) => {
@@ -424,25 +433,33 @@ ${form}`,
@@ -424,25 +433,33 @@ ${form}`,
// ── Community documents and the update log ───────────────
// ── Community documents and the update log ───────────────
// What shipped on OpenVibe.Codes: the shared update log every OpenVibe site has.
// What shipped on OpenVibe.Codes: the shared update log every OpenVibe site has.
- r.get('/updates', (req, res) => page(req, res, { index: true, cache: PUBLIC_CACHE, title: 'What shipped on OpenVibe.Codes', body: raw(frame.updatesBody({ service: 'codes', siteName: 'OpenVibe.Codes' }) + `<script src="${ovServe.url('shipped.js')}" defer></script>`) }));
- r.get('/policy', (req, res) => page(req, res, {
- index: true, cache: PUBLIC_CACHE, title: 'Community', crumbs: [{ label: 'Community' }],
+ const updatesPage = () => ({ agent: true, index: true, cache: PUBLIC_CACHE, title: 'What shipped on OpenVibe.Codes', body: raw(frame.updatesBody({ service: 'codes', siteName: 'OpenVibe.Codes' }) + `<script src="${ovServe.url('shipped.js')}" defer></script>`) });
+ r.get('/updates', (req, res) => page(req, res, updatesPage()));
+ const policyIndexPage = () => ({
+ agent: true, index: true, cache: PUBLIC_CACHE, title: 'Community', crumbs: [{ label: 'Community' }],
body: html`<h1>Community</h1>
body: html`<h1>Community</h1>
<p>The rules for everyone who builds OpenVibe and everything published on it: the repositories, and the apps and releases on OpenVibe.Services.</p>
<p>The rules for everyone who builds OpenVibe and everything published on it: the repositories, and the apps and releases on OpenVibe.Services.</p>
<ul class="cards">
<ul class="cards">
${governance.map((g) => html`<li><a href="/policy/${g.slug}"><strong>${g.title}</strong></a><span>${g.blurb}${g.draft ? ' (draft, pending owner review)' : ''}</span></li>`)}</ul>
${governance.map((g) => html`<li><a href="/policy/${g.slug}"><strong>${g.title}</strong></a><span>${g.blurb}${g.draft ? ' (draft, pending owner review)' : ''}</span></li>`)}</ul>
<p class="muted">The platform's own policy (how a contract or capability changes, compatibility and deprecation, licensing, transparency) is on <a href="${SERVICES_ORIGIN}/policy">OpenVibe.Services</a>.</p>`,
<p class="muted">The platform's own policy (how a contract or capability changes, compatibility and deprecation, licensing, transparency) is on <a href="${SERVICES_ORIGIN}/policy">OpenVibe.Services</a>.</p>`,
- }));
+ });
+ r.get('/policy', (req, res) => page(req, res, policyIndexPage()));
- for (const g of governance) {
- r.get(`/policy/${g.slug}`, (req, res) => page(req, res, {
- index: !g.draft, cache: PUBLIC_CACHE, title: g.title, crumbs: [{ label: 'Community', href: '/policy' }, { label: g.title }],
+ const policyDocPage = (g) => ({
+ agent: true, index: !g.draft, cache: PUBLIC_CACHE, title: g.title, crumbs: [{ label: 'Community', href: '/policy' }, { label: g.title }],
body: html`<h1>${g.title}</h1>
body: html`<h1>${g.title}</h1>
${g.draft ? notice(html`<strong>Draft — pending owner review.</strong> This is a proposal, not yet in effect. It takes effect when the owner of the OpenVibers organization approves it.`, 'warn') : ''}
${g.draft ? notice(html`<strong>Draft — pending owner review.</strong> This is a proposal, not yet in effect. It takes effect when the owner of the OpenVibers organization approves it.`, 'warn') : ''}
<article class="prose">${raw(markdown(g.body, { headingOffset: 0 }))}</article>
<article class="prose">${raw(markdown(g.body, { headingOffset: 0 }))}</article>
<p class="muted small">Source: <a href="${GITHUB}/OpenVibe.Codes/blob/main/${g.file}"><code>${g.file}</code></a>. Changes go through a pull request that the owner approves.</p>`,
<p class="muted small">Source: <a href="${GITHUB}/OpenVibe.Codes/blob/main/${g.file}"><code>${g.file}</code></a>. Changes go through a pull request that the owner approves.</p>`,
- }));
- }
+ });
+ for (const g of governance) r.get(`/policy/${g.slug}`, (req, res) => page(req, res, policyDocPage(g)));
+
+ // The public pages the agent layer's Markdown twins (server/agent-pages.js) render with the same builders.
+ r.publicViews = {
+ home: homePage, start: startPage, harnesses: harnessesPage, improve: improvePage,
+ improveRepo: improveRepoPage, updates: updatesPage, policy: policyIndexPage,
+ policyDoc: (slug) => { const g = governance.find((x) => x.slug === String(slug || '')); return g ? policyDocPage(g) : null; },
+ };
// ── Discovery: robots.txt, sitemap.xml, llms.txt, llms-full.txt (http/discovery.js, openvibe-shared/seo) ──
// ── Discovery: robots.txt, sitemap.xml, llms.txt, llms-full.txt (http/discovery.js, openvibe-shared/seo) ──
r.use(createDiscoveryRoutes({ ...ctx, governance }));
r.use(createDiscoveryRoutes({ ...ctx, governance }));
@@ -90,6 +90,10 @@ function renderPage(o) {
@@ -90,6 +90,10 @@ function renderPage(o) {
description: o.description || 'The open coding-agent harness: Claude Code, Codex, OpenCode, Command Code, Aider, the DeepSeek API and your own models behind one router, and the way to improve OpenVibe itself.',
description: o.description || 'The open coding-agent harness: Claude Code, Codex, OpenCode, Command Code, Aider, the DeepSeek API and your own models behind one router, and the way to improve OpenVibe itself.',
canonical,
canonical,
robots: o.index ? 'index, follow' : 'noindex, nofollow',
robots: o.index ? 'index, follow' : 'noindex, nofollow',
+ // A page with a Markdown twin (o.agent) advertises it in the head and the Link header; the twin itself
+ // is server/agent-pages.js.
+ agentLayer: o.agent === true,
+ llms: `${String(o.config.baseUrl).replace(/\/+$/, '')}/llms.txt`,
jsonLd: (o.jsonLd || []).filter(Boolean),
jsonLd: (o.jsonLd || []).filter(Boolean),
home: '/',
home: '/',
navLinks: NAV,
navLinks: NAV,
@@ -128,6 +132,10 @@ function send(res, status, o) {
@@ -128,6 +132,10 @@ function send(res, status, o) {
const personal = o.viewer && o.viewer.kind === 'user';
const personal = o.viewer && o.viewer.kind === 'user';
if (!res.get('Cache-Control')) res.set('Cache-Control', !personal && o.cache ? o.cache : cache.htmlHeaders({ private: true }));
if (!res.get('Cache-Control')) res.set('Cache-Control', !personal && o.cache ? o.cache : cache.htmlHeaders({ private: true }));
res.set('Vary', 'Cookie');
res.set('Vary', 'Cookie');
+ if (o.agent) {
+ const site = String(o.config.baseUrl).replace(/\/+$/, '');
+ res.set('Link', require('openvibe-shared/agent-layer').linkHeaderTags(`${site}${String(o.path || '/').split('?')[0]}`, `${site}/llms.txt`).header);
+ }
res.status(status).type('html').send(renderPage(o));
res.status(status).type('html').send(renderPage(o));
}
}