mirror of
https://github.com/hanzoai/bot-hub.git
synced 2026-10-30 18:46:42 +00:00
Every route mounts under /v1/skills, the address api.hanzo.ai routes to this API; auth, personas, users, tokens, upload, integrations, search and whoami are sub-resources and RESERVED keeps them from being slugs. The web, the OG renderers, the CLI's default registry, /.well-known/bothub.json and openapi.json name api.hanzo.ai. Sign-in calls back to api.hanzo.ai/v1/skills/auth/callback and hands the token over in the fragment. The store starts again. docker-entrypoint.sh read Base's port from BASE_PORT, which Kubernetes sets to tcp://<ip>:<port> in a namespace holding a Service named base; Base now listens on 127.0.0.1:8090, fixed. The collections migration is rewritten for the Base the Dockerfile pins (1.5.91): flat field options, declared created/updated, and the users collection every Base already has, extended rather than created twice. Reads are anonymous against public rules, since Base takes IAM credentials only and no longer serves password sign-in; writes answer 503 saying so. The catalogue migration, generated by scripts/catalog.mjs, publishes the 83 skills in hanzoai/bot@8be8f1dc7b. /health reports the store, so the probes restart a pod whose Base is gone. cache.ts decides caching once: an anonymous 2xx GET is public for a minute (five at the edge) with a weak ETag and 304s; a credential, a write or an error is private, no-store. Filter values go through lit(). Embeddings go through api.hanzo.ai and are skipped without a key. k8s/ was a second declaration of the deployment; universe is the one.
151 lines
5.1 KiB
JavaScript
151 lines
5.1 KiB
JavaScript
#!/usr/bin/env node
|
|
// Write the catalogue migration: every skill Hanzo Bot ships, as Base records.
|
|
//
|
|
// git -C ../bot archive <ref> skills | tar -x -C /tmp/bot
|
|
// node scripts/catalog.mjs /tmp/bot/skills <commit>
|
|
//
|
|
// Each directory holding a SKILL.md is one skill: its slug is the directory
|
|
// name, its display name and summary come from the SKILL.md front matter, and
|
|
// every file in the directory is carried inline, so the hub serves the skill
|
|
// with no object storage behind it. The output replaces
|
|
// base/hz_migrations/1709000001_catalog.js; rerun it against a newer commit to
|
|
// move the catalogue.
|
|
|
|
import { createHash } from 'node:crypto'
|
|
import { readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
|
|
import { join, relative } from 'node:path'
|
|
|
|
const [root, commit] = process.argv.slice(2)
|
|
if (!root || !/^[0-9a-f]{40}$/.test(commit ?? '')) {
|
|
console.error('usage: node scripts/catalog.mjs <bot skills dir> <40-hex commit>')
|
|
process.exit(2)
|
|
}
|
|
|
|
const RESERVED = new Set([
|
|
'auth', 'changelog-preview', 'download', 'integrations', 'personas', 'resolve',
|
|
'search', 'stars', 'telemetry', 'tokens', 'upload', 'users', 'whoami',
|
|
])
|
|
|
|
function frontmatter(text) {
|
|
const m = text.match(/^---\n([\s\S]*?)\n---\n/)
|
|
if (!m) return {}
|
|
const field = (key) => {
|
|
const line = m[1].match(new RegExp(`^${key}:\\s*(.+)$`, 'm'))?.[1]?.trim()
|
|
if (!line) return undefined
|
|
return line.startsWith('"') ? JSON.parse(line) : line.replace(/^'(.*)'$/, '$1')
|
|
}
|
|
return { name: field('name'), description: field('description'), version: field('version') }
|
|
}
|
|
|
|
function files(dir) {
|
|
return readdirSync(dir, { recursive: true })
|
|
.map((entry) => join(dir, entry))
|
|
.filter((path) => statSync(path).isFile())
|
|
.sort()
|
|
}
|
|
|
|
const skills = []
|
|
for (const slug of readdirSync(root).sort()) {
|
|
const dir = join(root, slug)
|
|
if (!statSync(dir).isDirectory()) continue
|
|
const skillMd = join(dir, 'SKILL.md')
|
|
try { statSync(skillMd) } catch { continue }
|
|
if (!/^[a-z0-9][a-z0-9-]*$/.test(slug)) throw new Error(`${slug}: not a url-safe slug`)
|
|
if (RESERVED.has(slug)) throw new Error(`${slug}: names a route under /v1/skills`)
|
|
|
|
const meta = frontmatter(readFileSync(skillMd, 'utf8'))
|
|
const entries = files(dir).map((path) => {
|
|
const bytes = readFileSync(path)
|
|
const content = bytes.toString('utf8')
|
|
if (Buffer.from(content, 'utf8').compare(bytes) !== 0) throw new Error(`${path}: not UTF-8 text`)
|
|
return {
|
|
path: relative(dir, path),
|
|
size: bytes.length,
|
|
sha256: createHash('sha256').update(bytes).digest('hex'),
|
|
contentType: path.endsWith('.md') ? 'text/markdown' : 'text/plain',
|
|
content,
|
|
}
|
|
})
|
|
const digest = createHash('sha256')
|
|
for (const f of entries) digest.update(`${f.path}\0${f.sha256}\n`)
|
|
|
|
skills.push({
|
|
slug,
|
|
displayName: meta.name ?? slug,
|
|
summary: meta.description ?? '',
|
|
version: meta.version ?? '1.0.0',
|
|
sha256hash: digest.digest('hex'),
|
|
files: entries,
|
|
})
|
|
}
|
|
|
|
const out = `/// <reference path="../base_/data/types.d.ts" />
|
|
|
|
// GENERATED by scripts/catalog.mjs from github.com/hanzoai/bot@${commit}/skills.
|
|
// Do not edit; rerun the script.
|
|
//
|
|
// The catalogue: ${skills.length} skills Hanzo Bot ships, published by the hanzo account.
|
|
|
|
migrate((app) => {
|
|
const catalog = ${JSON.stringify(skills).replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029')}
|
|
|
|
const users = app.findCollectionByNameOrId("users")
|
|
const hanzo = new Record(users, {
|
|
handle: "hanzo",
|
|
displayName: "Hanzo",
|
|
name: "Hanzo AI",
|
|
role: "admin",
|
|
trustedPublisher: true,
|
|
image: "https://hanzo.ai/favicon.svg",
|
|
})
|
|
hanzo.setEmail("[email protected]")
|
|
hanzo.setVerified(true)
|
|
hanzo.set("password", $security.randomString(48))
|
|
app.save(hanzo)
|
|
|
|
const skills = app.findCollectionByNameOrId("skills")
|
|
const versions = app.findCollectionByNameOrId("skill_versions")
|
|
for (const s of catalog) {
|
|
const skill = new Record(skills, {
|
|
slug: s.slug,
|
|
displayName: s.displayName,
|
|
summary: s.summary,
|
|
ownerUserId: hanzo.id,
|
|
tags: {},
|
|
badges: { official: { byUserId: hanzo.id, at: Date.now() } },
|
|
moderationStatus: "active",
|
|
statsDownloads: 0,
|
|
statsStars: 0,
|
|
statsVersions: 1,
|
|
statsComments: 0,
|
|
})
|
|
app.save(skill)
|
|
|
|
const version = new Record(versions, {
|
|
skillId: skill.id,
|
|
version: s.version,
|
|
changelog: "Published from github.com/hanzoai/bot@${commit.slice(0, 12)}.",
|
|
changelogSource: "auto",
|
|
files: s.files,
|
|
parsed: { frontmatter: { name: s.displayName, description: s.summary } },
|
|
createdBy: hanzo.id,
|
|
sha256hash: s.sha256hash,
|
|
})
|
|
app.save(version)
|
|
|
|
skill.set("latestVersionId", version.id)
|
|
app.save(skill)
|
|
}
|
|
}, (app) => {
|
|
const hanzo = app.findFirstRecordByData("users", "handle", "hanzo")
|
|
for (const skill of app.findRecordsByFilter("skills", "ownerUserId = {:id}", "", 0, 0, { id: hanzo.id })) {
|
|
app.delete(skill)
|
|
}
|
|
app.delete(hanzo)
|
|
})
|
|
`
|
|
|
|
const target = new URL('../base/hz_migrations/1709000001_catalog.js', import.meta.url)
|
|
writeFileSync(target, out)
|
|
console.log(`${skills.length} skills, ${skills.reduce((n, s) => n + s.files.length, 0)} files -> ${target.pathname}`)
|