openvibers/openvibe.codes Commits
The openvibe.codes home shows OpenVibe Git (its repositories, newest first, and how to clone), a quick start for the harness and the public router, and the questions people ask (#94)
Changes
@@@ -80,3 +80,17 @@ meter.headroom { width: 8rem; max-width: 100%; vertical-align: middle; }.page .sc-ic .ov-icon { --ovi-size: 28px !important; }.codes-hero-code { margin: 0; padding: 16px; border-radius: 14px; border: 1px solid var(--border, #1f2d47); background: var(--bg-primary, #0a0f1c); font-size: 13px; line-height: 1.6; overflow: auto; }++++/* The home page: quick-start samples one under another, OpenVibe Git's repositories, and the FAQ. */++#quick-start .sc-code { grid-template-columns: minmax(0, 1fr); }++.codes-repos { list-style: none; margin: 0 0 12px; padding: 0; display: grid; gap: 10px; grid-template-columns: repeat(auto-fill, minmax(min(240px, 100%), 1fr)); }++.codes-repos a { display: flex; flex-direction: column; gap: 3px; height: 100%; padding: 12px 14px; border-radius: 14px; border: 1px solid var(--border, #1f2d47); background: var(--bg-secondary, #101828); color: inherit; text-decoration: none; }++.codes-repos a:hover, .codes-repos a:focus-visible { border-color: var(--accent, #3b82f6); }++.codes-repos b { font-size: 14.5px; overflow-wrap: anywhere; }++.codes-repos small { font-size: 13px; line-height: 1.4; color: var(--text-secondary, #96a7c2); }++.codes-repos span { margin-top: auto; font-size: 12px; color: var(--text-muted, #7386a3); }++.codes-clone { margin: 0; padding: 12px 14px; border-radius: 12px; border: 1px solid var(--border, #1f2d47); background: var(--bg-primary, #0a0f1c); font-size: 13px; overflow: auto; }++.codes-faq { display: grid; gap: 12px; grid-template-columns: repeat(auto-fill, minmax(min(320px, 100%), 1fr)); }++.codes-faq > div { padding: 16px 18px; border-radius: 16px; border: 1px solid var(--border, #1f2d47); background: var(--bg-secondary, #101828); }++.codes-faq h3 { margin: 0 0 6px; font-size: 15.5px; line-height: 1.35; }++.codes-faq p { margin: 0; font-size: 14px; line-height: 1.55; color: var(--text-secondary, #96a7c2); }@@@ -43,6 +43,12 @@ function load(env = process.env) {networkUrl,networkInternalUrl: trim(env.OV_NETWORK_INTERNAL_URL || 'http://127.0.0.1:4000'),networkIssuer: trim(env.OV_NETWORK_ISSUER || networkUrl),++ // OpenVibe Git (Forgejo, deploy/forgejo): its loopback address for the home page's repository list (public++ // repositories only, no token), and the public address people use.++ git: {++ internalUrl: trim(env.CODES_GIT_INTERNAL_URL || 'http://127.0.0.1:5060'),++ publicUrl: trim(env.CODES_GIT_URL || 'https://git.openvibe.codes'),++ },// The audience of the Network service tokens a first-party service calls /api/v1/tasks with (codes.task.*).serviceAudience: env.CODES_AUDIENCE || 'openvibe.codes',oauth: {nnew file mode 100644@@@ -0,0 +1,105 @@++'use strict';++/**++ * The home page's sections beyond the kit's generic ones: OpenVibe Git (the forge at git.openvibe.codes, read from its++ * public API: how many repositories, which changed last), quick-start commands for the harness and the public router,++ * and the questions people ask, which the FAQPage JSON-LD repeats. The repository list is read in the background at most++ * every ten minutes and never awaited by a page; a forge that does not answer leaves the list out.++ */++const contracts = require('openvibe-contracts');++const showcase = require('openvibe-shared/showcase');++++const esc = showcase.esc;++++function createGitReader({ internalUrl, publicUrl, enabled = true, fetchImpl = globalThis.fetch, ttlMs = 10 * 60_000, timeoutMs = 3000, limit = 8 } = {}) {++ let value = null;++ let at = 0;++ let inflight = null;++ const pub = String(publicUrl || 'https://git.openvibe.codes').replace(/\/+$/, '');++ async function refresh() {++ if (inflight) return inflight;++ inflight = (async () => {++ try {++ const res = await fetchImpl(`${String(internalUrl).replace(/\/+$/, '')}/api/v1/repos/search?limit=${limit}&sort=updated&order=desc`, { headers: { Accept: 'application/json' }, signal: AbortSignal.timeout(timeoutMs) });++ if (res.ok) {++ const body = await res.json();++ const total = Number(res.headers.get('x-total-count')) || null;++ const repos = (Array.isArray(body.data) ? body.data : [])++ .filter((r) => r && !r.private && /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/.test(String(r.full_name || '')))++ .map((r) => ({ name: r.full_name, url: `${pub}/${r.full_name}`, updated: r.updated_at || null, language: r.language || '' }));++ if (repos.length) value = { total: total && total >= repos.length ? total : null, repos, publicUrl: pub };++ }++ } catch { /* keep the last answer */ }++ at = Date.now();++ return value;++ })().finally(() => { inflight = null; });++ return inflight;++ }++ return {++ peek() { if (enabled && Date.now() - at > ttlMs && !inflight) refresh().catch(() => {}); return value; }, // floating-ok: refresh catches its own errors++ refresh,++ /** Tests. */++ _set(v) { value = v; at = Date.now(); },++ };++}++++// A repository's line: the tagline of the service it builds, from the pinned contracts, when one names it.++const taglines = new Map(contracts.services.manifests.filter((m) => m && m.repository && m.site && m.site.tagline).map((m) => [m.repository, m.site.tagline]));++function ago(iso, now = Date.now()) {++ const t = Date.parse(iso || '');++ if (!Number.isFinite(t)) return '';++ const m = Math.max(0, Math.round((now - t) / 60000));++ if (m < 60) return `${m} min ago`;++ const h = Math.round(m / 60);++ if (h < 48) return `${h} h ago`;++ return `${Math.round(h / 24)} days ago`;++}++++function gitSection(git, now = Date.now()) {++ const pub = (git && git.publicUrl) || 'https://git.openvibe.codes';++ const list = git && git.repos && git.repos.length ? `<ul class="codes-repos">${git.repos.map((r) => {++ const line = taglines.get(r.name) || '';++ return `<li><a href="${esc(r.url)}"><b>${esc(r.name.replace(/^OpenVibers\//, ''))}</b>${line ? `<small>${esc(line)}</small>` : ''}<span>${[r.language, r.updated ? `updated ${ago(r.updated, now)}` : ''].filter(Boolean).map(esc).join(' · ')}</span></a></li>`;++ }).join('')}</ul>` : '';++ const count = git && git.total ? `${git.total} repositories` : 'Every OpenVibe repository';++ return `<section class="sc-sec" id="git" aria-labelledby="h-git"><h2 id="h-git">OpenVibe Git</h2>++<p class="sc-lede">${esc(count)} on OpenVibe's own code forge at <a href="${esc(pub)}/">git.openvibe.codes</a>: browse, clone over HTTPS and sign in with your OpenVibe account, in the network's theme. The repositories are mirrored from GitHub every hour; GitHub stays the home of each one until it moves here.</p>++${list}<pre tabindex="0" class="codes-clone"><code>git clone ${esc(pub)}/OpenVibers/OpenVibe.Codes.git</code></pre>++<p class="sc-note"><a href="${esc(pub)}/OpenVibers">All OpenVibers repositories</a> · <a href="${esc(pub)}/user/login">Sign in with OpenVibe</a></p></section>`;++}++++/** The harness from a terminal, and the public router any program can ask. */++function quickStart({ install }) {++ return showcase.code({++ id: 'quick-start', title: 'Start in a minute',++ lede: 'The harness runs on your machine with the agent CLIs and keys you already have. The router is public: ask it which agent fits a task, no key needed.',++ samples: [++ { label: 'Install and run', lang: 'bash', code: `${install}++openvibe-codes agents # which agents this machine can run++cd my-project++openvibe-codes run "add a test for the date parser"++openvibe-codes run --permission read "where is the session cookie set?"++openvibe-codes runs # every run, kept to show or resume` },++ { label: 'Ask the router (public API)', lang: 'bash', code: `curl -s https://openvibe.codes/api/v1/harnesses/route \\++ -H 'Content-Type: application/json' -d '{ "task": "review" }'++# → { "selected": "claude-code:claude-code-sonnet",++# "reasons": [ "no marginal cost (already-paid capacity or a free allowance)", … ],++# "candidates": [ … every agent, with why it was or was not picked ] }` },++ ],++ });++}++++// The questions people ask. The first three are Codes' own in the contracts (the answers OpenVibe.Help shows).++const EXTRA_FAQ = [++ { q: 'Does it cost anything?', a: 'The harness is free and open source. Each agent runs with your own CLI and keys, so you pay its provider as you already do; the router prefers capacity you have already paid for and free allowances.' },++ { q: 'What happens when an agent gets stuck?', a: 'A failed, rate-limited or silent agent is stopped and the task goes to the next one with a note: the task, why the last agent stopped, the files it changed and the state of the tree.' },++ { q: 'What is OpenVibe Git?', a: 'OpenVibe\'s own code forge at git.openvibe.codes, where you sign in with your OpenVibe account. It holds every public OpenVibe repository, mirrored from GitHub every hour.' },++];++function faqPairs() {++ const m = contracts.services.get('codes');++ const own = ((m && m.site && m.site.faq) || []).filter((f) => f && f.question && f.answer).map((f) => ({ q: f.question, a: f.answer }));++ const seen = new Set();++ return [...own, ...EXTRA_FAQ].filter((p) => !seen.has(p.q) && seen.add(p.q));++}++const faq = () => `<section class="sc-sec" id="faq" aria-labelledby="h-faq"><h2 id="h-faq">Questions people ask</h2><div class="codes-faq">${faqPairs().map((p) => `<div><h3>${esc(p.q)}</h3><p>${esc(p.a)}</p></div>`).join('')}</div></section>`;++++module.exports = { createGitReader, gitSection, quickStart, faq, faqPairs, ago };@@@ -24,6 +24,8 @@ const { html, raw, table, notice } = require('../render/html');const { send } = require('../render/layout');const { markdown } = require('../render/markdown');const placement = require('../domain/harness-placement');++const home = require('./home');++const seo = require('openvibe-shared/seo');const ROOT = path.join(__dirname, '..', '..');const GITHUB = 'https://github.com/OpenVibers';@@@ -76,12 +78,16 @@ function createPageRoutes(ctx) {const PUBLIC_CACHE = cache.htmlHeaders({ maxAge: 300 });const governance = loadGovernance();const page = (req, res, o, status = 200) => send(res, status, { viewer: req.viewer, config, path: req.originalUrl, ...o });++ // OpenVibe Git's public repositories for the home (read in the background, never awaited here); tests set ctx.git.++ 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) => {const offers = harnesses.list();const tasks = Object.keys(placement.TASKS);-- const snippet = `$ openvibe-codes run "add a test for the date parser"++ // What a run prints (bin/openvibe-codes.js), as an example: Codex hits its limit and Claude Code finishes.++ const snippet = `# an example run: one agent hits its limit, the next one finishes++$ openvibe-codes run "add a test for the date parser"run run_20261008… · ~/my-project▶ codex · session 0199…! You have hit your usage limit.@@@ -93,7 +99,7 @@ Added 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,-- jsonLd: homeJsonLd(config),++ jsonLd: [...homeJsonLd(config), seo.jsonLd.faq(home.faqPairs())],styles: [showcase.STYLESHEET],body: html`${raw(showcase.hero({eyebrow: 'OpenVibe.Codes · the open coding-agent harness · alpha',@@@ -116,6 +122,8 @@ ${raw(showcase.steps({{ title: 'Hand-offs', text: 'When an agent fails, hits its usage limit or goes quiet, the task moves to the next one with a note: the task, the files touched and the state of the tree.' },],}))}++${raw(home.quickStart({ install: INSTALL }))}++${raw(home.gitSection((ctx.git || gitReader).peek()))}<section class="sc-sec" aria-labelledby="h-improve"><h2 id="h-improve">Improve OpenVibe</h2><p class="sc-lede">OpenVibe is built in the open, ${repositories().running.length} services and their shared libraries, every one on GitHub. Pick a repository, describe the change, and send a pull request; the harness's job is to make that one step.</p><p><a class="sc-btn sc-primary" href="/improve">Start contributing</a> <a class="sc-btn" href="/policy/contributing">Read the guide</a></p></section>@@@ -127,7 +135,9 @@ ${table(['Piece', 'State'], [[html`<a href="/harnesses">The agent catalog</a>`, 'Live: every harness and model with its capabilities, prices and limits.'],[html`<code>GET /api/v1/harnesses</code>, <code>POST /api/v1/harnesses/route</code>`, 'Live: public, no key needed; per-address and per-caller limits.'],[html`<a href="/improve">Improve OpenVibe</a>: a repository, a change on a branch with its test, a pull request that CI checks and a maintainer reviews`, 'Live: by hand, or with the harness doing the change on your machine.'],++ [html`<a href="${config.git.publicUrl}/">OpenVibe Git</a>: OpenVibe's own code forge`, 'Live: every public OpenVibe repository, mirrored from GitHub hourly, with OpenVibe sign-in and HTTPS cloning. GitHub stays canonical until a repository moves here.'],])}</section>++${raw(home.faq())}${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' }] }))}`,});});nnew file mode 100644@@@ -0,0 +1,61 @@++'use strict';++/**++ * The home page's own sections (server/http/home.js): OpenVibe Git from the forge's public API (left out when it never++ * answered), the quick start with the release-tag install line and the public router, and a FAQ that its FAQPage JSON-LD++ * repeats word for word. The git reader keeps only public repositories with a plain owner/name.++ */++const assert = require('assert');++const http = require('http');++const home = require('../server/http/home');++const { boot, check, done } = require('./helpers/boot');++++(async () => {++ await check('the git reader: public repositories, newest first as the forge answers, with the total it reports', async () => {++ const srv = http.createServer((req, res) => {++ assert.match(req.url, /^\/api\/v1\/repos\/search\?limit=8&sort=updated&order=desc$/);++ res.setHeader('Content-Type', 'application/json');++ res.setHeader('X-Total-Count', '52');++ res.end(JSON.stringify({ ok: true, data: [++ { full_name: 'OpenVibers/OpenVibe.Codes', private: false, updated_at: '2026-10-22T10:00:00Z', language: 'JavaScript' },++ { full_name: 'OpenVibers/secret', private: true },++ { full_name: 'bad name/<x>', private: false },++ ] }));++ });++ await new Promise((r) => srv.listen(0, '127.0.0.1', r));++ try {++ const reader = home.createGitReader({ internalUrl: `http://127.0.0.1:${srv.address().port}`, publicUrl: 'https://git.openvibe.codes' });++ const v = await reader.refresh();++ assert.strictEqual(v.total, 52);++ assert.deepStrictEqual(v.repos.map((r) => r.url), ['https://git.openvibe.codes/OpenVibers/OpenVibe.Codes']);++ } finally { srv.close(); }++ const down = home.createGitReader({ internalUrl: 'http://127.0.0.1:9', timeoutMs: 500 });++ assert.strictEqual(await down.refresh(), null, 'a forge that does not answer gives no list');++ assert.strictEqual(home.ago('2026-10-22T10:00:00Z', Date.parse('2026-10-22T12:00:00Z')), '2 h ago');++ });++++ const t = await boot();++ try {++ await check('the home: quick start, OpenVibe Git, a FAQ and its JSON-LD', async () => {++ let r = await t.get('/');++ assert.strictEqual(r.status, 200);++ assert.match(r.text, /id="quick-start"/);++ assert.match(r.text, /npm install -g https:\/\/codeload\.github\.com\/OpenVibers\/OpenVibe\.Codes\/tar\.gz\/refs\/tags\/v\d+\.\d+\.\d+/);++ assert.match(r.text, /https:\/\/openvibe\.codes\/api\/v1\/harnesses\/route/);++ assert.match(r.text, /id="git"/, 'OpenVibe Git is on the home');++ assert.ok(!/class="codes-repos"/.test(r.text), 'no repository list while the forge has not answered');++ assert.ok(r.text.includes('git clone https://git.openvibe.codes/OpenVibers/OpenVibe.Codes.git'));++ assert.ok(r.text.includes('# an example run'), 'the hero transcript says it is an example');++ const blocks = [...r.text.matchAll(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/g)].map((m) => JSON.parse(m[1]));++ const faq = blocks.find((b) => b['@type'] === 'FAQPage');++ assert.ok(faq && faq.mainEntity.length === home.faqPairs().length && faq.mainEntity.length >= 5, 'a FAQPage node with every question');++ faq.mainEntity.forEach((q) => assert.ok(r.text.includes(`<h3>${q.name.replace(/'/g, ''')}</h3>`), `the page shows "${q.name}"`));++ assert.strictEqual((r.text.match(/<h1[\s>]/g) || []).length, 1, 'one h1');++++ t.ctx.git = { peek: () => ({ total: 52, publicUrl: 'https://git.openvibe.codes', repos: [{ name: 'OpenVibers/OpenVibe.Codes', url: 'https://git.openvibe.codes/OpenVibers/OpenVibe.Codes', updated: new Date().toISOString(), language: 'JavaScript' }] }) };++ r = await t.get('/');++ assert.match(r.text, /52 repositories on OpenVibe's own code forge|52 repositories on OpenVibe's own code forge/);++ assert.ok(r.text.includes('href="https://git.openvibe.codes/OpenVibers/OpenVibe.Codes"') && r.text.includes('<small>Code with any agent</small>'), 'a repository with its service tagline');++ });++ } finally { await t.close(); }++ done();++})().catch((e) => { console.error(e); process.exit(1); });@@@ -46,7 +46,7 @@ const BOOST = /<script src="\/shared\/boost\.js\?v=[0-9a-f]{12}" data-main="#maiassert.ok(head.includes('<link rel="canonical" href="https://openvibe.codes/">'), 'canonical');assert.ok(head.includes('<meta name="robots" content="index, follow">'), 'robots');const ld = [...head.matchAll(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/g)].map((m) => JSON.parse(m[1])['@type']);-- assert.deepStrictEqual(ld, ['WebSite', 'WebApplication', 'WebPage'], 'the home JSON-LD');++ assert.deepStrictEqual(ld, ['WebSite', 'WebApplication', 'WebPage', 'FAQPage'], 'the home JSON-LD (the FAQPage repeats the questions the page shows)');for (const tag of ['<meta property="og:title"', 'data-ov-icon="codes"', '/css/codes.css?v=', '/shared/theme-loader.js', '/shared/navbar.js', '/shared/footer.js', '<meta name="referrer"']) {assert.ok(head.includes(tag), `head has ${tag}`);}@@@ -12,13 +12,13 @@ const { spawn } = require('child_process');const { measure, check, format } = require('openvibe-shared/perf-budget');const BUDGETS = {-- htmlRawKB: 30, // measured 27.0 (fresh database; the showcase home, 2026-09-29: sections + six inline ring icons)-- htmlBrotliKB: 7.5, // 6.7++ htmlRawKB: 35, // 31.5 with the home's quick start, OpenVibe Git and six FAQs with their FAQPage JSON-LD (2026-10-22; in production the forge's eight repositories add about 2.5); measured 27.0 (fresh database; the showcase home, 2026-09-29: sections + six inline ring icons)++ htmlBrotliKB: 9, // 7.7 with those sections (2026-10-22); 6.7jsFiles: 5, // 5 (theme-loader, web-runtime, navbar, footer, boost: openvibe-shared/shell adds web-runtime, 2026-10-04)jsRawKB: 250, // 246.2 with openvibe-shared 4.2.x (Ask Actor in the navbar, 2026-10-15); 239.1 (212.2 before openvibe-shared/shell's web-runtime.js)jsBrotliKB: 60, // 59.1 with openvibe-shared 4.4.0 (every site in the launcher, 2026-10-17); 56.3 (49.9 before web-runtime.js)cssFiles: 2, // 1-- cssRawKB: 19, // 17.3 (codes.css + the cached /shared/showcase.css)++ cssRawKB: 21, // about 19.2 with the repository cards and FAQ styles (2026-10-22); 17.3 (codes.css + the cached /shared/showcase.css)cssBrotliKB: 5, // 4.1externalFiles: 2, // 0};@@@ -43,6 +43,7 @@ const INTERNAL = [`http://127.0.0.1:3000${PROBE}`, `http://2130706433${PROBE}`,'server/auth/sso.js': 'Network OAuth token and revoke (configured)','server/domain/run-client.js': 'OpenVibe.Run (CODES_RUN_URL, configured) and the Network token endpoint; job ids only, never a URL from a person','server/http/run.js': 'Network projects API (configured) with the viewer\'s token, and the Run job stream URL run-client.js builds on its own base',++ 'server/http/home.js': 'OpenVibe Git\'s public repository search (CODES_GIT_INTERNAL_URL, configured loopback Forgejo) for the home page; no token',};const root = path.join(__dirname, '..');const found = [];
@@ -80,3 +80,17 @@ meter.headroom { width: 8rem; max-width: 100%; vertical-align: middle; }
@@ -80,3 +80,17 @@ meter.headroom { width: 8rem; max-width: 100%; vertical-align: middle; }
.page .sc-ic .ov-icon { --ovi-size: 28px !important; }
.page .sc-ic .ov-icon { --ovi-size: 28px !important; }
.codes-hero-code { margin: 0; padding: 16px; border-radius: 14px; border: 1px solid var(--border, #1f2d47); background: var(--bg-primary, #0a0f1c); font-size: 13px; line-height: 1.6; overflow: auto; }
.codes-hero-code { margin: 0; padding: 16px; border-radius: 14px; border: 1px solid var(--border, #1f2d47); background: var(--bg-primary, #0a0f1c); font-size: 13px; line-height: 1.6; overflow: auto; }
+
+/* The home page: quick-start samples one under another, OpenVibe Git's repositories, and the FAQ. */
+#quick-start .sc-code { grid-template-columns: minmax(0, 1fr); }
+.codes-repos { list-style: none; margin: 0 0 12px; padding: 0; display: grid; gap: 10px; grid-template-columns: repeat(auto-fill, minmax(min(240px, 100%), 1fr)); }
+.codes-repos a { display: flex; flex-direction: column; gap: 3px; height: 100%; padding: 12px 14px; border-radius: 14px; border: 1px solid var(--border, #1f2d47); background: var(--bg-secondary, #101828); color: inherit; text-decoration: none; }
+.codes-repos a:hover, .codes-repos a:focus-visible { border-color: var(--accent, #3b82f6); }
+.codes-repos b { font-size: 14.5px; overflow-wrap: anywhere; }
+.codes-repos small { font-size: 13px; line-height: 1.4; color: var(--text-secondary, #96a7c2); }
+.codes-repos span { margin-top: auto; font-size: 12px; color: var(--text-muted, #7386a3); }
+.codes-clone { margin: 0; padding: 12px 14px; border-radius: 12px; border: 1px solid var(--border, #1f2d47); background: var(--bg-primary, #0a0f1c); font-size: 13px; overflow: auto; }
+.codes-faq { display: grid; gap: 12px; grid-template-columns: repeat(auto-fill, minmax(min(320px, 100%), 1fr)); }
+.codes-faq > div { padding: 16px 18px; border-radius: 16px; border: 1px solid var(--border, #1f2d47); background: var(--bg-secondary, #101828); }
+.codes-faq h3 { margin: 0 0 6px; font-size: 15.5px; line-height: 1.35; }
+.codes-faq p { margin: 0; font-size: 14px; line-height: 1.55; color: var(--text-secondary, #96a7c2); }
@@ -43,6 +43,12 @@ function load(env = process.env) {
@@ -43,6 +43,12 @@ function load(env = process.env) {
networkUrl,
networkUrl,
networkInternalUrl: trim(env.OV_NETWORK_INTERNAL_URL || 'http://127.0.0.1:4000'),
networkInternalUrl: trim(env.OV_NETWORK_INTERNAL_URL || 'http://127.0.0.1:4000'),
networkIssuer: trim(env.OV_NETWORK_ISSUER || networkUrl),
networkIssuer: trim(env.OV_NETWORK_ISSUER || networkUrl),
+ // OpenVibe Git (Forgejo, deploy/forgejo): its loopback address for the home page's repository list (public
+ // repositories only, no token), and the public address people use.
+ git: {
+ internalUrl: trim(env.CODES_GIT_INTERNAL_URL || 'http://127.0.0.1:5060'),
+ publicUrl: trim(env.CODES_GIT_URL || 'https://git.openvibe.codes'),
+ },
// The audience of the Network service tokens a first-party service calls /api/v1/tasks with (codes.task.*).
// The audience of the Network service tokens a first-party service calls /api/v1/tasks with (codes.task.*).
serviceAudience: env.CODES_AUDIENCE || 'openvibe.codes',
serviceAudience: env.CODES_AUDIENCE || 'openvibe.codes',
oauth: {
oauth: {
new file mode 100644
new file mode 100644
@@ -0,0 +1,105 @@
@@ -0,0 +1,105 @@
+'use strict';
+/**
+ * The home page's sections beyond the kit's generic ones: OpenVibe Git (the forge at git.openvibe.codes, read from its
+ * public API: how many repositories, which changed last), quick-start commands for the harness and the public router,
+ * and the questions people ask, which the FAQPage JSON-LD repeats. The repository list is read in the background at most
+ * every ten minutes and never awaited by a page; a forge that does not answer leaves the list out.
+ */
+const contracts = require('openvibe-contracts');
+const showcase = require('openvibe-shared/showcase');
+
+const esc = showcase.esc;
+
+function createGitReader({ internalUrl, publicUrl, enabled = true, fetchImpl = globalThis.fetch, ttlMs = 10 * 60_000, timeoutMs = 3000, limit = 8 } = {}) {
+ let value = null;
+ let at = 0;
+ let inflight = null;
+ const pub = String(publicUrl || 'https://git.openvibe.codes').replace(/\/+$/, '');
+ async function refresh() {
+ if (inflight) return inflight;
+ inflight = (async () => {
+ try {
+ const res = await fetchImpl(`${String(internalUrl).replace(/\/+$/, '')}/api/v1/repos/search?limit=${limit}&sort=updated&order=desc`, { headers: { Accept: 'application/json' }, signal: AbortSignal.timeout(timeoutMs) });
+ if (res.ok) {
+ const body = await res.json();
+ const total = Number(res.headers.get('x-total-count')) || null;
+ const repos = (Array.isArray(body.data) ? body.data : [])
+ .filter((r) => r && !r.private && /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/.test(String(r.full_name || '')))
+ .map((r) => ({ name: r.full_name, url: `${pub}/${r.full_name}`, updated: r.updated_at || null, language: r.language || '' }));
+ if (repos.length) value = { total: total && total >= repos.length ? total : null, repos, publicUrl: pub };
+ }
+ } catch { /* keep the last answer */ }
+ at = Date.now();
+ return value;
+ })().finally(() => { inflight = null; });
+ return inflight;
+ }
+ return {
+ peek() { if (enabled && Date.now() - at > ttlMs && !inflight) refresh().catch(() => {}); return value; }, // floating-ok: refresh catches its own errors
+ refresh,
+ /** Tests. */
+ _set(v) { value = v; at = Date.now(); },
+ };
+}
+
+// A repository's line: the tagline of the service it builds, from the pinned contracts, when one names it.
+const taglines = new Map(contracts.services.manifests.filter((m) => m && m.repository && m.site && m.site.tagline).map((m) => [m.repository, m.site.tagline]));
+function ago(iso, now = Date.now()) {
+ const t = Date.parse(iso || '');
+ if (!Number.isFinite(t)) return '';
+ const m = Math.max(0, Math.round((now - t) / 60000));
+ if (m < 60) return `${m} min ago`;
+ const h = Math.round(m / 60);
+ if (h < 48) return `${h} h ago`;
+ return `${Math.round(h / 24)} days ago`;
+}
+
+function gitSection(git, now = Date.now()) {
+ const pub = (git && git.publicUrl) || 'https://git.openvibe.codes';
+ const list = git && git.repos && git.repos.length ? `<ul class="codes-repos">${git.repos.map((r) => {
+ const line = taglines.get(r.name) || '';
+ return `<li><a href="${esc(r.url)}"><b>${esc(r.name.replace(/^OpenVibers\//, ''))}</b>${line ? `<small>${esc(line)}</small>` : ''}<span>${[r.language, r.updated ? `updated ${ago(r.updated, now)}` : ''].filter(Boolean).map(esc).join(' · ')}</span></a></li>`;
+ }).join('')}</ul>` : '';
+ const count = git && git.total ? `${git.total} repositories` : 'Every OpenVibe repository';
+ return `<section class="sc-sec" id="git" aria-labelledby="h-git"><h2 id="h-git">OpenVibe Git</h2>
+<p class="sc-lede">${esc(count)} on OpenVibe's own code forge at <a href="${esc(pub)}/">git.openvibe.codes</a>: browse, clone over HTTPS and sign in with your OpenVibe account, in the network's theme. The repositories are mirrored from GitHub every hour; GitHub stays the home of each one until it moves here.</p>
+${list}<pre tabindex="0" class="codes-clone"><code>git clone ${esc(pub)}/OpenVibers/OpenVibe.Codes.git</code></pre>
+<p class="sc-note"><a href="${esc(pub)}/OpenVibers">All OpenVibers repositories</a> · <a href="${esc(pub)}/user/login">Sign in with OpenVibe</a></p></section>`;
+}
+
+/** The harness from a terminal, and the public router any program can ask. */
+function quickStart({ install }) {
+ return showcase.code({
+ id: 'quick-start', title: 'Start in a minute',
+ lede: 'The harness runs on your machine with the agent CLIs and keys you already have. The router is public: ask it which agent fits a task, no key needed.',
+ samples: [
+ { label: 'Install and run', lang: 'bash', code: `${install}
+openvibe-codes agents # which agents this machine can run
+cd my-project
+openvibe-codes run "add a test for the date parser"
+openvibe-codes run --permission read "where is the session cookie set?"
+openvibe-codes runs # every run, kept to show or resume` },
+ { label: 'Ask the router (public API)', lang: 'bash', code: `curl -s https://openvibe.codes/api/v1/harnesses/route \\
+ -H 'Content-Type: application/json' -d '{ "task": "review" }'
+# → { "selected": "claude-code:claude-code-sonnet",
+# "reasons": [ "no marginal cost (already-paid capacity or a free allowance)", … ],
+# "candidates": [ … every agent, with why it was or was not picked ] }` },
+ ],
+ });
+}
+
+// The questions people ask. The first three are Codes' own in the contracts (the answers OpenVibe.Help shows).
+const EXTRA_FAQ = [
+ { q: 'Does it cost anything?', a: 'The harness is free and open source. Each agent runs with your own CLI and keys, so you pay its provider as you already do; the router prefers capacity you have already paid for and free allowances.' },
+ { q: 'What happens when an agent gets stuck?', a: 'A failed, rate-limited or silent agent is stopped and the task goes to the next one with a note: the task, why the last agent stopped, the files it changed and the state of the tree.' },
+ { q: 'What is OpenVibe Git?', a: 'OpenVibe\'s own code forge at git.openvibe.codes, where you sign in with your OpenVibe account. It holds every public OpenVibe repository, mirrored from GitHub every hour.' },
+];
+function faqPairs() {
+ const m = contracts.services.get('codes');
+ const own = ((m && m.site && m.site.faq) || []).filter((f) => f && f.question && f.answer).map((f) => ({ q: f.question, a: f.answer }));
+ const seen = new Set();
+ return [...own, ...EXTRA_FAQ].filter((p) => !seen.has(p.q) && seen.add(p.q));
+}
+const faq = () => `<section class="sc-sec" id="faq" aria-labelledby="h-faq"><h2 id="h-faq">Questions people ask</h2><div class="codes-faq">${faqPairs().map((p) => `<div><h3>${esc(p.q)}</h3><p>${esc(p.a)}</p></div>`).join('')}</div></section>`;
+
+module.exports = { createGitReader, gitSection, quickStart, faq, faqPairs, ago };
@@ -24,6 +24,8 @@ const { html, raw, table, notice } = require('../render/html');
@@ -24,6 +24,8 @@ const { html, raw, table, notice } = require('../render/html');
const { send } = require('../render/layout');
const { send } = require('../render/layout');
const { markdown } = require('../render/markdown');
const { markdown } = require('../render/markdown');
const placement = require('../domain/harness-placement');
const placement = require('../domain/harness-placement');
+const home = require('./home');
+const seo = require('openvibe-shared/seo');
const ROOT = path.join(__dirname, '..', '..');
const ROOT = path.join(__dirname, '..', '..');
const GITHUB = 'https://github.com/OpenVibers';
const GITHUB = 'https://github.com/OpenVibers';
@@ -76,12 +78,16 @@ function createPageRoutes(ctx) {
@@ -76,12 +78,16 @@ function createPageRoutes(ctx) {
const PUBLIC_CACHE = cache.htmlHeaders({ maxAge: 300 });
const PUBLIC_CACHE = cache.htmlHeaders({ maxAge: 300 });
const governance = loadGovernance();
const governance = loadGovernance();
const page = (req, res, o, status = 200) => send(res, status, { viewer: req.viewer, config, path: req.originalUrl, ...o });
const page = (req, res, o, status = 200) => send(res, status, { viewer: req.viewer, config, path: req.originalUrl, ...o });
+ // OpenVibe Git's public repositories for the home (read in the background, never awaited here); tests set ctx.git.
+ 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) => {
r.get('/', (req, res) => {
const offers = harnesses.list();
const offers = harnesses.list();
const tasks = Object.keys(placement.TASKS);
const tasks = Object.keys(placement.TASKS);
- const snippet = `$ openvibe-codes run "add a test for the date parser"
+ // What a run prints (bin/openvibe-codes.js), as an example: Codex hits its limit and Claude Code finishes.
+ const snippet = `# an example run: one agent hits its limit, the next one finishes
+$ openvibe-codes run "add a test for the date parser"
run run_20261008… · ~/my-project
run run_20261008… · ~/my-project
▶ codex · session 0199…
▶ codex · session 0199…
! You have hit your usage limit.
! You have hit your usage limit.
@@ -93,7 +99,7 @@ Added test/date.test.js: three cases, all pass.
@@ -93,7 +99,7 @@ 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, {
page(req, res, {
index: true, cache: PUBLIC_CACHE,
index: true, cache: PUBLIC_CACHE,
- jsonLd: homeJsonLd(config),
+ jsonLd: [...homeJsonLd(config), seo.jsonLd.faq(home.faqPairs())],
styles: [showcase.STYLESHEET],
styles: [showcase.STYLESHEET],
body: html`${raw(showcase.hero({
body: html`${raw(showcase.hero({
eyebrow: 'OpenVibe.Codes · the open coding-agent harness · alpha',
eyebrow: 'OpenVibe.Codes · the open coding-agent harness · alpha',
@@ -116,6 +122,8 @@ ${raw(showcase.steps({
@@ -116,6 +122,8 @@ ${raw(showcase.steps({
{ title: 'Hand-offs', text: 'When an agent fails, hits its usage limit or goes quiet, the task moves to the next one with a note: the task, the files touched and the state of the tree.' },
{ title: 'Hand-offs', text: 'When an agent fails, hits its usage limit or goes quiet, the task moves to the next one with a note: the task, the files touched and the state of the tree.' },
],
],
}))}
}))}
+${raw(home.quickStart({ install: INSTALL }))}
+${raw(home.gitSection((ctx.git || gitReader).peek()))}
<section class="sc-sec" aria-labelledby="h-improve"><h2 id="h-improve">Improve OpenVibe</h2>
<section class="sc-sec" aria-labelledby="h-improve"><h2 id="h-improve">Improve OpenVibe</h2>
<p class="sc-lede">OpenVibe is built in the open, ${repositories().running.length} services and their shared libraries, every one on GitHub. Pick a repository, describe the change, and send a pull request; the harness's job is to make that one step.</p>
<p class="sc-lede">OpenVibe is built in the open, ${repositories().running.length} services and their shared libraries, every one on GitHub. Pick a repository, describe the change, and send a pull request; the harness's job is to make that one step.</p>
<p><a class="sc-btn sc-primary" href="/improve">Start contributing</a> <a class="sc-btn" href="/policy/contributing">Read the guide</a></p></section>
<p><a class="sc-btn sc-primary" href="/improve">Start contributing</a> <a class="sc-btn" href="/policy/contributing">Read the guide</a></p></section>
@@ -127,7 +135,9 @@ ${table(['Piece', 'State'], [
@@ -127,7 +135,9 @@ ${table(['Piece', 'State'], [
[html`<a href="/harnesses">The agent catalog</a>`, 'Live: every harness and model with its capabilities, prices and limits.'],
[html`<a href="/harnesses">The agent catalog</a>`, 'Live: every harness and model with its capabilities, prices and limits.'],
[html`<code>GET /api/v1/harnesses</code>, <code>POST /api/v1/harnesses/route</code>`, 'Live: public, no key needed; per-address and per-caller limits.'],
[html`<code>GET /api/v1/harnesses</code>, <code>POST /api/v1/harnesses/route</code>`, 'Live: public, no key needed; per-address and per-caller limits.'],
[html`<a href="/improve">Improve OpenVibe</a>: a repository, a change on a branch with its test, a pull request that CI checks and a maintainer reviews`, 'Live: by hand, or with the harness doing the change on your machine.'],
[html`<a href="/improve">Improve OpenVibe</a>: a repository, a change on a branch with its test, a pull request that CI checks and a maintainer reviews`, 'Live: by hand, or with the harness doing the change on your machine.'],
+ [html`<a href="${config.git.publicUrl}/">OpenVibe Git</a>: OpenVibe's own code forge`, 'Live: every public OpenVibe repository, mirrored from GitHub hourly, with OpenVibe sign-in and HTTPS cloning. GitHub stays canonical until a repository moves here.'],
])}</section>
])}</section>
+${raw(home.faq())}
${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' }] }))}`,
});
});
});
});
new file mode 100644
new file mode 100644
@@ -0,0 +1,61 @@
@@ -0,0 +1,61 @@
+'use strict';
+/**
+ * The home page's own sections (server/http/home.js): OpenVibe Git from the forge's public API (left out when it never
+ * answered), the quick start with the release-tag install line and the public router, and a FAQ that its FAQPage JSON-LD
+ * repeats word for word. The git reader keeps only public repositories with a plain owner/name.
+ */
+const assert = require('assert');
+const http = require('http');
+const home = require('../server/http/home');
+const { boot, check, done } = require('./helpers/boot');
+
+(async () => {
+ await check('the git reader: public repositories, newest first as the forge answers, with the total it reports', async () => {
+ const srv = http.createServer((req, res) => {
+ assert.match(req.url, /^\/api\/v1\/repos\/search\?limit=8&sort=updated&order=desc$/);
+ res.setHeader('Content-Type', 'application/json');
+ res.setHeader('X-Total-Count', '52');
+ res.end(JSON.stringify({ ok: true, data: [
+ { full_name: 'OpenVibers/OpenVibe.Codes', private: false, updated_at: '2026-10-22T10:00:00Z', language: 'JavaScript' },
+ { full_name: 'OpenVibers/secret', private: true },
+ { full_name: 'bad name/<x>', private: false },
+ ] }));
+ });
+ await new Promise((r) => srv.listen(0, '127.0.0.1', r));
+ try {
+ const reader = home.createGitReader({ internalUrl: `http://127.0.0.1:${srv.address().port}`, publicUrl: 'https://git.openvibe.codes' });
+ const v = await reader.refresh();
+ assert.strictEqual(v.total, 52);
+ assert.deepStrictEqual(v.repos.map((r) => r.url), ['https://git.openvibe.codes/OpenVibers/OpenVibe.Codes']);
+ } finally { srv.close(); }
+ const down = home.createGitReader({ internalUrl: 'http://127.0.0.1:9', timeoutMs: 500 });
+ assert.strictEqual(await down.refresh(), null, 'a forge that does not answer gives no list');
+ assert.strictEqual(home.ago('2026-10-22T10:00:00Z', Date.parse('2026-10-22T12:00:00Z')), '2 h ago');
+ });
+
+ const t = await boot();
+ try {
+ await check('the home: quick start, OpenVibe Git, a FAQ and its JSON-LD', async () => {
+ let r = await t.get('/');
+ assert.strictEqual(r.status, 200);
+ assert.match(r.text, /id="quick-start"/);
+ assert.match(r.text, /npm install -g https:\/\/codeload\.github\.com\/OpenVibers\/OpenVibe\.Codes\/tar\.gz\/refs\/tags\/v\d+\.\d+\.\d+/);
+ assert.match(r.text, /https:\/\/openvibe\.codes\/api\/v1\/harnesses\/route/);
+ assert.match(r.text, /id="git"/, 'OpenVibe Git is on the home');
+ assert.ok(!/class="codes-repos"/.test(r.text), 'no repository list while the forge has not answered');
+ assert.ok(r.text.includes('git clone https://git.openvibe.codes/OpenVibers/OpenVibe.Codes.git'));
+ assert.ok(r.text.includes('# an example run'), 'the hero transcript says it is an example');
+ const blocks = [...r.text.matchAll(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/g)].map((m) => JSON.parse(m[1]));
+ const faq = blocks.find((b) => b['@type'] === 'FAQPage');
+ assert.ok(faq && faq.mainEntity.length === home.faqPairs().length && faq.mainEntity.length >= 5, 'a FAQPage node with every question');
+ faq.mainEntity.forEach((q) => assert.ok(r.text.includes(`<h3>${q.name.replace(/'/g, ''')}</h3>`), `the page shows "${q.name}"`));
+ assert.strictEqual((r.text.match(/<h1[\s>]/g) || []).length, 1, 'one h1');
+
+ t.ctx.git = { peek: () => ({ total: 52, publicUrl: 'https://git.openvibe.codes', repos: [{ name: 'OpenVibers/OpenVibe.Codes', url: 'https://git.openvibe.codes/OpenVibers/OpenVibe.Codes', updated: new Date().toISOString(), language: 'JavaScript' }] }) };
+ r = await t.get('/');
+ assert.match(r.text, /52 repositories on OpenVibe's own code forge|52 repositories on OpenVibe's own code forge/);
+ assert.ok(r.text.includes('href="https://git.openvibe.codes/OpenVibers/OpenVibe.Codes"') && r.text.includes('<small>Code with any agent</small>'), 'a repository with its service tagline');
+ });
+ } finally { await t.close(); }
+ done();
+})().catch((e) => { console.error(e); process.exit(1); });
@@ -46,7 +46,7 @@ const BOOST = /<script src="\/shared\/boost\.js\?v=[0-9a-f]{12}" data-main="#mai
@@ -46,7 +46,7 @@ const BOOST = /<script src="\/shared\/boost\.js\?v=[0-9a-f]{12}" data-main="#mai
assert.ok(head.includes('<link rel="canonical" href="https://openvibe.codes/">'), 'canonical');
assert.ok(head.includes('<link rel="canonical" href="https://openvibe.codes/">'), 'canonical');
assert.ok(head.includes('<meta name="robots" content="index, follow">'), 'robots');
assert.ok(head.includes('<meta name="robots" content="index, follow">'), 'robots');
const ld = [...head.matchAll(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/g)].map((m) => JSON.parse(m[1])['@type']);
const ld = [...head.matchAll(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/g)].map((m) => JSON.parse(m[1])['@type']);
- assert.deepStrictEqual(ld, ['WebSite', 'WebApplication', 'WebPage'], 'the home JSON-LD');
+ assert.deepStrictEqual(ld, ['WebSite', 'WebApplication', 'WebPage', 'FAQPage'], 'the home JSON-LD (the FAQPage repeats the questions the page shows)');
for (const tag of ['<meta property="og:title"', 'data-ov-icon="codes"', '/css/codes.css?v=', '/shared/theme-loader.js', '/shared/navbar.js', '/shared/footer.js', '<meta name="referrer"']) {
for (const tag of ['<meta property="og:title"', 'data-ov-icon="codes"', '/css/codes.css?v=', '/shared/theme-loader.js', '/shared/navbar.js', '/shared/footer.js', '<meta name="referrer"']) {
assert.ok(head.includes(tag), `head has ${tag}`);
assert.ok(head.includes(tag), `head has ${tag}`);
}
}
@@ -12,13 +12,13 @@ const { spawn } = require('child_process');
@@ -12,13 +12,13 @@ const { spawn } = require('child_process');
const { measure, check, format } = require('openvibe-shared/perf-budget');
const { measure, check, format } = require('openvibe-shared/perf-budget');
const BUDGETS = {
const BUDGETS = {
- htmlRawKB: 30, // measured 27.0 (fresh database; the showcase home, 2026-09-29: sections + six inline ring icons)
- htmlBrotliKB: 7.5, // 6.7
+ htmlRawKB: 35, // 31.5 with the home's quick start, OpenVibe Git and six FAQs with their FAQPage JSON-LD (2026-10-22; in production the forge's eight repositories add about 2.5); measured 27.0 (fresh database; the showcase home, 2026-09-29: sections + six inline ring icons)
+ htmlBrotliKB: 9, // 7.7 with those sections (2026-10-22); 6.7
jsFiles: 5, // 5 (theme-loader, web-runtime, navbar, footer, boost: openvibe-shared/shell adds web-runtime, 2026-10-04)
jsFiles: 5, // 5 (theme-loader, web-runtime, navbar, footer, boost: openvibe-shared/shell adds web-runtime, 2026-10-04)
jsRawKB: 250, // 246.2 with openvibe-shared 4.2.x (Ask Actor in the navbar, 2026-10-15); 239.1 (212.2 before openvibe-shared/shell's web-runtime.js)
jsRawKB: 250, // 246.2 with openvibe-shared 4.2.x (Ask Actor in the navbar, 2026-10-15); 239.1 (212.2 before openvibe-shared/shell's web-runtime.js)
jsBrotliKB: 60, // 59.1 with openvibe-shared 4.4.0 (every site in the launcher, 2026-10-17); 56.3 (49.9 before web-runtime.js)
jsBrotliKB: 60, // 59.1 with openvibe-shared 4.4.0 (every site in the launcher, 2026-10-17); 56.3 (49.9 before web-runtime.js)
cssFiles: 2, // 1
cssFiles: 2, // 1
- cssRawKB: 19, // 17.3 (codes.css + the cached /shared/showcase.css)
+ cssRawKB: 21, // about 19.2 with the repository cards and FAQ styles (2026-10-22); 17.3 (codes.css + the cached /shared/showcase.css)
cssBrotliKB: 5, // 4.1
cssBrotliKB: 5, // 4.1
externalFiles: 2, // 0
externalFiles: 2, // 0
};
};
@@ -43,6 +43,7 @@ const INTERNAL = [`http://127.0.0.1:3000${PROBE}`, `http://2130706433${PROBE}`,
@@ -43,6 +43,7 @@ const INTERNAL = [`http://127.0.0.1:3000${PROBE}`, `http://2130706433${PROBE}`,
'server/auth/sso.js': 'Network OAuth token and revoke (configured)',
'server/auth/sso.js': 'Network OAuth token and revoke (configured)',
'server/domain/run-client.js': 'OpenVibe.Run (CODES_RUN_URL, configured) and the Network token endpoint; job ids only, never a URL from a person',
'server/domain/run-client.js': 'OpenVibe.Run (CODES_RUN_URL, configured) and the Network token endpoint; job ids only, never a URL from a person',
'server/http/run.js': 'Network projects API (configured) with the viewer\'s token, and the Run job stream URL run-client.js builds on its own base',
'server/http/run.js': 'Network projects API (configured) with the viewer\'s token, and the Run job stream URL run-client.js builds on its own base',
+ 'server/http/home.js': 'OpenVibe Git\'s public repository search (CODES_GIT_INTERNAL_URL, configured loopback Forgejo) for the home page; no token',
};
};
const root = path.join(__dirname, '..');
const root = path.join(__dirname, '..');
const found = [];
const found = [];